Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05884533d1 | ||
|
|
310daa8060 | ||
|
|
7b798ae543 | ||
|
|
3143f87587 | ||
|
|
5fb3626208 | ||
|
|
fb2d1fb2f2 | ||
|
|
cadbf6f275 | ||
|
|
ea5d315dc3 | ||
|
|
46990eb632 | ||
|
|
71e799bb92 | ||
|
|
09069b0095 | ||
|
|
01c44f6877 | ||
|
|
3b94e68af7 | ||
|
|
5f598ff70c | ||
|
|
b8064ce6e3 | ||
|
|
3b8202d21b | ||
|
|
94a75aa100 | ||
|
|
cc62bbc3ec | ||
|
|
78869a13f6 | ||
|
|
c1028e7431 | ||
|
|
aa977c5a32 | ||
|
|
06eb4929a7 | ||
|
|
befd6f9a08 | ||
|
|
053526d2da | ||
|
|
1758dc1333 | ||
|
|
a0d2430319 | ||
|
|
b44e49b4d9 | ||
|
|
ec2e359dd4 | ||
|
|
4d191d7bbd | ||
|
|
930ad5b25e | ||
|
|
ce9ed6de9b | ||
|
|
7be530b445 | ||
|
|
7505766a83 | ||
|
|
328a82835e | ||
|
|
e43e9ace68 | ||
|
|
a7111b0e24 | ||
|
|
6a7e11cd2c | ||
|
|
5444a82c7f | ||
|
|
8b1b9b427d | ||
|
|
ba5eaec957 | ||
|
|
34557c7cff | ||
|
|
b8e4f64520 | ||
|
|
925df6c7f1 | ||
|
|
f84f63cc64 | ||
|
|
23a3d08e85 | ||
|
|
2d890d1189 | ||
|
|
b55e9abd7e | ||
|
|
c71241288c | ||
|
|
158fc71813 | ||
|
|
44900bd5c4 | ||
|
|
031bdb05dd | ||
|
|
5d0d218137 | ||
|
|
755b3b1adc | ||
|
|
ac6bbf8c2f | ||
|
|
896524a6c0 | ||
|
|
9dbd2ac260 | ||
|
|
91766f5b0f | ||
|
|
192fb96552 | ||
|
|
5a5b8625dd | ||
|
|
c5c416dd65 | ||
|
|
9c41888721 | ||
|
|
30323568c4 | ||
|
|
c9c5476cf2 | ||
|
|
44aebe054e | ||
|
|
01ae14522d | ||
|
|
3d81c7fc16 | ||
|
|
8aad7bcdf6 | ||
|
|
f2d86d634e | ||
|
|
8f19bed2b6 | ||
|
|
e1da2802b0 | ||
|
|
72ad7cc53d | ||
|
|
2e60e05fd1 | ||
|
|
d6bff17544 | ||
|
|
82c9884654 | ||
|
|
efc1b8385b | ||
|
|
a2faa84f86 | ||
|
|
4f48b3f5be | ||
|
|
5168f1d8a7 | ||
|
|
4fea65f982 | ||
|
|
b42fae4054 | ||
|
|
7b5c0026f9 | ||
|
|
c315b52cb2 | ||
|
|
b384cacbe3 | ||
|
|
d208769446 | ||
|
|
2423989192 | ||
|
|
de9524d16b | ||
|
|
8e6aa67412 | ||
|
|
b89b90f4ba | ||
|
|
7f2d4b01e8 | ||
|
|
714387ad7c | ||
|
|
e80686fe01 | ||
|
|
90b5d7d055 | ||
|
|
74d37a710c | ||
|
|
c4a9bb3178 | ||
|
|
bc787972cc | ||
|
|
aa8ed82379 | ||
|
|
b0c04ca0ec | ||
|
|
4b84885d19 | ||
|
|
382672bc5e | ||
|
|
4a6f9449d0 | ||
|
|
dc03894ea4 | ||
|
|
c957ed8605 | ||
|
|
ac978048b0 | ||
|
|
f6cc0edded | ||
|
|
efc01f9d20 | ||
|
|
4047b93f89 | ||
|
|
03f43f4408 | ||
|
|
bfb3dea059 | ||
|
|
d7ea126d2e | ||
|
|
17bc14e9c1 | ||
|
|
a09645669c | ||
|
|
60aeb5bddc | ||
|
|
26e50da938 | ||
|
|
5d0b00204f | ||
|
|
5ef410fb70 | ||
|
|
e655ee3bb9 | ||
|
|
b75efedf7b | ||
|
|
1a8fbb97e9 | ||
|
|
db6c027cac | ||
|
|
66ac1429b0 | ||
|
|
0acc731113 | ||
|
|
07a9fbc2e1 | ||
|
|
2358c01c97 | ||
|
|
10484cb0d6 | ||
|
|
5176bda496 | ||
|
|
6f5e0d3491 | ||
|
|
d0f1964dd9 | ||
|
|
3c99e015aa | ||
|
|
775f43ce03 | ||
|
|
b4cb80bc68 | ||
|
|
31c74b12c2 | ||
|
|
51e959e972 | ||
|
|
e871813a2b | ||
|
|
74e7a6fb68 | ||
|
|
5c652df904 | ||
|
|
9c31f04478 | ||
|
|
b18498e880 | ||
|
|
851f9eeb97 | ||
|
|
79e76d688e | ||
|
|
c364a6b9ef | ||
|
|
eceefe363a | ||
|
|
522c514b1a | ||
|
|
d42758b189 | ||
|
|
150cb01649 | ||
|
|
a9daac317e | ||
|
|
cb103a3f83 | ||
|
|
3f9f3857f1 | ||
|
|
12a9b5029c | ||
|
|
f79838687e | ||
|
|
95b880b92e | ||
|
|
b499e10590 | ||
|
|
b1e9f1d5a5 | ||
|
|
20b8db3755 | ||
|
|
71789e12fa | ||
|
|
afc689839a | ||
|
|
4e88bec0e4 | ||
|
|
ad9bf6f846 | ||
|
|
269d72c078 | ||
|
|
3a685e5671 | ||
|
|
a68633091a | ||
|
|
d116a2bea3 | ||
|
|
07b14c3f77 | ||
|
|
7aec2801a1 | ||
|
|
6e6cea89f7 | ||
|
|
5a4947372f | ||
|
|
7eb0abf4da | ||
|
|
67cf5659d4 | ||
|
|
8d8d83bc25 | ||
|
|
2860089712 | ||
|
|
908b3ca4b2 | ||
|
|
54bb79b294 | ||
|
|
2e4ac644f8 | ||
|
|
fbdf360fd9 | ||
|
|
a1245101de | ||
|
|
698244e3f1 | ||
|
|
8c062da5a1 | ||
|
|
f38adabb8c | ||
|
|
3cb97f6e27 | ||
|
|
ddb94e4480 | ||
|
|
eea1857a37 | ||
|
|
a248460e70 | ||
|
|
ead36404c0 | ||
|
|
78add9ad9f | ||
|
|
a2b130f75f | ||
|
|
3d10dd97f9 | ||
|
|
6da7c23fc2 | ||
|
|
77f00914d3 | ||
|
|
ff9c0886ce | ||
|
|
dc07404b48 | ||
|
|
7d0a159aed | ||
|
|
0ace45ca61 | ||
|
|
b4356408b6 | ||
|
|
1f81167f9e | ||
|
|
3c9cb68831 | ||
|
|
db2b663547 | ||
|
|
1d96a747e4 | ||
|
|
025cc19b6c | ||
|
|
a8180ab9d2 | ||
|
|
44eff703e0 | ||
|
|
96d29b9e3a | ||
|
|
e4b1824dd4 | ||
|
|
9fcf02df73 | ||
|
|
e395620e29 | ||
|
|
157f608eaa | ||
|
|
8d55619bb1 | ||
|
|
676a35da56 | ||
|
|
e8fd945879 | ||
|
|
798919690e | ||
|
|
1b96c43fa5 | ||
|
|
64762f064b | ||
|
|
b79dea809a | ||
|
|
8b2453b0bc | ||
|
|
09c6a6e22d | ||
|
|
73c086a5b1 | ||
|
|
8c98d99a8f | ||
|
|
3e4c5a5a28 | ||
|
|
a5a893a417 | ||
|
|
1eff50d243 | ||
|
|
cb82f90590 | ||
|
|
492bb56513 |
+1
-1
@@ -12,7 +12,7 @@ deploy:
|
||||
script:
|
||||
- echo "Building with gradle version ${BUILDVERSION}"
|
||||
- gradle -Pversion=${BUILDVERSION} publish
|
||||
- gradle bootBuildImage --cleanCache --publishImage -PbuildbootDockerHostNetwork=true -Pversion=${BUILDVERSION}
|
||||
- gradle bootBuildImage --publishImage -PbuildbootDockerHostNetwork=true -Pversion=${BUILDVERSION}
|
||||
- echo "BUILDVERSION=$BUILDVERSION" >> version.env
|
||||
artifacts:
|
||||
reports:
|
||||
|
||||
@@ -11,6 +11,10 @@ group = "com.iqser.red.service"
|
||||
java.sourceCompatibility = JavaVersion.VERSION_17
|
||||
java.targetCompatibility = JavaVersion.VERSION_17
|
||||
|
||||
pmd {
|
||||
isConsoleOutput = true
|
||||
}
|
||||
|
||||
tasks.pmdMain {
|
||||
pmd.ruleSetFiles = files("${rootDir}/config/pmd/pmd.xml")
|
||||
}
|
||||
|
||||
+3
-4
@@ -9,13 +9,12 @@
|
||||
</description>
|
||||
|
||||
<rule ref="category/java/errorprone.xml">
|
||||
<exclude name="DataflowAnomalyAnalysis"/>
|
||||
<exclude name="MissingSerialVersionUID"/>
|
||||
<exclude name="NullAssignment"/>
|
||||
<exclude name="AvoidLiteralsInIfCondition"/>
|
||||
<exclude name="AvoidDuplicateLiterals"/>
|
||||
<exclude name="AvoidFieldNameMatchingMethodName"/>
|
||||
<exclude name="NullAssignment"/>
|
||||
<exclude name="AssignmentInOperand"/>
|
||||
<exclude name="BeanMembersShouldSerialize"/>
|
||||
</rule>
|
||||
|
||||
</ruleset>
|
||||
</ruleset>
|
||||
|
||||
@@ -10,15 +10,13 @@
|
||||
|
||||
|
||||
<rule ref="category/java/errorprone.xml">
|
||||
<exclude name="DataflowAnomalyAnalysis"/>
|
||||
<exclude name="MissingSerialVersionUID"/>
|
||||
<exclude name="NullAssignment"/>
|
||||
<exclude name="AvoidLiteralsInIfCondition"/>
|
||||
<exclude name="AvoidDuplicateLiterals"/>
|
||||
<exclude name="AvoidFieldNameMatchingMethodName"/>
|
||||
<exclude name="AvoidFieldNameMatchingTypeName"/>
|
||||
<exclude name="NullAssignment"/>
|
||||
<exclude name="AssignmentInOperand"/>
|
||||
<exclude name="TestClassWithoutTestCases"/>
|
||||
<exclude name="BeanMembersShouldSerialize"/>
|
||||
</rule>
|
||||
|
||||
</ruleset>
|
||||
</ruleset>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
plugins {
|
||||
id("com.iqser.red.service.java-conventions")
|
||||
id("io.freefair.lombok") version "8.3"
|
||||
id("io.freefair.lombok") version "8.4"
|
||||
}
|
||||
|
||||
description = "redaction-service-api-v1"
|
||||
|
||||
dependencies {
|
||||
implementation("org.springframework:spring-web:6.0.12")
|
||||
implementation("com.iqser.red.service:persistence-service-internal-api-v1:2.181.0")
|
||||
implementation("com.iqser.red.service:persistence-service-internal-api-v1:2.351.0")
|
||||
}
|
||||
|
||||
publishing {
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package com.iqser.red.service.redaction.v1.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AnalyzeResponse {
|
||||
|
||||
private String fileId;
|
||||
private List<UnprocessedManualEntity> unprocessedManualEntities;
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package com.iqser.red.service.redaction.v1.model;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
public class DroolsSyntaxDeprecatedWarnings {
|
||||
|
||||
Integer line;
|
||||
Integer column;
|
||||
String message;
|
||||
}
|
||||
+2
@@ -19,6 +19,8 @@ public class DroolsSyntaxValidation {
|
||||
|
||||
@Builder.Default
|
||||
List<DroolsSyntaxErrorMessage> droolsSyntaxErrorMessages = new LinkedList<>();
|
||||
@Builder.Default
|
||||
List<DroolsSyntaxDeprecatedWarnings> droolsSyntaxDeprecatedWarnings = new LinkedList<>();
|
||||
|
||||
|
||||
public void addErrorMessage(int line, int column, String message) {
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.iqser.red.service.redaction.v1.model;
|
||||
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.ManualRedactions;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLog;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class MigrationRequest {
|
||||
|
||||
String dossierTemplateId;
|
||||
String dossierId;
|
||||
String fileId;
|
||||
ManualRedactions manualRedactions;
|
||||
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.iqser.red.service.redaction.v1.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class MigrationResponse {
|
||||
|
||||
String dossierId;
|
||||
String fileId;
|
||||
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.iqser.red.service.redaction.v1.model;
|
||||
|
||||
public class QueueNames {
|
||||
|
||||
public static final String REDACTION_QUEUE = "redactionQueue";
|
||||
public static final String REDACTION_PRIORITY_QUEUE = "redactionPriorityQueue";
|
||||
public static final String REDACTION_ANALYSIS_RESPONSE_QUEUE = "redactionAnalysisResponseQueue";
|
||||
public static final String REDACTION_DQL = "redactionDQL";
|
||||
|
||||
public static final String MIGRATION_QUEUE = "migrationQueue";
|
||||
public static final String MIGRATION_RESPONSE_QUEUE = "migrationResponseQueue";
|
||||
public static final String MIGRATION_DLQ = "migrationDLQ";
|
||||
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.iqser.red.service.redaction.v1.model;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.Position;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Builder
|
||||
@EqualsAndHashCode
|
||||
public class UnprocessedManualEntity {
|
||||
|
||||
private String annotationId;
|
||||
private String textBefore;
|
||||
private String textAfter;
|
||||
private String section;
|
||||
@Builder.Default
|
||||
private List<Position> positions = new ArrayList<>();
|
||||
}
|
||||
@@ -3,24 +3,27 @@ import org.springframework.boot.gradle.tasks.bundling.BootBuildImage
|
||||
plugins {
|
||||
application
|
||||
id("com.iqser.red.service.java-conventions")
|
||||
id("org.springframework.boot") version "3.1.4"
|
||||
id("org.springframework.boot") version "3.1.5"
|
||||
id("io.spring.dependency-management") version "1.1.3"
|
||||
id("org.sonarqube") version "4.3.1.3277"
|
||||
id("io.freefair.lombok") version "8.3"
|
||||
id("org.sonarqube") version "4.4.1.3373"
|
||||
id("io.freefair.lombok") version "8.4"
|
||||
}
|
||||
|
||||
description = "redaction-service-server-v1"
|
||||
|
||||
|
||||
val layoutParserVersion = "0.70.0"
|
||||
val layoutParserVersion = "0.86.0"
|
||||
val jacksonVersion = "2.15.2"
|
||||
val droolsVersion = "8.44.0.Final"
|
||||
val droolsVersion = "9.44.0.Final"
|
||||
val pdfBoxVersion = "3.0.0"
|
||||
val persistenceServiceVersion = "2.187.0"
|
||||
val persistenceServiceVersion = "2.351.0"
|
||||
val springBootStarterVersion = "3.1.5"
|
||||
|
||||
configurations {
|
||||
all {
|
||||
exclude(group = "org.springframework.boot", module = "spring-boot-starter-logging")
|
||||
exclude(group = "commons-logging", module = "commons-logging")
|
||||
exclude(group = "org.springframework.boot", module = "spring-boot-starter-log4j2")
|
||||
exclude(group = "com.iqser.red.commons", module = "logging-commons")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,8 +37,9 @@ dependencies {
|
||||
implementation("com.iqser.red.commons:metric-commons:2.3.0")
|
||||
|
||||
implementation("com.iqser.red.commons:dictionary-merge-commons:1.5.0")
|
||||
implementation("com.iqser.red.commons:storage-commons:2.44.0")
|
||||
implementation("com.knecon.fforesight:tenant-commons:0.13.0")
|
||||
implementation("com.iqser.red.commons:storage-commons:2.45.0")
|
||||
implementation("com.knecon.fforesight:tenant-commons:0.21.0")
|
||||
implementation("com.knecon.fforesight:tracing-commons:0.5.0")
|
||||
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-afterburner:${jacksonVersion}")
|
||||
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${jacksonVersion}")
|
||||
@@ -49,12 +53,20 @@ dependencies {
|
||||
implementation("org.locationtech.jts:jts-core:1.19.0")
|
||||
|
||||
implementation("org.springframework.cloud:spring-cloud-starter-openfeign:4.0.4")
|
||||
implementation("org.springframework.boot:spring-boot-starter-amqp:3.1.4")
|
||||
implementation("org.springframework.boot:spring-boot-starter-amqp:${springBootStarterVersion}")
|
||||
implementation("org.springframework.boot:spring-boot-starter-cache:${springBootStarterVersion}")
|
||||
implementation("org.springframework.boot:spring-boot-starter-data-redis:${springBootStarterVersion}")
|
||||
|
||||
implementation("net.logstash.logback:logstash-logback-encoder:7.4")
|
||||
implementation("ch.qos.logback:logback-classic")
|
||||
|
||||
implementation("org.reflections:reflections:0.10.2")
|
||||
|
||||
testImplementation(project(":rules-management"))
|
||||
testImplementation("org.apache.pdfbox:pdfbox:${pdfBoxVersion}")
|
||||
testImplementation("org.apache.pdfbox:pdfbox-tools:${pdfBoxVersion}")
|
||||
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-test:3.1.4")
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-test:${springBootStarterVersion}")
|
||||
testImplementation("com.knecon.fforesight:layoutparser-service-processor:${layoutParserVersion}") {
|
||||
exclude(
|
||||
group = "com.iqser.red.service",
|
||||
@@ -67,15 +79,19 @@ tasks.test {
|
||||
configure<JacocoTaskExtension> {
|
||||
excludes = listOf("org/drools/**/*")
|
||||
}
|
||||
minHeapSize = "512m"
|
||||
maxHeapSize = "1024m"
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
tasks.named<BootBuildImage>("bootBuildImage") {
|
||||
|
||||
environment.put("BPE_DELIM_JAVA_TOOL_OPTIONS", " ")
|
||||
environment.put("BPE_APPEND_JAVA_TOOL_OPTIONS", "-Dfile.encoding=UTF-8")
|
||||
environment.put("BPE_APPEND_JAVA_TOOL_OPTIONS", "-XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8")
|
||||
|
||||
imageName.set("nexus.knecon.com:5001/red/${project.name}:${project.version}")
|
||||
imageName.set("nexus.knecon.com:5001/red/${project.name}")// must build image with same name always, otherwise the builder will not know which image to use as cache. DO NOT CHANGE!
|
||||
if (project.hasProperty("buildbootDockerHostNetwork")) {
|
||||
network.set("host")
|
||||
}
|
||||
@@ -91,5 +107,8 @@ tasks.named<BootBuildImage>("bootBuildImage") {
|
||||
email.set(providers.gradleProperty("mavenEmail").getOrNull())
|
||||
url.set("https://nexus.knecon.com:5001/")
|
||||
}
|
||||
|
||||
val dockerTag = "nexus.knecon.com:5001/red/${project.name}:${project.version}"
|
||||
tags.set(listOf(dockerTag))
|
||||
}
|
||||
}
|
||||
|
||||
+16
@@ -6,6 +6,7 @@ import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
@@ -17,7 +18,10 @@ import com.knecon.fforesight.tenantcommons.MultiTenancyAutoConfiguration;
|
||||
|
||||
import io.micrometer.core.aop.TimedAspect;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
import io.micrometer.observation.aop.ObservedAspect;
|
||||
|
||||
@EnableCaching
|
||||
@ImportAutoConfiguration({MultiTenancyAutoConfiguration.class})
|
||||
@Import({MetricsConfiguration.class, StorageAutoConfiguration.class})
|
||||
@EnableFeignClients(basePackageClasses = RulesClient.class)
|
||||
@@ -31,6 +35,10 @@ public class Application {
|
||||
SpringApplication.run(Application.class, args);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ObservedAspect observedAspect(ObservationRegistry observationRegistry) {
|
||||
return new ObservedAspect(observationRegistry);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TimedAspect timedAspect(MeterRegistry registry) {
|
||||
@@ -38,10 +46,18 @@ public class Application {
|
||||
return new TimedAspect(registry);
|
||||
}
|
||||
|
||||
|
||||
@Bean
|
||||
public DictionaryMergeService dictionaryMergeService() {
|
||||
|
||||
return new DictionaryMergeService();
|
||||
}
|
||||
|
||||
|
||||
@Bean(initMethod = "findDeprecatedMethods")
|
||||
public DeprecatedElementsFinder deprecatedElementsFinder() {
|
||||
|
||||
return new DeprecatedElementsFinder();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
package com.iqser.red.service.redaction.v1.server;
|
||||
|
||||
import static java.util.stream.Collectors.groupingBy;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.reflections.Reflections;
|
||||
import org.reflections.scanners.Scanners;
|
||||
import org.reflections.util.ConfigurationBuilder;
|
||||
|
||||
import com.iqser.red.service.redaction.v1.server.model.dictionary.SearchImplementation;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class DeprecatedElementsFinder {
|
||||
|
||||
public static final String PACKAGE_NAME = "com.iqser.red.service.redaction.v1.server";
|
||||
private Set<Method> deprecatedMethods;
|
||||
@Getter
|
||||
private Map<String, String> deprecatedMethodsSignaturesMap;
|
||||
private Set<Class<?>> deprecatedClasses;
|
||||
@Getter
|
||||
private SearchImplementation classesSearchImplementation;
|
||||
@Getter
|
||||
private SearchImplementation methodsSearchImplementation;
|
||||
|
||||
|
||||
public void findDeprecatedMethods() {
|
||||
|
||||
deprecatedMethods = new HashSet<>();
|
||||
deprecatedClasses = new HashSet<>();
|
||||
deprecatedMethodsSignaturesMap = new HashMap<>();
|
||||
|
||||
Reflections reflections = new Reflections(new ConfigurationBuilder().forPackage(PACKAGE_NAME)
|
||||
.setExpandSuperTypes(true)
|
||||
.setScanners(Scanners.MethodsAnnotated, Scanners.TypesAnnotated, Scanners.SubTypes));
|
||||
|
||||
deprecatedMethods = reflections.get(Scanners.MethodsAnnotated.with(Deprecated.class).as(Method.class));
|
||||
|
||||
deprecatedClasses = reflections.get(Scanners.TypesAnnotated.with(Deprecated.class).asClass());
|
||||
|
||||
if (!deprecatedClasses.isEmpty()) {
|
||||
deprecatedClasses.forEach(deprecatedType -> deprecatedMethods.addAll(Arrays.stream(deprecatedType.getDeclaredMethods())
|
||||
.collect(Collectors.toSet())));
|
||||
classesSearchImplementation = new SearchImplementation(deprecatedClasses.stream()
|
||||
.map(Class::getName)
|
||||
.collect(Collectors.toSet()), false);
|
||||
}
|
||||
|
||||
if (!deprecatedMethods.isEmpty()) {
|
||||
methodsSearchImplementation = new SearchImplementation(deprecatedMethods.stream()
|
||||
.map(Method::getName)
|
||||
.collect(Collectors.toSet()), false);
|
||||
deprecatedMethodsSignaturesMap = new HashMap<>();
|
||||
Map<String, List<Method>> deprecatedMethodsNameMap = deprecatedMethods.stream()
|
||||
.collect(groupingBy(Method::getName));
|
||||
for (String deprecatedMethod : deprecatedMethodsNameMap.keySet()) {
|
||||
deprecatedMethodsSignaturesMap.computeIfAbsent(deprecatedMethod,
|
||||
key -> deprecatedMethodsNameMap.get(deprecatedMethod)
|
||||
.stream()
|
||||
.map(this::getMethodSignature)
|
||||
.collect(Collectors.joining("\n")));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
log.info("For package: {} found deprecated classes {} deprecated methods {}", PACKAGE_NAME, deprecatedClasses.size(), deprecatedMethods.size());
|
||||
}
|
||||
|
||||
|
||||
public Set<Method> getDeprecatedMethods() {
|
||||
|
||||
if (this.deprecatedMethods == null) {
|
||||
findDeprecatedMethods();
|
||||
}
|
||||
return this.deprecatedMethods;
|
||||
}
|
||||
|
||||
|
||||
public Set<Class<?>> getDeprecatedClasses() {
|
||||
|
||||
if (this.deprecatedClasses == null) {
|
||||
findDeprecatedMethods();
|
||||
}
|
||||
return this.deprecatedClasses;
|
||||
}
|
||||
|
||||
private String getMethodSignature(Method method) {
|
||||
|
||||
String methodName = method.getName();
|
||||
String parameterTypes = Arrays.stream(method.getGenericParameterTypes())
|
||||
.map(Type::getTypeName)
|
||||
.collect(Collectors.joining(", "));
|
||||
|
||||
return method.getGenericReturnType() + " " + methodName + "(" + parameterTypes + ")";
|
||||
}
|
||||
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package com.iqser.red.service.redaction.v1.server;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.springframework.boot.autoconfigure.cache.RedisCacheManagerBuilderCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.cache.RedisCacheConfiguration;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
|
||||
@Configuration
|
||||
public class RedisCachingConfiguration {
|
||||
|
||||
@Bean
|
||||
public RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer() {
|
||||
|
||||
return (builder) -> builder.withCacheConfiguration("documentDataCache",
|
||||
RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(30)).disableCachingNullValues());
|
||||
}
|
||||
|
||||
|
||||
@Bean
|
||||
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
|
||||
|
||||
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
return template;
|
||||
}
|
||||
|
||||
}
|
||||
+424
@@ -0,0 +1,424 @@
|
||||
package com.iqser.red.service.redaction.v1.server.migration;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.AnnotationStatus;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.ManualRedactions;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.IdRemoval;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualForceRedaction;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualLegalBasisChange;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualRecategorization;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualRedactionEntry;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualResizeRedaction;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Change;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.ChangeType;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Engine;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.ManualChange;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.ManualRedactionType;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Point;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Rectangle;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLog;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLogEntry;
|
||||
import com.iqser.red.service.redaction.v1.server.service.DictionaryService;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class LegacyRedactionLogMergeService {
|
||||
|
||||
private final DictionaryService dictionaryService;
|
||||
|
||||
|
||||
public RedactionLog addManualAddEntriesAndRemoveSkippedImported(RedactionLog redactionLog, ManualRedactions manualRedactions, String dossierTemplateId) {
|
||||
|
||||
Set<String> skippedImportedRedactions = new HashSet<>();
|
||||
log.info("Merging Redaction log with manual redactions");
|
||||
if (manualRedactions != null) {
|
||||
|
||||
var manualRedactionLogEntries = addManualAddEntries(manualRedactions.getEntriesToAdd(), redactionLog.getAnalysisNumber());
|
||||
|
||||
redactionLog.getRedactionLogEntry().addAll(manualRedactionLogEntries);
|
||||
|
||||
var manualRedactionWrappers = createManualRedactionWrappers(manualRedactions);
|
||||
|
||||
for (RedactionLogEntry entry : redactionLog.getRedactionLogEntry()) {
|
||||
|
||||
if (entry.isImported()) {
|
||||
processRedactionLogEntry(manualRedactionWrappers.stream()
|
||||
.filter(ManualRedactionWrapper::isApproved)
|
||||
.filter(mr -> entry.getId().equals(mr.getId()))
|
||||
.collect(Collectors.toList()), entry, dossierTemplateId);
|
||||
if (!entry.isRedacted()) {
|
||||
skippedImportedRedactions.add(entry.getId());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Set<String> processedIds = new HashSet<>();
|
||||
redactionLog.getRedactionLogEntry().removeIf(entry -> {
|
||||
if (entry.isFalsePositive()) {
|
||||
return true;
|
||||
}
|
||||
if (entry.getImportedRedactionIntersections() != null) {
|
||||
entry.getImportedRedactionIntersections().removeAll(skippedImportedRedactions);
|
||||
if (!entry.getImportedRedactionIntersections().isEmpty() && (!entry.isImage() || entry.isImage() && !(entry.getType().equals("image") || entry.getType()
|
||||
.equals("ocr")))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (processedIds.contains(entry.getId())) {
|
||||
log.info("Duplicate annotation found with id {}", entry.getId());
|
||||
return true;
|
||||
}
|
||||
processedIds.add(entry.getId());
|
||||
return false;
|
||||
});
|
||||
return redactionLog;
|
||||
}
|
||||
|
||||
|
||||
private List<ManualRedactionWrapper> createManualRedactionWrappers(ManualRedactions manualRedactions) {
|
||||
|
||||
List<ManualRedactionWrapper> manualRedactionWrappers = new ArrayList<>();
|
||||
|
||||
manualRedactions.getRecategorizations()
|
||||
.forEach(item -> {
|
||||
if (item.getSoftDeletedTime() == null) {
|
||||
manualRedactionWrappers.add(new ManualRedactionWrapper(item.getAnnotationId(), item.getRequestDate(), item, item.isApproved()));
|
||||
}
|
||||
});
|
||||
|
||||
manualRedactions.getIdsToRemove()
|
||||
.forEach(item -> {
|
||||
if (item.getSoftDeletedTime() == null) {
|
||||
manualRedactionWrappers.add(new ManualRedactionWrapper(item.getAnnotationId(), item.getRequestDate(), item, item.isApproved()));
|
||||
}
|
||||
});
|
||||
|
||||
manualRedactions.getForceRedactions()
|
||||
.forEach(item -> {
|
||||
if (item.getSoftDeletedTime() == null) {
|
||||
manualRedactionWrappers.add(new ManualRedactionWrapper(item.getAnnotationId(), item.getRequestDate(), item, item.isApproved()));
|
||||
}
|
||||
});
|
||||
|
||||
manualRedactions.getLegalBasisChanges()
|
||||
.forEach(item -> {
|
||||
if (item.getSoftDeletedTime() == null) {
|
||||
manualRedactionWrappers.add(new ManualRedactionWrapper(item.getAnnotationId(), item.getRequestDate(), item, item.isApproved()));
|
||||
}
|
||||
});
|
||||
|
||||
manualRedactions.getResizeRedactions()
|
||||
.forEach(item -> {
|
||||
if (item.getSoftDeletedTime() == null) {
|
||||
manualRedactionWrappers.add(new ManualRedactionWrapper(item.getAnnotationId(), item.getRequestDate(), item, item.isApproved()));
|
||||
}
|
||||
});
|
||||
|
||||
Collections.sort(manualRedactionWrappers);
|
||||
|
||||
return manualRedactionWrappers;
|
||||
}
|
||||
|
||||
|
||||
private void processRedactionLogEntry(List<ManualRedactionWrapper> manualRedactionWrappers, RedactionLogEntry redactionLogEntry, String dossierTemplateId) {
|
||||
|
||||
manualRedactionWrappers.forEach(mrw -> {
|
||||
|
||||
Object item = mrw.getItem();
|
||||
if (item instanceof ManualRecategorization imageRecategorization) {
|
||||
processManualImageRecategorization(redactionLogEntry, dossierTemplateId, imageRecategorization);
|
||||
}
|
||||
|
||||
if (item instanceof IdRemoval manualRemoval) {
|
||||
processIdRemoval(redactionLogEntry, manualRemoval);
|
||||
}
|
||||
|
||||
if (item instanceof ManualForceRedaction manualForceRedact) {
|
||||
processManualForceRedaction(redactionLogEntry, dossierTemplateId, manualForceRedact);
|
||||
}
|
||||
|
||||
if (item instanceof ManualLegalBasisChange manualLegalBasisChange) {
|
||||
processManualLegalBasisChange(redactionLogEntry, manualLegalBasisChange);
|
||||
}
|
||||
|
||||
if (item instanceof ManualResizeRedaction manualResizeRedact) {
|
||||
processManualResizeRedaction(redactionLogEntry, manualResizeRedact);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
private void processManualImageRecategorization(RedactionLogEntry redactionLogEntry, String dossierTemplateId, ManualRecategorization imageRecategorization) {
|
||||
|
||||
String manualOverrideReason = null;
|
||||
if (imageRecategorization.getStatus().equals(AnnotationStatus.APPROVED)) {
|
||||
|
||||
redactionLogEntry.setType(imageRecategorization.getType());
|
||||
redactionLogEntry.setSection("Image:" + redactionLogEntry.getType());
|
||||
|
||||
if (dictionaryService.isHint(imageRecategorization.getType(), dossierTemplateId)) {
|
||||
redactionLogEntry.setRedacted(false);
|
||||
redactionLogEntry.setHint(true);
|
||||
} else {
|
||||
redactionLogEntry.setHint(false);
|
||||
}
|
||||
|
||||
manualOverrideReason = mergeReasonIfNecessary(redactionLogEntry.getReason(), ", recategorized by manual override");
|
||||
} else if (imageRecategorization.getStatus().equals(AnnotationStatus.REQUESTED)) {
|
||||
manualOverrideReason = mergeReasonIfNecessary(redactionLogEntry.getReason(), ", requested to recategorize");
|
||||
}
|
||||
|
||||
if (manualOverrideReason != null) {
|
||||
redactionLogEntry.setReason(manualOverrideReason);
|
||||
}
|
||||
|
||||
redactionLogEntry.getManualChanges()
|
||||
.add(ManualChange.from(imageRecategorization).withManualRedactionType(ManualRedactionType.RECATEGORIZE).withChange("type", imageRecategorization.getType()));
|
||||
}
|
||||
|
||||
|
||||
private String mergeReasonIfNecessary(String currentReason, String addition) {
|
||||
|
||||
if (currentReason != null) {
|
||||
if (!currentReason.contains(addition)) {
|
||||
return currentReason + addition;
|
||||
}
|
||||
return currentReason;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void processIdRemoval(RedactionLogEntry redactionLogEntry, IdRemoval manualRemoval) {
|
||||
|
||||
boolean isApprovedRedaction = manualRemoval.getStatus().equals(AnnotationStatus.APPROVED);
|
||||
if (isApprovedRedaction && manualRemoval.isRemoveFromDictionary() && isBasedOnDictionaryOnly(redactionLogEntry)) {
|
||||
log.debug("Skipping merge for dictionary-modifying entry");
|
||||
} else {
|
||||
String manualOverrideReason = null;
|
||||
if (isApprovedRedaction) {
|
||||
redactionLogEntry.setRedacted(false);
|
||||
manualOverrideReason = mergeReasonIfNecessary(redactionLogEntry.getReason(), ", removed by manual override");
|
||||
redactionLogEntry.setHint(false);
|
||||
} else if (manualRemoval.getStatus().equals(AnnotationStatus.REQUESTED)) {
|
||||
manualOverrideReason = mergeReasonIfNecessary(redactionLogEntry.getReason(), ", requested to remove");
|
||||
}
|
||||
|
||||
if (manualOverrideReason != null) {
|
||||
redactionLogEntry.setReason(manualOverrideReason);
|
||||
}
|
||||
}
|
||||
|
||||
redactionLogEntry.getManualChanges()
|
||||
.add(ManualChange.from(manualRemoval)
|
||||
.withManualRedactionType(manualRemoval.isRemoveFromDictionary() ? ManualRedactionType.REMOVE_FROM_DICTIONARY : ManualRedactionType.REMOVE_LOCALLY));
|
||||
}
|
||||
|
||||
|
||||
private boolean isBasedOnDictionaryOnly(RedactionLogEntry redactionLogEntry) {
|
||||
|
||||
return redactionLogEntry.getEngines().contains(Engine.DICTIONARY) && redactionLogEntry.getEngines().size() == 1;
|
||||
}
|
||||
|
||||
|
||||
private void processManualForceRedaction(RedactionLogEntry redactionLogEntry, String dossierTemplateId, ManualForceRedaction manualForceRedact) {
|
||||
|
||||
String manualOverrideReason = null;
|
||||
var dictionaryIsHint = dictionaryService.isHint(redactionLogEntry.getType(), dossierTemplateId);
|
||||
if (manualForceRedact.getStatus().equals(AnnotationStatus.APPROVED)) {
|
||||
// Forcing a skipped hint should result in a hint
|
||||
if (dictionaryIsHint) {
|
||||
redactionLogEntry.setHint(true);
|
||||
} else {
|
||||
redactionLogEntry.setRedacted(true);
|
||||
}
|
||||
manualOverrideReason = mergeReasonIfNecessary(redactionLogEntry.getReason(), ", forced by manual override");
|
||||
redactionLogEntry.setLegalBasis(manualForceRedact.getLegalBasis());
|
||||
} else if (manualForceRedact.getStatus().equals(AnnotationStatus.REQUESTED)) {
|
||||
manualOverrideReason = mergeReasonIfNecessary(redactionLogEntry.getReason(), ", requested to force " + (dictionaryIsHint ? "hint" : "redact"));
|
||||
redactionLogEntry.setLegalBasis(manualForceRedact.getLegalBasis());
|
||||
}
|
||||
|
||||
if (manualOverrideReason != null) {
|
||||
redactionLogEntry.setReason(manualOverrideReason);
|
||||
}
|
||||
|
||||
var manualChange = ManualChange.from(manualForceRedact).withManualRedactionType(dictionaryIsHint ? ManualRedactionType.FORCE_HINT : ManualRedactionType.FORCE_REDACT);
|
||||
|
||||
redactionLogEntry.getManualChanges().add(manualChange);
|
||||
}
|
||||
|
||||
|
||||
private void processManualLegalBasisChange(RedactionLogEntry redactionLogEntry, ManualLegalBasisChange manualLegalBasisChange) {
|
||||
|
||||
String manualOverrideReason = null;
|
||||
if (manualLegalBasisChange.getStatus().equals(AnnotationStatus.APPROVED)) {
|
||||
manualOverrideReason = mergeReasonIfNecessary(redactionLogEntry.getReason(), ", legal basis was manually changed");
|
||||
redactionLogEntry.setLegalBasis(manualLegalBasisChange.getLegalBasis());
|
||||
redactionLogEntry.setRedacted(true);
|
||||
if (manualLegalBasisChange.getSection() != null) {
|
||||
redactionLogEntry.setSection(manualLegalBasisChange.getSection());
|
||||
}
|
||||
if (redactionLogEntry.isRectangle() && manualLegalBasisChange.getValue() != null) {
|
||||
redactionLogEntry.setValue(manualLegalBasisChange.getValue());
|
||||
}
|
||||
} else if (manualLegalBasisChange.getStatus().equals(AnnotationStatus.REQUESTED)) {
|
||||
manualOverrideReason = mergeReasonIfNecessary(redactionLogEntry.getReason(), ", legal basis change requested");
|
||||
}
|
||||
|
||||
if (manualOverrideReason != null) {
|
||||
redactionLogEntry.setReason(manualOverrideReason);
|
||||
}
|
||||
|
||||
var manualChange = ManualChange.from(manualLegalBasisChange).withManualRedactionType(ManualRedactionType.LEGAL_BASIS_CHANGE);
|
||||
manualChange.withChange("legalBasis", manualLegalBasisChange.getLegalBasis());
|
||||
if (manualLegalBasisChange.getSection() != null) {
|
||||
manualChange.withChange("section", manualLegalBasisChange.getSection());
|
||||
}
|
||||
if (redactionLogEntry.isRectangle() && manualLegalBasisChange.getValue() != null) {
|
||||
manualChange.withChange("value", manualLegalBasisChange.getValue());
|
||||
}
|
||||
redactionLogEntry.getManualChanges().add(manualChange);
|
||||
}
|
||||
|
||||
|
||||
private void processManualResizeRedaction(RedactionLogEntry redactionLogEntry, ManualResizeRedaction manualResizeRedact) {
|
||||
|
||||
String manualOverrideReason = null;
|
||||
if (manualResizeRedact.getStatus().equals(AnnotationStatus.APPROVED)) {
|
||||
redactionLogEntry.setPositions(convertPositions(manualResizeRedact.getPositions()));
|
||||
if (!"signature".equalsIgnoreCase(redactionLogEntry.getType()) && !"logo".equalsIgnoreCase(redactionLogEntry.getType())) {
|
||||
redactionLogEntry.setValue(manualResizeRedact.getValue());
|
||||
}
|
||||
// This is for backwards compatibility, now the text after/before is calculated during reanalysis because we need to find dict entries on positions where entries are resized to smaller.
|
||||
if (manualResizeRedact.getTextBefore() != null || manualResizeRedact.getTextAfter() != null) {
|
||||
redactionLogEntry.setTextBefore(manualResizeRedact.getTextBefore());
|
||||
redactionLogEntry.setTextAfter(manualResizeRedact.getTextAfter());
|
||||
}
|
||||
manualOverrideReason = mergeReasonIfNecessary(redactionLogEntry.getReason(), ", resized by manual override");
|
||||
} else if (manualResizeRedact.getStatus().equals(AnnotationStatus.REQUESTED)) {
|
||||
manualOverrideReason = mergeReasonIfNecessary(redactionLogEntry.getReason(), ", requested to resize redact");
|
||||
redactionLogEntry.setPositions(convertPositions(manualResizeRedact.getPositions()));
|
||||
|
||||
// This is for backwards compatibility, now the text after/before is calculated during reanalysis because we need to find dict entries on positions where entries are resized to smaller.
|
||||
if (manualResizeRedact.getTextBefore() != null || manualResizeRedact.getTextAfter() != null) {
|
||||
redactionLogEntry.setTextBefore(manualResizeRedact.getTextBefore());
|
||||
redactionLogEntry.setTextAfter(manualResizeRedact.getTextAfter());
|
||||
}
|
||||
}
|
||||
|
||||
redactionLogEntry.setReason(manualOverrideReason);
|
||||
redactionLogEntry.getManualChanges()
|
||||
.add(ManualChange.from(manualResizeRedact).withManualRedactionType(ManualRedactionType.RESIZE).withChange("value", manualResizeRedact.getValue()));
|
||||
}
|
||||
|
||||
|
||||
public List<RedactionLogEntry> addManualAddEntries(Set<ManualRedactionEntry> manualAdds, int analysisNumber) {
|
||||
|
||||
List<RedactionLogEntry> redactionLogEntries = new ArrayList<>();
|
||||
|
||||
for (ManualRedactionEntry manualRedactionEntry : manualAdds) {
|
||||
|
||||
if (shouldCreateManualEntry(manualRedactionEntry)) {
|
||||
RedactionLogEntry redactionLogEntry = createRedactionLogEntry(manualRedactionEntry, manualRedactionEntry.getAnnotationId(), analysisNumber);
|
||||
redactionLogEntry.setPositions(convertPositions(manualRedactionEntry.getPositions()));
|
||||
redactionLogEntry.setTextBefore(manualRedactionEntry.getTextBefore());
|
||||
redactionLogEntry.setTextAfter(manualRedactionEntry.getTextAfter());
|
||||
|
||||
redactionLogEntries.add(redactionLogEntry);
|
||||
}
|
||||
}
|
||||
|
||||
return redactionLogEntries;
|
||||
}
|
||||
|
||||
|
||||
private List<Rectangle> convertPositions(List<com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.Rectangle> positions) {
|
||||
|
||||
return positions.stream()
|
||||
.map(pos -> new Rectangle(new Point(pos.getTopLeftX(), pos.getTopLeftY()), pos.getWidth(), pos.getHeight(), pos.getPage()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("PMD.UselessParentheses")
|
||||
private boolean shouldCreateManualEntry(ManualRedactionEntry manualRedactionEntry) {
|
||||
|
||||
if (!manualRedactionEntry.isApproved()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (!manualRedactionEntry.isAddToDictionary() && !manualRedactionEntry.isAddToDossierDictionary()) || ((manualRedactionEntry.isAddToDictionary()
|
||||
|| manualRedactionEntry.isAddToDossierDictionary())
|
||||
&& manualRedactionEntry.getProcessedDate() == null);
|
||||
}
|
||||
|
||||
|
||||
private RedactionLogEntry createRedactionLogEntry(ManualRedactionEntry manualRedactionEntry, String id, int analysisNumber) {
|
||||
|
||||
var addToDictionary = manualRedactionEntry.isAddToDictionary() || manualRedactionEntry.isAddToDossierDictionary();
|
||||
|
||||
var change = ManualChange.from(manualRedactionEntry).withManualRedactionType(addToDictionary ? ManualRedactionType.ADD_TO_DICTIONARY : ManualRedactionType.ADD_LOCALLY);
|
||||
List<ManualChange> changeList = new ArrayList<>();
|
||||
changeList.add(change);
|
||||
|
||||
return RedactionLogEntry.builder()
|
||||
.id(id)
|
||||
.reason(manualRedactionEntry.getReason())
|
||||
.isDictionaryEntry(manualRedactionEntry.isAddToDictionary())
|
||||
.isDossierDictionaryEntry(manualRedactionEntry.isAddToDossierDictionary())
|
||||
.legalBasis(manualRedactionEntry.getLegalBasis())
|
||||
.value(manualRedactionEntry.getValue())
|
||||
.sourceId(manualRedactionEntry.getSourceId())
|
||||
.section(manualRedactionEntry.getSection())
|
||||
.type(manualRedactionEntry.getType())
|
||||
.redacted(true)
|
||||
.isHint(false)
|
||||
.sectionNumber(-1)
|
||||
.rectangle(manualRedactionEntry.isRectangle())
|
||||
.manualChanges(changeList)
|
||||
.changes(List.of(new Change(analysisNumber + 1, ChangeType.ADDED, manualRedactionEntry.getRequestDate())))
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
private static class ManualRedactionWrapper implements Comparable<ManualRedactionWrapper> {
|
||||
|
||||
private String id;
|
||||
private OffsetDateTime date;
|
||||
private Object item;
|
||||
private boolean approved;
|
||||
|
||||
|
||||
@Override
|
||||
public int compareTo(ManualRedactionWrapper o) {
|
||||
|
||||
return this.date.compareTo(o.date);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package com.iqser.red.service.redaction.v1.server.migration;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLog;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLogEntry;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
@Service
|
||||
@SuppressWarnings("PMD")
|
||||
public class LegacyVersion0MigrationService {
|
||||
|
||||
public RedactionLog mergeDuplicateAnnotationIds(RedactionLog redactionLog) {
|
||||
|
||||
List<RedactionLogEntry> mergedEntries = new LinkedList<>();
|
||||
Map<String, List<RedactionLogEntry>> entriesById = redactionLog.getRedactionLogEntry().stream().collect(Collectors.groupingBy(RedactionLogEntry::getId));
|
||||
for (List<RedactionLogEntry> entries : entriesById.values()) {
|
||||
|
||||
if (entries.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entries.size() == 1) {
|
||||
mergedEntries.add(entries.get(0));
|
||||
continue;
|
||||
}
|
||||
|
||||
List<RedactionLogEntry> sortedEntries = entries.stream().sorted(Comparator.comparing(entry -> entry.getChanges().get(0).getDateTime())).toList();
|
||||
|
||||
RedactionLogEntry initialEntry = sortedEntries.get(0);
|
||||
for (RedactionLogEntry entry : sortedEntries.subList(1, sortedEntries.size())) {
|
||||
copyNonNullFields(entry, initialEntry);
|
||||
}
|
||||
mergedEntries.add(initialEntry);
|
||||
}
|
||||
|
||||
redactionLog.setRedactionLogEntry(mergedEntries);
|
||||
return redactionLog;
|
||||
}
|
||||
|
||||
|
||||
@SneakyThrows
|
||||
public static void copyNonNullFields(RedactionLogEntry source, RedactionLogEntry destination) {
|
||||
|
||||
if (source == null || destination == null) {
|
||||
throw new IllegalArgumentException("Source and destination objects must not be null");
|
||||
}
|
||||
|
||||
Class<?> sourceClass = source.getClass();
|
||||
|
||||
Field[] sourceFields = sourceClass.getDeclaredFields();
|
||||
|
||||
for (Field field : sourceFields) {
|
||||
field.setAccessible(true);
|
||||
Object value = field.get(source);
|
||||
if (value != null) {
|
||||
field.set(destination, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
package com.iqser.red.service.redaction.v1.server.migration;
|
||||
|
||||
import static com.iqser.red.service.redaction.v1.model.QueueNames.MIGRATION_QUEUE;
|
||||
import static com.iqser.red.service.redaction.v1.model.QueueNames.MIGRATION_RESPONSE_QUEUE;
|
||||
|
||||
import org.springframework.amqp.core.Message;
|
||||
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
|
||||
import org.springframework.amqp.rabbit.annotation.RabbitListener;
|
||||
import org.springframework.amqp.rabbit.core.RabbitTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.file.FileType;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLog;
|
||||
import com.iqser.red.service.redaction.v1.model.MigrationRequest;
|
||||
import com.iqser.red.service.redaction.v1.model.MigrationResponse;
|
||||
import com.iqser.red.service.redaction.v1.server.model.MigratedEntityLog;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.Document;
|
||||
import com.iqser.red.service.redaction.v1.server.service.DictionaryService;
|
||||
import com.iqser.red.service.redaction.v1.server.service.document.DocumentGraphMapper;
|
||||
import com.iqser.red.service.redaction.v1.server.storage.RedactionStorageService;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
|
||||
public class MigrationMessageReceiver {
|
||||
|
||||
ObjectMapper objectMapper;
|
||||
RedactionLogToEntityLogMigrationService redactionLogToEntityLogMigrationService;
|
||||
RedactionStorageService redactionStorageService;
|
||||
LegacyRedactionLogMergeService legacyRedactionLogMergeService;
|
||||
LegacyVersion0MigrationService legacyVersion0MigrationService;
|
||||
RabbitTemplate rabbitTemplate;
|
||||
DictionaryService dictionaryService;
|
||||
|
||||
|
||||
@SneakyThrows
|
||||
@RabbitHandler
|
||||
@RabbitListener(queues = MIGRATION_QUEUE)
|
||||
public void receiveMigrationRequest(Message message) {
|
||||
|
||||
MigrationRequest migrationRequest = objectMapper.readValue(message.getBody(), MigrationRequest.class);
|
||||
log.info("--------------------------------------------------------------------");
|
||||
log.info("Starting redactionLog to entityLog migration for dossierId {} and fileId {}", migrationRequest.getDossierId(), migrationRequest.getFileId());
|
||||
|
||||
dictionaryService.updateDictionary(migrationRequest.getDossierTemplateId(), migrationRequest.getDossierId());
|
||||
|
||||
Document document = DocumentGraphMapper.toDocumentGraph(redactionStorageService.getDocumentData(migrationRequest.getDossierId(), migrationRequest.getFileId()));
|
||||
RedactionLog redactionLog = redactionStorageService.getRedactionLog(migrationRequest.getDossierId(), migrationRequest.getFileId());
|
||||
|
||||
if (redactionLog.getAnalysisVersion() == 0) {
|
||||
redactionLog = legacyVersion0MigrationService.mergeDuplicateAnnotationIds(redactionLog);
|
||||
} else if (migrationRequest.getManualRedactions() != null) {
|
||||
redactionLog = legacyRedactionLogMergeService.addManualAddEntriesAndRemoveSkippedImported(redactionLog, migrationRequest.getManualRedactions(), migrationRequest.getDossierTemplateId());
|
||||
}
|
||||
|
||||
MigratedEntityLog migratedEntityLog = redactionLogToEntityLogMigrationService.migrate(redactionLog, document, migrationRequest.getDossierTemplateId(), migrationRequest.getManualRedactions());
|
||||
|
||||
redactionStorageService.storeObject(migrationRequest.getDossierId(), migrationRequest.getFileId(), FileType.ENTITY_LOG, migratedEntityLog.getEntityLog());
|
||||
redactionStorageService.storeObject(migrationRequest.getDossierId(), migrationRequest.getFileId(), FileType.MIGRATED_IDS, migratedEntityLog.getMigratedIds());
|
||||
|
||||
sendFinished(MigrationResponse.builder().dossierId(migrationRequest.getDossierId()).fileId(migrationRequest.getFileId()).build());
|
||||
log.info("Migrated {} redactionLog entries for dossierId {} and fileId {}",
|
||||
migratedEntityLog.getEntityLog().getEntityLogEntry().size(),
|
||||
migrationRequest.getDossierId(),
|
||||
migrationRequest.getFileId());
|
||||
log.info("");
|
||||
}
|
||||
|
||||
|
||||
@SneakyThrows
|
||||
public void sendFinished(MigrationResponse migrationResponse) {
|
||||
|
||||
rabbitTemplate.convertAndSend(MIGRATION_RESPONSE_QUEUE, migrationResponse);
|
||||
}
|
||||
|
||||
}
|
||||
+341
@@ -0,0 +1,341 @@
|
||||
package com.iqser.red.service.redaction.v1.server.migration;
|
||||
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntityLog;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntityLogLegalBasis;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.migration.MigratedIds;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.ManualRedactions;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.BaseAnnotation;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualResizeRedaction;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.ManualRedactionType;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Rectangle;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLog;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLogEntry;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLogLegalBasis;
|
||||
import com.iqser.red.service.redaction.v1.model.MigrationRequest;
|
||||
import com.iqser.red.service.redaction.v1.server.model.PrecursorEntity;
|
||||
import com.iqser.red.service.redaction.v1.server.model.MigratedEntityLog;
|
||||
import com.iqser.red.service.redaction.v1.server.model.MigrationEntity;
|
||||
import com.iqser.red.service.redaction.v1.server.model.RectangleWithPage;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.TextRange;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.entity.EntityType;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.entity.TextEntity;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.Document;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.Image;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.ImageType;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.SemanticNode;
|
||||
import com.iqser.red.service.redaction.v1.server.service.DictionaryService;
|
||||
import com.iqser.red.service.redaction.v1.server.service.ManualChangesApplicationService;
|
||||
import com.iqser.red.service.redaction.v1.server.service.document.EntityCreationService;
|
||||
import com.iqser.red.service.redaction.v1.server.service.document.EntityEnrichmentService;
|
||||
import com.iqser.red.service.redaction.v1.server.service.document.EntityFindingUtility;
|
||||
import com.iqser.red.service.redaction.v1.server.utils.IdBuilder;
|
||||
import com.iqser.red.service.redaction.v1.server.utils.MigratedIdsCollector;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
|
||||
//TODO: remove this, once the migration is done
|
||||
public class RedactionLogToEntityLogMigrationService {
|
||||
|
||||
private static final double MATCH_THRESHOLD = 10;
|
||||
EntityFindingUtility entityFindingUtility;
|
||||
EntityEnrichmentService entityEnrichmentService;
|
||||
DictionaryService dictionaryService;
|
||||
ManualChangesApplicationService manualChangesApplicationService;
|
||||
|
||||
|
||||
public MigratedEntityLog migrate(RedactionLog redactionLog, Document document, String dossierTemplateId, ManualRedactions manualRedactions) {
|
||||
|
||||
List<MigrationEntity> entitiesToMigrate = calculateMigrationEntitiesFromRedactionLog(redactionLog, document, dossierTemplateId);
|
||||
MigratedIds migratedIds = entitiesToMigrate.stream()
|
||||
.collect(new MigratedIdsCollector());
|
||||
|
||||
applyManualChanges(entitiesToMigrate, manualRedactions);
|
||||
|
||||
EntityLog entityLog = new EntityLog();
|
||||
entityLog.setAnalysisNumber(redactionLog.getAnalysisNumber());
|
||||
entityLog.setRulesVersion(redactionLog.getRulesVersion());
|
||||
entityLog.setDictionaryVersion(redactionLog.getDictionaryVersion());
|
||||
entityLog.setDossierDictionaryVersion(redactionLog.getDossierDictionaryVersion());
|
||||
entityLog.setLegalBasisVersion(redactionLog.getLegalBasisVersion());
|
||||
entityLog.setAnalysisVersion(redactionLog.getAnalysisVersion());
|
||||
entityLog.setLegalBasis(redactionLog.getLegalBasis()
|
||||
.stream()
|
||||
.map(RedactionLogToEntityLogMigrationService::toEntityLogLegalBasis)
|
||||
.toList());
|
||||
|
||||
Map<String, String> oldToNewIDMapping = migratedIds.buildOldToNewMapping();
|
||||
entityLog.setEntityLogEntry(entitiesToMigrate.stream()
|
||||
.map(migrationEntity -> migrationEntity.toEntityLogEntry(oldToNewIDMapping))
|
||||
.toList());
|
||||
|
||||
if (getNumberOfApprovedEntries(redactionLog) != entityLog.getEntityLogEntry().size()) {
|
||||
String message = String.format("Not all entities have been found during the migration redactionLog has %d entries and new entityLog %d",
|
||||
redactionLog.getRedactionLogEntry().size(),
|
||||
entityLog.getEntityLogEntry().size());
|
||||
log.error(message);
|
||||
throw new AssertionError(message);
|
||||
}
|
||||
|
||||
MigratedIds idsToMigrateInDb = entitiesToMigrate.stream()
|
||||
.filter(MigrationEntity::hasManualChangesOrComments)
|
||||
.filter(m -> !m.getOldId().equals(m.getNewId()))
|
||||
.collect(new MigratedIdsCollector());
|
||||
|
||||
return new MigratedEntityLog(idsToMigrateInDb, entityLog);
|
||||
}
|
||||
|
||||
|
||||
private void applyManualChanges(List<MigrationEntity> entitiesToMigrate, ManualRedactions manualRedactions) {
|
||||
|
||||
if (manualRedactions == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, List<BaseAnnotation>> manualChangesPerAnnotationId = Stream.of(manualRedactions.getIdsToRemove(),
|
||||
manualRedactions.getEntriesToAdd(),
|
||||
manualRedactions.getForceRedactions(),
|
||||
manualRedactions.getResizeRedactions(),
|
||||
manualRedactions.getLegalBasisChanges(),
|
||||
manualRedactions.getRecategorizations(),
|
||||
manualRedactions.getLegalBasisChanges())
|
||||
.flatMap(Collection::stream)
|
||||
.collect(Collectors.groupingBy(BaseAnnotation::getAnnotationId));
|
||||
|
||||
entitiesToMigrate.forEach(migrationEntity -> manualChangesPerAnnotationId.getOrDefault(migrationEntity.getOldId(), Collections.emptyList())
|
||||
.forEach(manualChange -> {
|
||||
if (manualChange instanceof ManualResizeRedaction manualResizeRedaction && migrationEntity.getMigratedEntity() instanceof TextEntity textEntity) {
|
||||
ManualResizeRedaction migratedManualResizeRedaction = ManualResizeRedaction.builder()
|
||||
.positions(manualResizeRedaction.getPositions())
|
||||
.annotationId(migrationEntity.getNewId())
|
||||
.updateDictionary(manualResizeRedaction.getUpdateDictionary())
|
||||
.addToAllDossiers(manualResizeRedaction.isAddToAllDossiers())
|
||||
.textAfter(manualResizeRedaction.getTextAfter())
|
||||
.textBefore(manualResizeRedaction.getTextBefore())
|
||||
.build();
|
||||
manualChangesApplicationService.resize(textEntity, migratedManualResizeRedaction);
|
||||
} else {
|
||||
migrationEntity.getMigratedEntity().getManualOverwrite().addChange(manualChange);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
private static long getNumberOfApprovedEntries(RedactionLog redactionLog) {
|
||||
|
||||
return redactionLog.getRedactionLogEntry().size();
|
||||
}
|
||||
|
||||
|
||||
private List<MigrationEntity> calculateMigrationEntitiesFromRedactionLog(RedactionLog redactionLog, Document document, String dossierTemplateId) {
|
||||
|
||||
List<MigrationEntity> images = getImageBasedMigrationEntities(redactionLog, document, dossierTemplateId);
|
||||
List<MigrationEntity> textMigrationEntities = getTextBasedMigrationEntities(redactionLog, document, dossierTemplateId);
|
||||
return Stream.of(textMigrationEntities.stream(), images.stream())
|
||||
.flatMap(Function.identity())
|
||||
.toList();
|
||||
}
|
||||
|
||||
|
||||
private static EntityLogLegalBasis toEntityLogLegalBasis(RedactionLogLegalBasis redactionLogLegalBasis) {
|
||||
|
||||
return new EntityLogLegalBasis(redactionLogLegalBasis.getName(), redactionLogLegalBasis.getDescription(), redactionLogLegalBasis.getReason());
|
||||
}
|
||||
|
||||
|
||||
private List<MigrationEntity> getImageBasedMigrationEntities(RedactionLog redactionLog, Document document, String dossierTemplateId) {
|
||||
|
||||
List<Image> images = document.streamAllImages()
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<RedactionLogEntry> redactionLogImages = redactionLog.getRedactionLogEntry()
|
||||
.stream()
|
||||
.filter(RedactionLogEntry::isImage)
|
||||
.toList();
|
||||
|
||||
List<MigrationEntity> migrationEntities = new LinkedList<>();
|
||||
for (RedactionLogEntry redactionLogImage : redactionLogImages) {
|
||||
List<RectangleWithPage> imagePositions = redactionLogImage.getPositions()
|
||||
.stream()
|
||||
.map(RectangleWithPage::fromRedactionLogRectangle)
|
||||
.toList();
|
||||
assert imagePositions.size() == 1;
|
||||
Optional<Image> optionalClosestImage = images.stream()
|
||||
.filter(image -> image.onPage(redactionLogImage.getPositions()
|
||||
.get(0).getPage()))
|
||||
.min(Comparator.comparingDouble(image -> EntityFindingUtility.calculateDistance(image.getPosition(), imagePositions.get(0).rectangle2D())))
|
||||
.filter(image -> EntityFindingUtility.calculateDistance(image.getPosition(), imagePositions.get(0).rectangle2D()) <= MATCH_THRESHOLD);
|
||||
|
||||
Image closestImage;
|
||||
if (optionalClosestImage.isEmpty()) { // if no fitting image can be found create a new one with the previous values!
|
||||
closestImage = buildImageDirectly(document, redactionLogImage);
|
||||
} else {
|
||||
closestImage = optionalClosestImage.get();
|
||||
images.remove(closestImage);
|
||||
}
|
||||
|
||||
String ruleIdentifier;
|
||||
String reason = Optional.ofNullable(redactionLogImage.getReason()).orElse("");
|
||||
if (redactionLogImage.getMatchedRule().isBlank() || redactionLogImage.getMatchedRule() == null) {
|
||||
ruleIdentifier = "OLDIMG.0.0";
|
||||
} else {
|
||||
ruleIdentifier = "OLDIMG." + redactionLogImage.getMatchedRule() + ".0";
|
||||
}
|
||||
|
||||
if (redactionLogImage.lastChangeIsRemoved()) {
|
||||
closestImage.remove(ruleIdentifier, reason);
|
||||
} else if (redactionLogImage.isRedacted()) {
|
||||
closestImage.apply(ruleIdentifier, reason, redactionLogImage.getLegalBasis());
|
||||
} else {
|
||||
closestImage.skip(ruleIdentifier, reason);
|
||||
}
|
||||
migrationEntities.add(new MigrationEntity(null, redactionLogImage, closestImage, redactionLogImage.getId(), closestImage.getId()));
|
||||
}
|
||||
return migrationEntities;
|
||||
}
|
||||
|
||||
|
||||
private static Image buildImageDirectly(Document document, RedactionLogEntry redactionLogImage) {
|
||||
|
||||
Image image = Image.builder()
|
||||
.documentTree(document.getDocumentTree())
|
||||
.imageType(ImageType.fromString(redactionLogImage.getType()))
|
||||
.transparent(redactionLogImage.isImageHasTransparency())
|
||||
.page(document.getPages()
|
||||
.stream()
|
||||
.filter(p -> p.getNumber() == redactionLogImage.getPositions()
|
||||
.get(0).getPage())
|
||||
.findFirst()
|
||||
.orElseThrow())
|
||||
.position(toRectangle2D(redactionLogImage.getPositions()
|
||||
.get(0)))
|
||||
.build();
|
||||
|
||||
List<Integer> treeId = document.getDocumentTree().createNewMainEntryAndReturnId(image);
|
||||
image.setTreeId(treeId);
|
||||
image.setId(IdBuilder.buildId(image.getPages(),
|
||||
image.getBBox().values()
|
||||
.stream()
|
||||
.toList(),
|
||||
"",
|
||||
""));
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
|
||||
private static Rectangle2D toRectangle2D(Rectangle rect) {
|
||||
|
||||
return new Rectangle2D.Double(rect.getTopLeft().getX(), rect.getTopLeft().getY(), rect.getWidth(), rect.getHeight());
|
||||
}
|
||||
|
||||
|
||||
private List<MigrationEntity> getTextBasedMigrationEntities(RedactionLog redactionLog, Document document, String dossierTemplateId) {
|
||||
|
||||
List<MigrationEntity> entitiesToMigrate = redactionLog.getRedactionLogEntry()
|
||||
.stream()
|
||||
.filter(redactionLogEntry -> !redactionLogEntry.isImage())
|
||||
.map(entry -> MigrationEntity.fromRedactionLogEntry(entry, dictionaryService.isHint(entry.getType(), dossierTemplateId)))
|
||||
.peek(migrationEntity -> {
|
||||
if (migrationEntity.getPrecursorEntity().getEntityType().equals(EntityType.HINT) &&//
|
||||
!migrationEntity.getRedactionLogEntry().isHint() &&//
|
||||
!migrationEntity.getRedactionLogEntry().isRedacted()) {
|
||||
migrationEntity.getPrecursorEntity().ignore(migrationEntity.getPrecursorEntity().getRuleIdentifier(), migrationEntity.getPrecursorEntity().getReason());
|
||||
} else if (migrationEntity.getRedactionLogEntry().lastChangeIsRemoved()) {
|
||||
migrationEntity.getPrecursorEntity().remove(migrationEntity.getPrecursorEntity().getRuleIdentifier(), migrationEntity.getPrecursorEntity().getReason());
|
||||
} else if (lastManualChangeIsRemove(migrationEntity)) {
|
||||
migrationEntity.getPrecursorEntity().ignore(migrationEntity.getPrecursorEntity().getRuleIdentifier(), migrationEntity.getPrecursorEntity().getReason());
|
||||
} else if (migrationEntity.getPrecursorEntity().isApplied() && migrationEntity.getRedactionLogEntry().isRecommendation()) {
|
||||
migrationEntity.getPrecursorEntity()
|
||||
.skip(migrationEntity.getPrecursorEntity().getRuleIdentifier(), migrationEntity.getPrecursorEntity().getReason());
|
||||
} else if (migrationEntity.getPrecursorEntity().isApplied()) {
|
||||
migrationEntity.getPrecursorEntity()
|
||||
.apply(migrationEntity.getPrecursorEntity().getRuleIdentifier(),
|
||||
migrationEntity.getPrecursorEntity().getReason(),
|
||||
migrationEntity.getPrecursorEntity().getLegalBasis());
|
||||
} else {
|
||||
migrationEntity.getPrecursorEntity()
|
||||
.skip(migrationEntity.getPrecursorEntity().getRuleIdentifier(), migrationEntity.getPrecursorEntity().getReason());
|
||||
}
|
||||
})
|
||||
.toList();
|
||||
|
||||
Map<String, List<TextEntity>> tempEntitiesByValue = entityFindingUtility.findAllPossibleEntitiesAndGroupByValue(document,
|
||||
entitiesToMigrate.stream()
|
||||
.map(MigrationEntity::getPrecursorEntity)
|
||||
.toList());
|
||||
|
||||
for (MigrationEntity migrationEntity : entitiesToMigrate) {
|
||||
Optional<TextEntity> optionalTextEntity = entityFindingUtility.findClosestEntityAndReturnEmptyIfNotFound(migrationEntity.getPrecursorEntity(),
|
||||
tempEntitiesByValue,
|
||||
MATCH_THRESHOLD);
|
||||
|
||||
if (optionalTextEntity.isEmpty()) {
|
||||
migrationEntity.setMigratedEntity(migrationEntity.getPrecursorEntity());
|
||||
migrationEntity.setOldId(migrationEntity.getPrecursorEntity().getId());
|
||||
migrationEntity.setNewId(migrationEntity.getPrecursorEntity().getId());
|
||||
continue;
|
||||
}
|
||||
|
||||
TextEntity entity = createCorrectEntity(migrationEntity.getPrecursorEntity(), document, optionalTextEntity.get().getTextRange());
|
||||
migrationEntity.setMigratedEntity(entity);
|
||||
migrationEntity.setOldId(migrationEntity.getPrecursorEntity().getId());
|
||||
migrationEntity.setNewId(entity.getId()); // Can only be on one page, since redactionLogEntries can only be on one page
|
||||
|
||||
}
|
||||
|
||||
tempEntitiesByValue.values()
|
||||
.stream()
|
||||
.flatMap(Collection::stream)
|
||||
.forEach(TextEntity::removeFromGraph);
|
||||
return entitiesToMigrate;
|
||||
}
|
||||
|
||||
|
||||
private static boolean lastManualChangeIsRemove(MigrationEntity migrationEntity) {
|
||||
|
||||
if (migrationEntity.getRedactionLogEntry().getManualChanges() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return migrationEntity.getRedactionLogEntry().getManualChanges()
|
||||
.stream()
|
||||
.reduce((a, b) -> b)
|
||||
.map(m -> m.getManualRedactionType().equals(ManualRedactionType.REMOVE_LOCALLY))
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
|
||||
private TextEntity createCorrectEntity(PrecursorEntity precursorEntity, SemanticNode node, TextRange closestTextRange) {
|
||||
|
||||
EntityCreationService entityCreationService = new EntityCreationService(entityEnrichmentService);
|
||||
TextEntity correctEntity = entityCreationService.forceByTextRange(closestTextRange, precursorEntity.getType(), precursorEntity.getEntityType(), node);
|
||||
|
||||
correctEntity.addMatchedRules(precursorEntity.getMatchedRuleList());
|
||||
correctEntity.setDictionaryEntry(precursorEntity.isDictionaryEntry());
|
||||
correctEntity.setDossierDictionaryEntry(precursorEntity.isDossierDictionaryEntry());
|
||||
correctEntity.getManualOverwrite().addChanges(precursorEntity.getManualOverwrite().getManualChangeLog());
|
||||
return correctEntity;
|
||||
}
|
||||
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.iqser.red.service.redaction.v1.server.model;
|
||||
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.entity.TextEntity;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Builder
|
||||
@Getter
|
||||
@Setter
|
||||
public class ClosestEntity {
|
||||
|
||||
private double distance;
|
||||
private TextEntity textEntity;
|
||||
|
||||
}
|
||||
-116
@@ -1,116 +0,0 @@
|
||||
package com.iqser.red.service.redaction.v1.server.model;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.PriorityQueue;
|
||||
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualRedactionEntry;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLogEntry;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.TextRange;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.entity.IEntity;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.entity.EntityType;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.entity.ManualChangeOverwrite;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.entity.MatchedRule;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
|
||||
public class ManualEntity implements IEntity {
|
||||
|
||||
// The id must be mapped into a TextEntity as is for comments to work correctly
|
||||
String id;
|
||||
String value;
|
||||
List<RectangleWithPage> entityPosition;
|
||||
String ruleIdentifier;
|
||||
String reason;
|
||||
String legalBasis;
|
||||
String type;
|
||||
String section;
|
||||
EntityType entityType;
|
||||
boolean applied;
|
||||
boolean isDictionaryEntry;
|
||||
boolean isDossierDictionaryEntry;
|
||||
boolean rectangle;
|
||||
|
||||
@Builder.Default
|
||||
PriorityQueue<MatchedRule> matchedRuleList = new PriorityQueue<>();
|
||||
@Builder.Default
|
||||
ManualChangeOverwrite manualOverwrite = new ManualChangeOverwrite();
|
||||
|
||||
|
||||
public static ManualEntity fromRedactionLogEntry(RedactionLogEntry redactionLogEntry) {
|
||||
|
||||
String ruleIdentifier = redactionLogEntry.getType() + "." + redactionLogEntry.getMatchedRule() + ".0";
|
||||
List<RectangleWithPage> rectangleWithPages = redactionLogEntry.getPositions().stream().map(RectangleWithPage::fromRedactionLogRectangle).toList();
|
||||
return ManualEntity.builder()
|
||||
.id(redactionLogEntry.getId())
|
||||
.value(redactionLogEntry.getValue())
|
||||
.entityPosition(rectangleWithPages)
|
||||
.ruleIdentifier(ruleIdentifier)
|
||||
.reason(redactionLogEntry.getReason())
|
||||
.legalBasis(redactionLogEntry.getLegalBasis())
|
||||
.type(redactionLogEntry.getType())
|
||||
.section(redactionLogEntry.getSection()).entityType(getEntityType(redactionLogEntry))
|
||||
.applied(redactionLogEntry.isRedacted())
|
||||
.isDictionaryEntry(redactionLogEntry.isDictionaryEntry())
|
||||
.isDossierDictionaryEntry(redactionLogEntry.isDossierDictionaryEntry())
|
||||
.rectangle(redactionLogEntry.isRectangle())
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
private static EntityType getEntityType(RedactionLogEntry redactionLogEntry) {
|
||||
|
||||
if (redactionLogEntry.isRecommendation()) {
|
||||
return EntityType.RECOMMENDATION;
|
||||
}
|
||||
if (redactionLogEntry.isHint()) {
|
||||
return EntityType.HINT;
|
||||
}
|
||||
return EntityType.ENTITY;
|
||||
}
|
||||
|
||||
|
||||
public static ManualEntity fromManualRedactionEntry(ManualRedactionEntry manualRedactionEntry, boolean hint) {
|
||||
|
||||
List<RectangleWithPage> rectangleWithPages = manualRedactionEntry.getPositions().stream().map(RectangleWithPage::fromAnnotationRectangle).toList();
|
||||
ManualChangeOverwrite manualChangeOverwrite = new ManualChangeOverwrite();
|
||||
manualChangeOverwrite.addChange(manualRedactionEntry);
|
||||
return ManualEntity.builder()
|
||||
.id(manualRedactionEntry.getAnnotationId())
|
||||
.value(manualRedactionEntry.getValue())
|
||||
.entityPosition(rectangleWithPages)
|
||||
.ruleIdentifier("MAN.0.0")
|
||||
.reason(manualRedactionEntry.getReason())
|
||||
.legalBasis(manualRedactionEntry.getLegalBasis())
|
||||
.type(manualRedactionEntry.getType())
|
||||
.section(manualRedactionEntry.getSection()).entityType(hint ? EntityType.HINT : EntityType.ENTITY)
|
||||
.applied(true)
|
||||
.isDictionaryEntry(false)
|
||||
.isDossierDictionaryEntry(false)
|
||||
.rectangle(manualRedactionEntry.isRectangle())
|
||||
.manualOverwrite(manualChangeOverwrite)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public TextRange getTextRange() {
|
||||
|
||||
return new TextRange(-1, -1);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String type() {
|
||||
|
||||
return getManualOverwrite().getType().orElse(type);
|
||||
}
|
||||
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package com.iqser.red.service.redaction.v1.server.model;
|
||||
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntityLog;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.migration.MigratedIds;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class MigratedEntityLog {
|
||||
|
||||
MigratedIds migratedIds;
|
||||
EntityLog entityLog;
|
||||
|
||||
}
|
||||
+375
@@ -0,0 +1,375 @@
|
||||
package com.iqser.red.service.redaction.v1.server.model;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.ChangeType;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntityLogEntry;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntryState;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntryType;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.Position;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Change;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Engine;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.ManualChange;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.ManualRedactionType;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLogEntry;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.entity.EntityType;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.entity.IEntity;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.entity.ManualChangeOverwrite;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.entity.TextEntity;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.Image;
|
||||
import com.iqser.red.service.redaction.v1.server.service.ManualChangeFactory;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@RequiredArgsConstructor
|
||||
public final class MigrationEntity {
|
||||
|
||||
private final PrecursorEntity precursorEntity;
|
||||
private final RedactionLogEntry redactionLogEntry;
|
||||
private IEntity migratedEntity;
|
||||
private String oldId;
|
||||
private String newId;
|
||||
|
||||
|
||||
public static MigrationEntity fromRedactionLogEntry(RedactionLogEntry redactionLogEntry, boolean hint) {
|
||||
|
||||
return new MigrationEntity(createPrecursorEntity(redactionLogEntry, hint), redactionLogEntry);
|
||||
}
|
||||
|
||||
|
||||
public static PrecursorEntity createPrecursorEntity(RedactionLogEntry redactionLogEntry, boolean hint) {
|
||||
|
||||
String ruleIdentifier = buildRuleIdentifier(redactionLogEntry);
|
||||
List<RectangleWithPage> rectangleWithPages = redactionLogEntry.getPositions().stream().map(RectangleWithPage::fromRedactionLogRectangle).toList();
|
||||
EntityType entityType = getEntityType(redactionLogEntry, hint);
|
||||
return PrecursorEntity.builder()
|
||||
.id(redactionLogEntry.getId())
|
||||
.value(redactionLogEntry.getValue())
|
||||
.entityPosition(rectangleWithPages)
|
||||
.ruleIdentifier(ruleIdentifier)
|
||||
.reason(Optional.ofNullable(redactionLogEntry.getReason()).orElse(""))
|
||||
.legalBasis(redactionLogEntry.getLegalBasis())
|
||||
.type(redactionLogEntry.getType())
|
||||
.section(redactionLogEntry.getSection())
|
||||
.entityType(entityType)
|
||||
.applied(redactionLogEntry.isRedacted())
|
||||
.isDictionaryEntry(redactionLogEntry.isDictionaryEntry())
|
||||
.isDossierDictionaryEntry(redactionLogEntry.isDossierDictionaryEntry())
|
||||
.rectangle(redactionLogEntry.isRectangle())
|
||||
.manualOverwrite(new ManualChangeOverwrite(entityType))
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
private static String buildRuleIdentifier(RedactionLogEntry redactionLogEntry) {
|
||||
|
||||
String ruleIdentifier;
|
||||
if (redactionLogEntry.getMatchedRule() != null) {
|
||||
ruleIdentifier = "OLD." + redactionLogEntry.getMatchedRule() + ".0";
|
||||
} else {
|
||||
ruleIdentifier = "MAN.5.0"; // pure ManualRedactions used to have no matched rule
|
||||
}
|
||||
return ruleIdentifier;
|
||||
}
|
||||
|
||||
|
||||
private static EntityType getEntityType(RedactionLogEntry redactionLogEntry, boolean hint) {
|
||||
|
||||
if (hint) {
|
||||
return EntityType.HINT;
|
||||
}
|
||||
if (redactionLogEntry.isFalsePositive()) {
|
||||
return EntityType.FALSE_POSITIVE;
|
||||
}
|
||||
if (redactionLogEntry.isHint()) {
|
||||
return EntityType.HINT;
|
||||
}
|
||||
if (redactionLogEntry.isRecommendation()) {
|
||||
return EntityType.RECOMMENDATION;
|
||||
}
|
||||
return EntityType.ENTITY;
|
||||
}
|
||||
|
||||
|
||||
private static com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.Change toEntityLogChanges(Change change) {
|
||||
|
||||
return new com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.Change(change.getAnalysisNumber(),
|
||||
toEntityLogType(change.getType()),
|
||||
change.getDateTime());
|
||||
}
|
||||
|
||||
|
||||
private static EntryType getEntryType(EntityType entityType) {
|
||||
|
||||
return switch (entityType) {
|
||||
case ENTITY -> EntryType.ENTITY;
|
||||
case HINT -> EntryType.HINT;
|
||||
case FALSE_POSITIVE -> EntryType.FALSE_POSITIVE;
|
||||
case RECOMMENDATION -> EntryType.RECOMMENDATION;
|
||||
case FALSE_RECOMMENDATION -> EntryType.FALSE_RECOMMENDATION;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private static com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.ManualChange toEntityLogManualChanges(ManualChange manualChange) {
|
||||
|
||||
return new com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.ManualChange(toManualRedactionType(manualChange.getManualRedactionType()),
|
||||
manualChange.getProcessedDate(),
|
||||
manualChange.getRequestedDate(),
|
||||
manualChange.getUserId(),
|
||||
manualChange.getPropertyChanges());
|
||||
}
|
||||
|
||||
|
||||
private static ChangeType toEntityLogType(com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.ChangeType type) {
|
||||
|
||||
return switch (type) {
|
||||
case ADDED -> ChangeType.ADDED;
|
||||
case REMOVED -> ChangeType.REMOVED;
|
||||
case CHANGED -> ChangeType.CHANGED;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private static com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.ManualRedactionType toManualRedactionType(ManualRedactionType manualRedactionType) {
|
||||
|
||||
return switch (manualRedactionType) {
|
||||
case ADD_LOCALLY -> com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.ManualRedactionType.ADD_LOCALLY;
|
||||
case ADD_TO_DICTIONARY -> com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.ManualRedactionType.ADD_TO_DICTIONARY;
|
||||
case REMOVE_LOCALLY -> com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.ManualRedactionType.REMOVE_LOCALLY;
|
||||
case REMOVE_FROM_DICTIONARY -> com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.ManualRedactionType.REMOVE_FROM_DICTIONARY;
|
||||
case FORCE_REDACT -> com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.ManualRedactionType.FORCE_REDACT;
|
||||
case FORCE_HINT -> com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.ManualRedactionType.FORCE_HINT;
|
||||
case RECATEGORIZE -> com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.ManualRedactionType.RECATEGORIZE;
|
||||
case LEGAL_BASIS_CHANGE -> com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.ManualRedactionType.LEGAL_BASIS_CHANGE;
|
||||
case RESIZE -> com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.ManualRedactionType.RESIZE;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
public EntityLogEntry toEntityLogEntry(Map<String, String> oldToNewIdMapping) {
|
||||
|
||||
EntityLogEntry entityLogEntry;
|
||||
if (migratedEntity instanceof Image image) {
|
||||
entityLogEntry = createEntityLogEntry(image);
|
||||
} else if (migratedEntity instanceof TextEntity textEntity) {
|
||||
entityLogEntry = createEntityLogEntry(textEntity);
|
||||
} else if (migratedEntity instanceof PrecursorEntity entity) {
|
||||
entityLogEntry = createEntityLogEntry(entity);
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Unknown subclass " + migratedEntity.getClass());
|
||||
}
|
||||
|
||||
entityLogEntry.setManualChanges(ManualChangeFactory.toManualChangeList(migratedEntity.getManualOverwrite().getManualChangeLog(), redactionLogEntry.isHint()));
|
||||
entityLogEntry.setColor(redactionLogEntry.getColor());
|
||||
entityLogEntry.setChanges(redactionLogEntry.getChanges().stream().map(MigrationEntity::toEntityLogChanges).toList());
|
||||
entityLogEntry.setReference(migrateSetOfIds(redactionLogEntry.getReference(), oldToNewIdMapping));
|
||||
entityLogEntry.setImportedRedactionIntersections(migrateSetOfIds(redactionLogEntry.getImportedRedactionIntersections(), oldToNewIdMapping));
|
||||
entityLogEntry.setEngines(getMigratedEngines(redactionLogEntry));
|
||||
if (redactionLogEntry.getLegalBasis() != null) {
|
||||
entityLogEntry.setLegalBasis(redactionLogEntry.getLegalBasis());
|
||||
}
|
||||
|
||||
if (entityLogEntry.getEntryType().equals(EntryType.HINT) && lastManualChangeIsRemoveLocally(entityLogEntry)) {
|
||||
entityLogEntry.setState(EntryState.IGNORED);
|
||||
}
|
||||
|
||||
return entityLogEntry;
|
||||
}
|
||||
|
||||
|
||||
private static boolean lastManualChangeIsRemoveLocally(EntityLogEntry entityLogEntry) {
|
||||
|
||||
return entityLogEntry.getManualChanges()
|
||||
.stream()
|
||||
.reduce((a, b) -> b)
|
||||
.filter(mc -> mc.getManualRedactionType()
|
||||
.equals(com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.ManualRedactionType.REMOVE_LOCALLY))
|
||||
.isPresent();
|
||||
}
|
||||
|
||||
|
||||
private List<com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.ManualChange> migrateManualChanges(List<ManualChange> manualChanges) {
|
||||
|
||||
if (manualChanges == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return manualChanges.stream().map(MigrationEntity::toEntityLogManualChanges).toList();
|
||||
}
|
||||
|
||||
|
||||
private static Set<com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.Engine> getMigratedEngines(RedactionLogEntry entry) {
|
||||
|
||||
if (entry.getEngines() == null) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
return entry.getEngines().stream().map(MigrationEntity::toEntityLogEngine).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
|
||||
private Set<String> migrateSetOfIds(Set<String> ids, Map<String, String> oldToNewIdMapping) {
|
||||
|
||||
if (ids == null) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
return ids.stream().map(oldToNewIdMapping::get).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
|
||||
private static com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.Engine toEntityLogEngine(Engine engine) {
|
||||
|
||||
return switch (engine) {
|
||||
case DICTIONARY -> com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.Engine.DICTIONARY;
|
||||
case NER -> com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.Engine.NER;
|
||||
case RULE -> com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.Engine.RULE;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
public EntityLogEntry createEntityLogEntry(Image image) {
|
||||
|
||||
List<Position> positions = getPositionsFromOverride(image).orElse(List.of(new Position(image.getPosition(), image.getPage().getNumber())));
|
||||
return EntityLogEntry.builder()
|
||||
.id(image.getId())
|
||||
.value(image.value())
|
||||
.type(image.type())
|
||||
.reason(image.buildReasonWithManualChangeDescriptions())
|
||||
.legalBasis(image.legalBasis())
|
||||
.matchedRule(image.getMatchedRule().getRuleIdentifier().toString())
|
||||
.dictionaryEntry(false)
|
||||
.positions(positions)
|
||||
.containingNodeId(image.getTreeId())
|
||||
.closestHeadline(image.getHeadline().getTextBlock().getSearchText())
|
||||
.section(redactionLogEntry.getSection())
|
||||
.textAfter(redactionLogEntry.getTextAfter())
|
||||
.textBefore(redactionLogEntry.getTextBefore())
|
||||
.imageHasTransparency(image.isTransparent())
|
||||
.state(buildEntryState(image))
|
||||
.entryType(redactionLogEntry.isHint() ? EntryType.IMAGE_HINT : EntryType.IMAGE)
|
||||
.build();
|
||||
|
||||
}
|
||||
|
||||
|
||||
public EntityLogEntry createEntityLogEntry(PrecursorEntity precursorEntity) {
|
||||
|
||||
return EntityLogEntry.builder()
|
||||
.id(precursorEntity.getId())
|
||||
.reason(precursorEntity.buildReasonWithManualChangeDescriptions())
|
||||
.legalBasis(precursorEntity.legalBasis())
|
||||
.value(precursorEntity.value())
|
||||
.type(precursorEntity.type())
|
||||
.state(buildEntryState(precursorEntity))
|
||||
.entryType(buildEntryType(precursorEntity))
|
||||
.section(redactionLogEntry.getSection())
|
||||
.textAfter(redactionLogEntry.getTextAfter())
|
||||
.textBefore(redactionLogEntry.getTextBefore())
|
||||
.containingNodeId(Collections.emptyList())
|
||||
.closestHeadline("")
|
||||
.matchedRule(precursorEntity.getMatchedRule().getRuleIdentifier().toString())
|
||||
.dictionaryEntry(precursorEntity.isDictionaryEntry())
|
||||
.dossierDictionaryEntry(precursorEntity.isDossierDictionaryEntry())
|
||||
.startOffset(-1)
|
||||
.endOffset(-1)
|
||||
.positions(precursorEntity.getManualOverwrite()
|
||||
.getPositions()
|
||||
.orElse(precursorEntity.getEntityPosition())
|
||||
.stream()
|
||||
.map(entityPosition -> new Position(entityPosition.rectangle2D(), entityPosition.pageNumber()))
|
||||
.toList())
|
||||
.engines(Collections.emptySet())
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
public EntityLogEntry createEntityLogEntry(TextEntity entity) {
|
||||
|
||||
assert entity.getPositionsOnPagePerPage().size() == 1;
|
||||
List<Position> rectanglesPerLine = getRectanglesPerLine(entity);
|
||||
return EntityLogEntry.builder()
|
||||
.id(entity.getId())
|
||||
.positions(rectanglesPerLine)
|
||||
.reason(entity.buildReasonWithManualChangeDescriptions())
|
||||
.legalBasis(entity.legalBasis())
|
||||
.value(entity.getManualOverwrite().getValue().orElse(entity.getMatchedRule().isWriteValueWithLineBreaks() ? entity.getValueWithLineBreaks() : entity.getValue()))
|
||||
.type(entity.type())
|
||||
.section(redactionLogEntry.getSection())
|
||||
.textAfter(redactionLogEntry.getTextAfter())
|
||||
.textBefore(redactionLogEntry.getTextBefore())
|
||||
.containingNodeId(entity.getDeepestFullyContainingNode().getTreeId())
|
||||
.closestHeadline(entity.getDeepestFullyContainingNode().getHeadline().getTextBlock().getSearchText())
|
||||
.matchedRule(entity.getMatchedRule().getRuleIdentifier().toString())
|
||||
.dictionaryEntry(entity.isDictionaryEntry())
|
||||
.startOffset(entity.getTextRange().start())
|
||||
.endOffset(entity.getTextRange().end())
|
||||
.dossierDictionaryEntry(entity.isDossierDictionaryEntry())
|
||||
.engines(entity.getEngines() != null ? entity.getEngines() : Collections.emptySet())
|
||||
.state(buildEntryState(entity))
|
||||
.entryType(buildEntryType(entity))
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
private static List<Position> getRectanglesPerLine(TextEntity entity) {
|
||||
|
||||
return getPositionsFromOverride(entity).orElse(entity.getPositionsOnPagePerPage()
|
||||
.get(0)
|
||||
.getRectanglePerLine()
|
||||
.stream()
|
||||
.map(rectangle2D -> new Position(rectangle2D, entity.getPositionsOnPagePerPage().get(0).getPage().getNumber()))
|
||||
.toList());
|
||||
}
|
||||
|
||||
|
||||
private static Optional<List<Position>> getPositionsFromOverride(IEntity entity) {
|
||||
|
||||
return entity.getManualOverwrite().getPositions().map(rects -> rects.stream().map(r -> new Position(r.rectangle2D(), r.pageNumber())).toList());
|
||||
}
|
||||
|
||||
|
||||
private EntryState buildEntryState(IEntity entity) {
|
||||
|
||||
if (entity.applied() && entity.active()) {
|
||||
return EntryState.APPLIED;
|
||||
} else if (entity.skipped() && entity.active()) {
|
||||
return EntryState.SKIPPED;
|
||||
} else if (entity.ignored()) {
|
||||
return EntryState.IGNORED;
|
||||
} else {
|
||||
return EntryState.REMOVED;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private EntryType buildEntryType(IEntity entity) {
|
||||
|
||||
if (entity instanceof TextEntity textEntity) {
|
||||
return getEntryType(textEntity.getEntityType());
|
||||
} else if (entity instanceof PrecursorEntity precursorEntity) {
|
||||
if (precursorEntity.isRectangle()) {
|
||||
return EntryType.AREA;
|
||||
}
|
||||
return getEntryType(precursorEntity.getEntityType());
|
||||
} else if (entity instanceof Image) {
|
||||
return EntryType.IMAGE;
|
||||
}
|
||||
throw new UnsupportedOperationException(String.format("Entity subclass %s is not implemented!", entity.getClass()));
|
||||
}
|
||||
|
||||
|
||||
public boolean hasManualChangesOrComments() {
|
||||
|
||||
return !(redactionLogEntry.getManualChanges() == null || redactionLogEntry.getManualChanges().isEmpty()) || //
|
||||
!(redactionLogEntry.getComments() == null || redactionLogEntry.getComments().isEmpty());
|
||||
}
|
||||
|
||||
}
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
package com.iqser.red.service.redaction.v1.server.model;
|
||||
|
||||
import static com.iqser.red.service.redaction.v1.server.service.NotFoundImportedEntitiesService.IMPORTED_REDACTION_TYPE;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.PriorityQueue;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.Engine;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntityLogEntry;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntryType;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.imported.ImportedRedaction;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualRedactionEntry;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualResizeRedaction;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.TextRange;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.entity.EntityType;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.entity.IEntity;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.entity.ManualChangeOverwrite;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.entity.MatchedRule;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
|
||||
@SuppressWarnings("PMD.AvoidFieldNameMatchingMethodName")
|
||||
public class PrecursorEntity implements IEntity {
|
||||
|
||||
// The id must be mapped into a TextEntity as is for comments to work correctly
|
||||
String id;
|
||||
String value;
|
||||
List<RectangleWithPage> entityPosition;
|
||||
String ruleIdentifier;
|
||||
String reason;
|
||||
String legalBasis;
|
||||
String type;
|
||||
String section;
|
||||
EntityType entityType;
|
||||
EntryType entryType;
|
||||
boolean applied;
|
||||
boolean isDictionaryEntry;
|
||||
boolean isDossierDictionaryEntry;
|
||||
boolean rectangle;
|
||||
Set<Engine> engines;
|
||||
|
||||
@Builder.Default
|
||||
PriorityQueue<MatchedRule> matchedRuleList = new PriorityQueue<>();
|
||||
ManualChangeOverwrite manualOverwrite;
|
||||
|
||||
|
||||
public static PrecursorEntity fromManualRedactionEntry(ManualRedactionEntry manualRedactionEntry, boolean hint) {
|
||||
|
||||
List<RectangleWithPage> rectangleWithPages = manualRedactionEntry.getPositions()
|
||||
.stream()
|
||||
.map(RectangleWithPage::fromAnnotationRectangle)
|
||||
.toList();
|
||||
var entityType = hint ? EntityType.HINT : EntityType.ENTITY;
|
||||
var entryType = hint ? EntryType.HINT : (manualRedactionEntry.isRectangle() ? EntryType.AREA : EntryType.ENTITY);
|
||||
ManualChangeOverwrite manualChangeOverwrite = new ManualChangeOverwrite(entityType);
|
||||
manualChangeOverwrite.addChange(manualRedactionEntry);
|
||||
return PrecursorEntity.builder()
|
||||
.id(manualRedactionEntry.getAnnotationId())
|
||||
.value(manualRedactionEntry.getValue())
|
||||
.entityPosition(rectangleWithPages)
|
||||
.ruleIdentifier("MAN.0.0")
|
||||
.reason(manualRedactionEntry.getReason())
|
||||
.legalBasis(manualRedactionEntry.getLegalBasis())
|
||||
.type(manualRedactionEntry.getType())
|
||||
.section(manualRedactionEntry.getSection())
|
||||
.entityType(entityType)
|
||||
.entryType(entryType)
|
||||
.applied(true)
|
||||
.isDictionaryEntry(false)
|
||||
.isDossierDictionaryEntry(false)
|
||||
.rectangle(manualRedactionEntry.isRectangle())
|
||||
.manualOverwrite(manualChangeOverwrite)
|
||||
.engines(Set.of(Engine.MANUAL))
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
public static PrecursorEntity fromEntityLogEntry(EntityLogEntry entityLogEntry) {
|
||||
|
||||
List<RectangleWithPage> rectangleWithPages = entityLogEntry.getPositions()
|
||||
.stream()
|
||||
.map(RectangleWithPage::fromEntityLogPosition)
|
||||
.toList();
|
||||
EntityType entityType = getEntityType(entityLogEntry.getEntryType());
|
||||
return PrecursorEntity.builder()
|
||||
.id(entityLogEntry.getId())
|
||||
.value(entityLogEntry.getValue())
|
||||
.entityPosition(rectangleWithPages)
|
||||
.ruleIdentifier(entityLogEntry.getMatchedRule())
|
||||
.reason(entityLogEntry.getReason())
|
||||
.legalBasis(entityLogEntry.getLegalBasis())
|
||||
.type(entityLogEntry.getType())
|
||||
.section(entityLogEntry.getSection())
|
||||
.entityType(entityType)
|
||||
.entryType(entityLogEntry.getEntryType())
|
||||
.isDictionaryEntry(entityLogEntry.isDictionaryEntry())
|
||||
.isDossierDictionaryEntry(entityLogEntry.isDossierDictionaryEntry())
|
||||
.manualOverwrite(new ManualChangeOverwrite(entityType))
|
||||
.engines(entityLogEntry.getEngines())
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
public static PrecursorEntity fromImportedEntry(ImportedRedaction importedRedaction) {
|
||||
|
||||
List<RectangleWithPage> rectangleWithPages = importedRedaction.getPositions()
|
||||
.stream()
|
||||
.map(RectangleWithPage::fromEntityLogPosition)
|
||||
.toList();
|
||||
EntryType entryType = Optional.ofNullable(importedRedaction.getEntryType())
|
||||
.orElse(EntryType.ENTITY);
|
||||
EntityType entityType = getEntityType(entryType);
|
||||
String value = Optional.ofNullable(importedRedaction.getValue())
|
||||
.orElse("");
|
||||
return PrecursorEntity.builder()
|
||||
.id(importedRedaction.getId())
|
||||
.value(value)
|
||||
.entityPosition(rectangleWithPages)
|
||||
.reason(Optional.ofNullable(importedRedaction.getReason())
|
||||
.orElse(""))
|
||||
.legalBasis(Optional.ofNullable(importedRedaction.getLegalBasis())
|
||||
.orElse(""))
|
||||
.type(Optional.ofNullable(importedRedaction.getType())
|
||||
.orElse(IMPORTED_REDACTION_TYPE))
|
||||
.entityType(entityType)
|
||||
.entryType(entryType)
|
||||
.isDictionaryEntry(false)
|
||||
.isDossierDictionaryEntry(false)
|
||||
.rectangle(value.isBlank() || entryType.equals(EntryType.IMAGE) || entryType.equals(EntryType.IMAGE_HINT) || entryType.equals(EntryType.AREA))
|
||||
.manualOverwrite(new ManualChangeOverwrite(entityType))
|
||||
.engines(Set.of(Engine.IMPORTED))
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
public static PrecursorEntity fromManualResizeRedaction(ManualResizeRedaction manualResizeRedaction) {
|
||||
|
||||
List<RectangleWithPage> rectangleWithPages = manualResizeRedaction.getPositions()
|
||||
.stream()
|
||||
.map(RectangleWithPage::fromAnnotationRectangle)
|
||||
.toList();
|
||||
return PrecursorEntity.builder()
|
||||
.id(UUID.randomUUID().toString())
|
||||
.value(manualResizeRedaction.getValue())
|
||||
.entityPosition(rectangleWithPages)
|
||||
.entityType(EntityType.ENTITY)
|
||||
.manualOverwrite(new ManualChangeOverwrite(EntityType.ENTITY))
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public TextRange getTextRange() {
|
||||
|
||||
return new TextRange(-1, -1);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String type() {
|
||||
|
||||
return getManualOverwrite().getType()
|
||||
.orElse(type);
|
||||
}
|
||||
|
||||
|
||||
private static EntityType getEntityType(EntryType entryType) {
|
||||
|
||||
switch (entryType) {
|
||||
case FALSE_RECOMMENDATION -> {
|
||||
return EntityType.FALSE_RECOMMENDATION;
|
||||
}
|
||||
case FALSE_POSITIVE -> {
|
||||
return EntityType.FALSE_POSITIVE;
|
||||
}
|
||||
case HINT -> {
|
||||
return EntityType.HINT;
|
||||
}
|
||||
case RECOMMENDATION -> {
|
||||
return EntityType.RECOMMENDATION;
|
||||
}
|
||||
default -> {
|
||||
return EntityType.ENTITY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+15
-2
@@ -2,6 +2,7 @@ package com.iqser.red.service.redaction.v1.server.model;
|
||||
|
||||
import java.awt.geom.Rectangle2D;
|
||||
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.Position;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Rectangle;
|
||||
|
||||
public record RectangleWithPage(int pageNumber, Rectangle2D rectangle2D) {
|
||||
@@ -12,6 +13,12 @@ public record RectangleWithPage(int pageNumber, Rectangle2D rectangle2D) {
|
||||
}
|
||||
|
||||
|
||||
public static RectangleWithPage fromEntityLogPosition(Position position) {
|
||||
|
||||
return new RectangleWithPage(position.getPageNumber(), toRectangle2D(position));
|
||||
}
|
||||
|
||||
|
||||
public static RectangleWithPage fromAnnotationRectangle(com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.Rectangle rectangle) {
|
||||
|
||||
return new RectangleWithPage(rectangle.getPage(), toRectangle2D(rectangle));
|
||||
@@ -20,13 +27,19 @@ public record RectangleWithPage(int pageNumber, Rectangle2D rectangle2D) {
|
||||
|
||||
private static Rectangle2D toRectangle2D(Rectangle rectangle) {
|
||||
|
||||
return new Rectangle2D.Float(rectangle.getTopLeft().getX(), rectangle.getTopLeft().getY() + rectangle.getHeight(), rectangle.getWidth(), -rectangle.getHeight());
|
||||
return new Rectangle2D.Float(rectangle.getTopLeft().getX(), rectangle.getTopLeft().getY(), rectangle.getWidth(), rectangle.getHeight());
|
||||
}
|
||||
|
||||
|
||||
private static Rectangle2D toRectangle2D(Position position) {
|
||||
|
||||
return new Rectangle2D.Float(position.x(), position.y(), position.w(), position.h());
|
||||
}
|
||||
|
||||
|
||||
private static Rectangle2D toRectangle2D(com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.Rectangle rectangle) {
|
||||
|
||||
return new Rectangle2D.Float(rectangle.getTopLeft().getX(), rectangle.getTopLeft().getY() + rectangle.getHeight(), rectangle.getWidth(), -rectangle.getHeight());
|
||||
return new Rectangle2D.Float(rectangle.getTopLeft().getX(), rectangle.getTopLeft().getY(), rectangle.getWidth(), rectangle.getHeight());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+5
-2
@@ -1,6 +1,7 @@
|
||||
package com.iqser.red.service.redaction.v1.server.model.component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.Change;
|
||||
@@ -80,12 +81,14 @@ public class Entity {
|
||||
.legalBasis(e.getLegalBasis())
|
||||
.imported(e.isImported())
|
||||
.section(e.getSection())
|
||||
.color(e.getColor()).positions(e.getPositions()).containingNode(document.getDocumentTree().getEntryById(e.getContainingNodeId()).getNode())
|
||||
.color(e.getColor())
|
||||
.positions(e.getPositions())
|
||||
.containingNode(document.getDocumentTree().getEntryById(e.getContainingNodeId()).getNode())
|
||||
.textBefore(e.getTextBefore())
|
||||
.textAfter(e.getTextAfter())
|
||||
.startOffset(e.getStartOffset())
|
||||
.endOffset(e.getEndOffset())
|
||||
.length(e.getValue().length())
|
||||
.length(Optional.ofNullable(e.getValue()).orElse("").length())
|
||||
.imageHasTransparency(e.isImageHasTransparency())
|
||||
.isDictionaryEntry(e.isDictionaryEntry())
|
||||
.isDossierDictionaryEntry(e.isDossierDictionaryEntry())
|
||||
|
||||
+30
-16
@@ -2,23 +2,23 @@ package com.iqser.red.service.redaction.v1.server.model.dictionary;
|
||||
|
||||
import static java.lang.String.format;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.iqser.red.service.redaction.v1.server.utils.exception.NotFoundException;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.entity.MatchedRule;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.entity.TextEntity;
|
||||
import com.iqser.red.service.redaction.v1.server.utils.Patterns;
|
||||
import com.iqser.red.service.redaction.v1.server.utils.exception.NotFoundException;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
@@ -107,39 +107,53 @@ public class Dictionary {
|
||||
if (StringUtils.isEmpty(value)) {
|
||||
throw new IllegalArgumentException(format("%s is not a valid dictionary entry", value));
|
||||
}
|
||||
boolean isCaseInsensitive = localAccessMap.get(type).isCaseInsensitive();
|
||||
Set<MatchedRule> matchedRulesSet = new HashSet<>(matchedRules);
|
||||
localAccessMap.get(type).getLocalEntriesWithMatchedRules().merge(value.trim(), matchedRulesSet, (set1, set2) -> Stream.concat(set1.stream(), set2.stream()).collect(Collectors.toSet()));
|
||||
|
||||
String cleanedValue = value;
|
||||
if (isCaseInsensitive) {
|
||||
cleanedValue = cleanedValue.toLowerCase(Locale.US);
|
||||
}
|
||||
localAccessMap.get(type)
|
||||
.getLocalEntriesWithMatchedRules()
|
||||
.merge(cleanedValue.trim(), matchedRulesSet, (set1, set2) -> Stream.concat(set1.stream(), set2.stream()).collect(Collectors.toSet()));
|
||||
if (alsoAddLastname) {
|
||||
String lastname = value.split(" ")[0];
|
||||
localAccessMap.get(type).getLocalEntriesWithMatchedRules().merge(lastname, matchedRulesSet, (set1, set2) -> Stream.concat(set1.stream(), set2.stream()).collect(Collectors.toSet()));
|
||||
String lastname = cleanedValue.split(" ")[0];
|
||||
localAccessMap.get(type)
|
||||
.getLocalEntriesWithMatchedRules()
|
||||
.merge(lastname, matchedRulesSet, (set1, set2) -> Stream.concat(set1.stream(), set2.stream()).collect(Collectors.toSet()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void recommendEverywhere(TextEntity textEntity) {
|
||||
|
||||
addLocalDictionaryEntry(textEntity.getType(), textEntity.getValue(), textEntity.getMatchedRuleList(), false);
|
||||
addLocalDictionaryEntry(textEntity.type(), textEntity.getValue(), textEntity.getMatchedRuleList(), false);
|
||||
}
|
||||
|
||||
|
||||
public void recommendEverywhereWithLastNameSeparately(TextEntity textEntity) {
|
||||
|
||||
addLocalDictionaryEntry(textEntity.getType(), textEntity.getValue(), textEntity.getMatchedRuleList(), true);
|
||||
addLocalDictionaryEntry(textEntity.type(), textEntity.getValue(), textEntity.getMatchedRuleList(), true);
|
||||
}
|
||||
|
||||
|
||||
public void addMultipleAuthorsAsRecommendation(TextEntity textEntity) {
|
||||
|
||||
String cleanedWord = textEntity.getValue().replaceAll(",", " ").replaceAll(" ", " ").trim() + " ";
|
||||
Pattern pattern = Patterns.AUTHOR_TABLE_SPLITTER;
|
||||
Matcher matcher = pattern.matcher(cleanedWord);
|
||||
splitIntoAuthorNames(textEntity).forEach(authorName -> addLocalDictionaryEntry(textEntity.type(), authorName, textEntity.getMatchedRuleList(), true));
|
||||
|
||||
while (matcher.find()) {
|
||||
String match = matcher.group().trim();
|
||||
if (match.length() >= 3) {
|
||||
addLocalDictionaryEntry(textEntity.getType(), match, textEntity.getMatchedRuleList(), true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static List<String> splitIntoAuthorNames(TextEntity textEntity) {
|
||||
|
||||
List<String> splitAuthorNames;
|
||||
if (textEntity.getValue().contains(",")) {
|
||||
splitAuthorNames = Arrays.asList(textEntity.getValue().split(","));
|
||||
} else {
|
||||
splitAuthorNames = Arrays.asList(textEntity.getValueWithLineBreaks().split("\n"));
|
||||
}
|
||||
return splitAuthorNames.stream().map(String::trim).filter(authorName -> Patterns.AUTHOR_NAME_PATTERN.matcher(authorName).matches()).toList();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+9
@@ -2,6 +2,8 @@ package com.iqser.red.service.redaction.v1.server.model.dictionary;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -109,4 +111,11 @@ public class DictionaryModel implements Serializable {
|
||||
return falseRecommendationsSearch;
|
||||
}
|
||||
|
||||
public Set<MatchedRule> getMatchedRulesForLocalDictionaryEntry(String value) {
|
||||
var cleanedValue = isCaseInsensitive() ? value.toLowerCase(Locale.US) : value;
|
||||
|
||||
return localEntriesWithMatchedRules.get(cleanedValue);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+6
-2
@@ -1,5 +1,7 @@
|
||||
package com.iqser.red.service.redaction.v1.server.model.document;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.DocumentPage;
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.DocumentPositionData;
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.DocumentStructure;
|
||||
@@ -9,13 +11,15 @@ import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
|
||||
public class DocumentData {
|
||||
@NoArgsConstructor
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
public class DocumentData implements Serializable {
|
||||
|
||||
DocumentPage[] documentPages;
|
||||
DocumentTextData[] documentTextData;
|
||||
|
||||
+1
@@ -13,6 +13,7 @@ import lombok.Setter;
|
||||
|
||||
@Setter
|
||||
@EqualsAndHashCode
|
||||
@SuppressWarnings("PMD.AvoidFieldNameMatchingMethodName")
|
||||
public class TextRange implements Comparable<TextRange> {
|
||||
|
||||
private int start;
|
||||
|
||||
+69
-18
@@ -10,30 +10,29 @@ import java.util.Optional;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.BaseAnnotation;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.IdRemoval;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualForceRedaction;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualRecategorization;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualLegalBasisChange;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualRecategorization;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualRedactionEntry;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualResizeRedaction;
|
||||
import com.iqser.red.service.redaction.v1.server.model.RectangleWithPage;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
public class ManualChangeOverwrite {
|
||||
|
||||
private static final Map<Class<? extends BaseAnnotation>, String> MANUAL_CHANGE_DESCRIPTIONS = Map.of(//
|
||||
ManualRedactionEntry.class, "created by manual change", //
|
||||
ManualLegalBasisChange.class, "legal basis was manually changed", //
|
||||
ManualResizeRedaction.class, "resized by manual override", //
|
||||
ManualForceRedaction.class, "forced by manual override", //
|
||||
IdRemoval.class, "removed by manual override", //
|
||||
ManualRecategorization.class, "recategorized by manual override");
|
||||
ManualRedactionEntry.class, "created by manual change", //
|
||||
ManualLegalBasisChange.class, "legal basis was manually changed", //
|
||||
ManualResizeRedaction.class, "resized by manual override", //
|
||||
ManualForceRedaction.class, "forced by manual override", //
|
||||
IdRemoval.class, "removed by manual override", //
|
||||
ManualRecategorization.class, "recategorized by manual override");
|
||||
|
||||
List<BaseAnnotation> manualChanges = new LinkedList<>();
|
||||
boolean changed;
|
||||
@@ -42,11 +41,25 @@ public class ManualChangeOverwrite {
|
||||
String legalBasis;
|
||||
String section;
|
||||
String value;
|
||||
List<RectangleWithPage> positions;
|
||||
Boolean applied;
|
||||
Boolean removed;
|
||||
Boolean ignored;
|
||||
Boolean resized;
|
||||
Boolean recategorized;
|
||||
EntityType entityType;
|
||||
|
||||
|
||||
public ManualChangeOverwrite() {
|
||||
|
||||
this(EntityType.ENTITY);
|
||||
}
|
||||
|
||||
|
||||
public ManualChangeOverwrite(EntityType entityType) {
|
||||
|
||||
this.entityType = entityType;
|
||||
}
|
||||
|
||||
|
||||
public void calculateCurrentOverride() {
|
||||
@@ -67,7 +80,8 @@ public class ManualChangeOverwrite {
|
||||
manualChanges.sort(Comparator.comparing(BaseAnnotation::getRequestDate));
|
||||
updateFields(manualChanges);
|
||||
// make list unmodifiable.
|
||||
return manualChanges.stream().toList();
|
||||
return manualChanges.stream()
|
||||
.toList();
|
||||
}
|
||||
|
||||
|
||||
@@ -80,13 +94,20 @@ public class ManualChangeOverwrite {
|
||||
|
||||
if (manualChange instanceof IdRemoval) {
|
||||
applied = false;
|
||||
ignored = true;
|
||||
if (sortedManualChanges.get(0) instanceof ManualRedactionEntry) { // set to removed if it's a Manual Redaction
|
||||
removed = true;
|
||||
} else {
|
||||
ignored = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (manualChange instanceof ManualRedactionEntry manualRedactionEntry) {
|
||||
setToApplied();
|
||||
legalBasis = manualRedactionEntry.getLegalBasis();
|
||||
}
|
||||
|
||||
if (manualChange instanceof ManualForceRedaction manualForceRedaction) {
|
||||
removed = false;
|
||||
ignored = false;
|
||||
applied = true;
|
||||
setToApplied();
|
||||
legalBasis = manualForceRedaction.getLegalBasis();
|
||||
}
|
||||
|
||||
@@ -96,16 +117,23 @@ public class ManualChangeOverwrite {
|
||||
value = manualLegalBasisChange.getValue();
|
||||
}
|
||||
|
||||
if (manualChange instanceof ManualResizeRedaction) {
|
||||
// resizing logic happens in ManualChangesApplicationService.
|
||||
if (manualChange instanceof ManualResizeRedaction manualResizeRedaction) {
|
||||
// resizing logic for TextEntities and Images happens in ManualChangesApplicationService.
|
||||
resized = true;
|
||||
// This is only for not found Manual Entities.
|
||||
value = manualResizeRedaction.getValue();
|
||||
positions = manualResizeRedaction.getPositions()
|
||||
.stream()
|
||||
.map(RectangleWithPage::fromAnnotationRectangle)
|
||||
.toList();
|
||||
}
|
||||
|
||||
if (manualChange instanceof ManualRecategorization recategorization) {
|
||||
// recategorization logic happens in ManualChangesApplicationService.
|
||||
recategorized = true;
|
||||
// this is only relevant for ManualEntities. Image and TextEntity is recategorized in the ManualChangesApplicationService.
|
||||
type = recategorization.getType();
|
||||
if (recategorization.getLegalBasis() != null && !recategorization.getLegalBasis().isEmpty()) {
|
||||
legalBasis = recategorization.getLegalBasis();
|
||||
}
|
||||
}
|
||||
|
||||
descriptions.add(MANUAL_CHANGE_DESCRIPTIONS.get(manualChange.getClass()));
|
||||
@@ -114,6 +142,16 @@ public class ManualChangeOverwrite {
|
||||
}
|
||||
|
||||
|
||||
private void setToApplied() {
|
||||
|
||||
removed = false;
|
||||
ignored = false;
|
||||
if (!isHint(entityType)) {
|
||||
applied = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void addChange(BaseAnnotation manualChange) {
|
||||
|
||||
changed = true;
|
||||
@@ -197,4 +235,17 @@ public class ManualChangeOverwrite {
|
||||
return descriptions == null ? Collections.emptyList() : descriptions;
|
||||
}
|
||||
|
||||
|
||||
public Optional<List<RectangleWithPage>> getPositions() {
|
||||
|
||||
calculateCurrentOverride();
|
||||
return positions == null ? Optional.empty() : Optional.of(positions);
|
||||
}
|
||||
|
||||
|
||||
private boolean isHint(EntityType entityType) {
|
||||
|
||||
return entityType.equals(EntityType.HINT);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+18
-3
@@ -24,18 +24,19 @@ public final class MatchedRule implements Comparable<MatchedRule> {
|
||||
|
||||
public static final RuleType FINAL_TYPE = RuleType.fromString("FINAL");
|
||||
public static final RuleType ELIMINATION_RULE_TYPE = RuleType.fromString("X");
|
||||
private static final List<RuleType> RULE_TYPE_PRIORITIES = List.of(FINAL_TYPE, ELIMINATION_RULE_TYPE);
|
||||
public static final RuleType IMPORTED_TYPE = RuleType.fromString("IMP");
|
||||
private static final List<RuleType> RULE_TYPE_PRIORITIES = List.of(FINAL_TYPE, ELIMINATION_RULE_TYPE, IMPORTED_TYPE);
|
||||
|
||||
RuleIdentifier ruleIdentifier;
|
||||
@Builder.Default
|
||||
String reason = "";
|
||||
@Builder.Default
|
||||
String legalBasis = "";
|
||||
boolean applied;
|
||||
boolean writeValueWithLineBreaks;
|
||||
boolean applied;
|
||||
boolean removed;
|
||||
boolean ignored;
|
||||
boolean resized;
|
||||
|
||||
@Builder.Default
|
||||
Set<TextEntity> references = Collections.emptySet();
|
||||
|
||||
@@ -46,6 +47,20 @@ public final class MatchedRule implements Comparable<MatchedRule> {
|
||||
}
|
||||
|
||||
|
||||
public MatchedRule asSkippedIfApplied() {
|
||||
|
||||
if (!this.isApplied()) {
|
||||
return this;
|
||||
}
|
||||
return MatchedRule.builder().ruleIdentifier(getRuleIdentifier())
|
||||
.writeValueWithLineBreaks(this.isWriteValueWithLineBreaks())
|
||||
.legalBasis(this.getLegalBasis())
|
||||
.reason(this.getReason())
|
||||
.references(this.getReferences())
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int compareTo(MatchedRule matchedRule) {
|
||||
|
||||
|
||||
+40
-13
@@ -1,6 +1,7 @@
|
||||
package com.iqser.red.service.redaction.v1.server.model.document.entity;
|
||||
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
@@ -28,21 +29,23 @@ import lombok.experimental.FieldDefaults;
|
||||
@AllArgsConstructor
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
|
||||
@SuppressWarnings("PMD.AvoidFieldNameMatchingMethodName")
|
||||
public class TextEntity implements IEntity {
|
||||
|
||||
// primary key
|
||||
@EqualsAndHashCode.Include
|
||||
final TextRange textRange;
|
||||
@EqualsAndHashCode.Include
|
||||
final String type;
|
||||
@EqualsAndHashCode.Include
|
||||
final EntityType entityType;
|
||||
final String id;
|
||||
// primary key end
|
||||
|
||||
TextRange textRange;
|
||||
@Builder.Default
|
||||
List<TextRange> duplicateTextRanges = new ArrayList<>();
|
||||
String type; // TODO: make final once ManualChangesApplicatioService recategorize is deleted
|
||||
final EntityType entityType;
|
||||
|
||||
@Builder.Default
|
||||
final PriorityQueue<MatchedRule> matchedRuleList = new PriorityQueue<>();
|
||||
@Builder.Default
|
||||
final ManualChangeOverwrite manualOverwrite = new ManualChangeOverwrite();
|
||||
final ManualChangeOverwrite manualOverwrite;
|
||||
|
||||
boolean dictionaryEntry;
|
||||
boolean dossierDictionaryEntry;
|
||||
@@ -62,12 +65,30 @@ public class TextEntity implements IEntity {
|
||||
SemanticNode deepestFullyContainingNode;
|
||||
|
||||
|
||||
public static TextEntity initialEntityNode(TextRange textRange, String type, EntityType entityType) {
|
||||
public static TextEntity initialEntityNode(TextRange textRange, String type, EntityType entityType, SemanticNode node) {
|
||||
|
||||
return TextEntity.builder().type(type).entityType(entityType).textRange(textRange).build();
|
||||
return TextEntity.builder().id(buildId(node, textRange, type, entityType)).type(type).entityType(entityType).textRange(textRange).manualOverwrite(new ManualChangeOverwrite(entityType)).build();
|
||||
}
|
||||
|
||||
|
||||
public static TextEntity initialEntityNode(TextRange textRange, String type, EntityType entityType, String id) {
|
||||
|
||||
return TextEntity.builder().id(id).type(type).entityType(entityType).textRange(textRange).manualOverwrite(new ManualChangeOverwrite(entityType)).build();
|
||||
}
|
||||
|
||||
|
||||
private static String buildId(SemanticNode node, TextRange textRange, String type, EntityType entityType) {
|
||||
|
||||
Map<Page, List<Rectangle2D>> rectanglesPerLinePerPage = node.getPositionsPerPage(textRange);
|
||||
return IdBuilder.buildId(rectanglesPerLinePerPage.keySet(), rectanglesPerLinePerPage.values().stream().flatMap(Collection::stream).toList(), type, entityType.name());
|
||||
}
|
||||
|
||||
|
||||
public void addTextRange(TextRange textRange) {
|
||||
|
||||
duplicateTextRanges.add(textRange);
|
||||
}
|
||||
|
||||
public boolean occursInNodeOfType(Class<? extends SemanticNode> clazz) {
|
||||
|
||||
return intersectingNodes.stream().anyMatch(clazz::isInstance);
|
||||
@@ -82,13 +103,13 @@ public class TextEntity implements IEntity {
|
||||
|
||||
public boolean isType(String type) {
|
||||
|
||||
return this.type.equals(type);
|
||||
return type().equals(type);
|
||||
}
|
||||
|
||||
|
||||
public boolean isAnyType(List<String> types) {
|
||||
|
||||
return types.contains(type);
|
||||
return types.contains(type());
|
||||
}
|
||||
|
||||
|
||||
@@ -125,7 +146,6 @@ public class TextEntity implements IEntity {
|
||||
.min(Comparator.comparingInt(Page::getNumber))
|
||||
.orElseThrow(() -> new RuntimeException("No Positions found on any page!"));
|
||||
|
||||
String id = IdBuilder.buildId(pages, rectanglesPerLinePerPage.values().stream().flatMap(Collection::stream).toList(), type, entityType.name());
|
||||
positionsOnPagePerPage = rectanglesPerLinePerPage.entrySet().stream().map(entry -> buildPositionOnPage(firstPage, id, entry)).toList();
|
||||
}
|
||||
return positionsOnPagePerPage;
|
||||
@@ -193,7 +213,7 @@ public class TextEntity implements IEntity {
|
||||
});
|
||||
sb.delete(sb.length() - 2, sb.length());
|
||||
sb.append("], type = \"");
|
||||
sb.append(type);
|
||||
sb.append(type());
|
||||
sb.append("\", EntityType.");
|
||||
sb.append(entityType);
|
||||
sb.append("]");
|
||||
@@ -207,4 +227,11 @@ public class TextEntity implements IEntity {
|
||||
return getManualOverwrite().getType().orElse(type);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String value() {
|
||||
|
||||
return getManualOverwrite().getValue().orElse(getMatchedRule().isWriteValueWithLineBreaks() ? getValueWithLineBreaks() : value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+5
-7
@@ -19,6 +19,7 @@ import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
|
||||
@@ -27,8 +28,12 @@ import lombok.experimental.FieldDefaults;
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
|
||||
public class Document implements GenericSemanticNode {
|
||||
|
||||
@EqualsAndHashCode.Include
|
||||
List<Integer> treeId = Collections.emptyList();
|
||||
|
||||
Set<Page> pages;
|
||||
DocumentTree documentTree;
|
||||
Integer numberOfPages;
|
||||
@@ -67,13 +72,6 @@ public class Document implements GenericSemanticNode {
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public List<Integer> getTreeId() {
|
||||
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void setTreeId(List<Integer> tocId) {
|
||||
|
||||
|
||||
+17
-3
@@ -1,7 +1,9 @@
|
||||
package com.iqser.red.service.redaction.v1.server.model.document.nodes;
|
||||
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.DocumentTree;
|
||||
@@ -21,21 +23,23 @@ import lombok.experimental.FieldDefaults;
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
|
||||
public class Footer implements GenericSemanticNode {
|
||||
|
||||
@Builder.Default
|
||||
final static SectionIdentifier sectionIdentifier = SectionIdentifier.empty();
|
||||
|
||||
@EqualsAndHashCode.Include
|
||||
List<Integer> treeId;
|
||||
|
||||
TextBlock leafTextBlock;
|
||||
|
||||
@EqualsAndHashCode.Exclude
|
||||
DocumentTree documentTree;
|
||||
|
||||
@Builder.Default
|
||||
@EqualsAndHashCode.Exclude
|
||||
Set<TextEntity> entities = new HashSet<>();
|
||||
|
||||
Map<Page, Rectangle2D> bBoxCache;
|
||||
|
||||
|
||||
@Override
|
||||
public NodeType getType() {
|
||||
@@ -71,4 +75,14 @@ public class Footer implements GenericSemanticNode {
|
||||
return treeId + ": " + NodeType.FOOTER + ": " + leafTextBlock.buildSummary();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Map<Page, Rectangle2D> getBBox() {
|
||||
|
||||
if (bBoxCache == null) {
|
||||
bBoxCache = GenericSemanticNode.super.getBBox();
|
||||
}
|
||||
return bBoxCache;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+17
-3
@@ -1,7 +1,9 @@
|
||||
package com.iqser.red.service.redaction.v1.server.model.document.nodes;
|
||||
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.entity.TextEntity;
|
||||
@@ -21,21 +23,23 @@ import lombok.experimental.FieldDefaults;
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
|
||||
public class Header implements GenericSemanticNode {
|
||||
|
||||
@Builder.Default
|
||||
final static SectionIdentifier sectionIdentifier = SectionIdentifier.empty();
|
||||
|
||||
@EqualsAndHashCode.Include
|
||||
List<Integer> treeId;
|
||||
|
||||
TextBlock leafTextBlock;
|
||||
|
||||
@EqualsAndHashCode.Exclude
|
||||
DocumentTree documentTree;
|
||||
|
||||
@Builder.Default
|
||||
@EqualsAndHashCode.Exclude
|
||||
Set<TextEntity> entities = new HashSet<>();
|
||||
|
||||
Map<Page, Rectangle2D> bBoxCache;
|
||||
|
||||
|
||||
@Override
|
||||
public boolean isLeaf() {
|
||||
@@ -71,4 +75,14 @@ public class Header implements GenericSemanticNode {
|
||||
return treeId + ": " + NodeType.HEADER + ": " + leafTextBlock.buildSummary();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Map<Page, Rectangle2D> getBBox() {
|
||||
|
||||
if (bBoxCache == null) {
|
||||
bBoxCache = GenericSemanticNode.super.getBBox();
|
||||
}
|
||||
return bBoxCache;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+16
-2
@@ -1,7 +1,9 @@
|
||||
package com.iqser.red.service.redaction.v1.server.model.document.nodes;
|
||||
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.DocumentTree;
|
||||
@@ -22,19 +24,21 @@ import lombok.experimental.FieldDefaults;
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
|
||||
public class Headline implements GenericSemanticNode {
|
||||
|
||||
@EqualsAndHashCode.Include
|
||||
List<Integer> treeId;
|
||||
TextBlock leafTextBlock;
|
||||
SectionIdentifier sectionIdentifier;
|
||||
|
||||
@EqualsAndHashCode.Exclude
|
||||
DocumentTree documentTree;
|
||||
|
||||
@Builder.Default
|
||||
@EqualsAndHashCode.Exclude
|
||||
Set<TextEntity> entities = new HashSet<>();
|
||||
|
||||
Map<Page, Rectangle2D> bBoxCache;
|
||||
|
||||
|
||||
@Override
|
||||
public NodeType getType() {
|
||||
@@ -71,6 +75,16 @@ public class Headline implements GenericSemanticNode {
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Map<Page, Rectangle2D> getBBox() {
|
||||
|
||||
if (bBoxCache == null) {
|
||||
bBoxCache = GenericSemanticNode.super.getBBox();
|
||||
}
|
||||
return bBoxCache;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public SectionIdentifier getSectionIdentifier() {
|
||||
|
||||
|
||||
+2
@@ -32,8 +32,10 @@ import lombok.experimental.FieldDefaults;
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
|
||||
public class Image implements GenericSemanticNode, IEntity {
|
||||
|
||||
@EqualsAndHashCode.Include
|
||||
List<Integer> treeId;
|
||||
String id;
|
||||
|
||||
|
||||
+2
-20
@@ -23,26 +23,23 @@ import lombok.experimental.FieldDefaults;
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
|
||||
public class Page {
|
||||
|
||||
@EqualsAndHashCode.Include
|
||||
Integer number;
|
||||
Integer height;
|
||||
Integer width;
|
||||
Integer rotation;
|
||||
|
||||
@EqualsAndHashCode.Exclude
|
||||
List<SemanticNode> mainBody;
|
||||
@EqualsAndHashCode.Exclude
|
||||
Header header;
|
||||
@EqualsAndHashCode.Exclude
|
||||
Footer footer;
|
||||
|
||||
@Builder.Default
|
||||
@EqualsAndHashCode.Exclude
|
||||
Set<TextEntity> entities = new HashSet<>();
|
||||
|
||||
@Builder.Default
|
||||
@EqualsAndHashCode.Exclude
|
||||
Set<Image> images = new HashSet<>();
|
||||
|
||||
|
||||
@@ -57,19 +54,4 @@ public class Page {
|
||||
|
||||
return String.valueOf(number);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
|
||||
return number;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
|
||||
return o instanceof Page && o.hashCode() == this.hashCode();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+16
-2
@@ -1,7 +1,9 @@
|
||||
package com.iqser.red.service.redaction.v1.server.model.document.nodes;
|
||||
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.DocumentTree;
|
||||
@@ -19,18 +21,20 @@ import lombok.experimental.FieldDefaults;
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
|
||||
public class Paragraph implements GenericSemanticNode {
|
||||
|
||||
@EqualsAndHashCode.Include
|
||||
List<Integer> treeId;
|
||||
TextBlock leafTextBlock;
|
||||
|
||||
@EqualsAndHashCode.Exclude
|
||||
DocumentTree documentTree;
|
||||
|
||||
@Builder.Default
|
||||
@EqualsAndHashCode.Exclude
|
||||
Set<TextEntity> entities = new HashSet<>();
|
||||
|
||||
Map<Page, Rectangle2D> bBoxCache;
|
||||
|
||||
|
||||
@Override
|
||||
public NodeType getType() {
|
||||
@@ -59,4 +63,14 @@ public class Paragraph implements GenericSemanticNode {
|
||||
return treeId + ": " + NodeType.PARAGRAPH + ": " + leafTextBlock.buildSummary();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Map<Page, Rectangle2D> getBBox() {
|
||||
|
||||
if (bBoxCache == null) {
|
||||
bBoxCache = GenericSemanticNode.super.getBBox();
|
||||
}
|
||||
return bBoxCache;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+16
-3
@@ -1,7 +1,9 @@
|
||||
package com.iqser.red.service.redaction.v1.server.model.document.nodes;
|
||||
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.entity.TextEntity;
|
||||
@@ -22,18 +24,19 @@ import lombok.extern.slf4j.Slf4j;
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
|
||||
public class Section implements GenericSemanticNode {
|
||||
|
||||
@EqualsAndHashCode.Include
|
||||
List<Integer> treeId;
|
||||
|
||||
TextBlock textBlock;
|
||||
@EqualsAndHashCode.Exclude
|
||||
DocumentTree documentTree;
|
||||
|
||||
@Builder.Default
|
||||
@EqualsAndHashCode.Exclude
|
||||
Set<TextEntity> entities = new HashSet<>();
|
||||
|
||||
Map<Page, Rectangle2D> bBoxCache;
|
||||
|
||||
|
||||
@Override
|
||||
public NodeType getType() {
|
||||
@@ -92,4 +95,14 @@ public class Section implements GenericSemanticNode {
|
||||
return streamAllSubNodesOfType(NodeType.HEADLINE).anyMatch(h -> h.containsStringIgnoreCase(value));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Map<Page, Rectangle2D> getBBox() {
|
||||
|
||||
if (bBoxCache == null) {
|
||||
bBoxCache = GenericSemanticNode.super.getBBox();
|
||||
}
|
||||
return bBoxCache;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+144
-26
@@ -19,6 +19,7 @@ import com.iqser.red.service.redaction.v1.server.model.document.TextRange;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.entity.TextEntity;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.textblock.AtomicTextBlock;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.textblock.TextBlock;
|
||||
import com.iqser.red.service.redaction.v1.server.service.document.NodeVisitor;
|
||||
import com.iqser.red.service.redaction.v1.server.utils.RectangleTransformations;
|
||||
import com.iqser.red.service.redaction.v1.server.utils.RedactionSearchUtility;
|
||||
|
||||
@@ -61,6 +62,7 @@ public interface SemanticNode {
|
||||
return getTextBlock().getPages();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Finds the first page associated with this Node.
|
||||
*
|
||||
@@ -79,13 +81,19 @@ public interface SemanticNode {
|
||||
*/
|
||||
default Set<Page> getPages(TextRange textRange) {
|
||||
|
||||
if (!getTextRange().contains(textRange)) {
|
||||
throw new IllegalArgumentException(format("%s which was used to query for pages is not contained in the %s of this node!", textRange, getTextRange()));
|
||||
if (!getTextRange().intersects(textRange)) {
|
||||
throw new IllegalArgumentException(format("%s which was used to query for pages is not intersected in the %s of this node!", textRange, getTextRange()));
|
||||
}
|
||||
return getTextBlock().getPages(textRange);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the given page number exists in the list of pages.
|
||||
*
|
||||
* @param pageNumber the page number to be checked
|
||||
* @return true if the page number exists, otherwise false
|
||||
*/
|
||||
default boolean onPage(int pageNumber) {
|
||||
|
||||
return getPages().stream().anyMatch(page -> page.getNumber() == pageNumber);
|
||||
@@ -240,7 +248,7 @@ public interface SemanticNode {
|
||||
*/
|
||||
default boolean hasEntitiesOfType(String type) {
|
||||
|
||||
return getEntities().stream().filter(TextEntity::active).anyMatch(redactionEntity -> redactionEntity.getType().equals(type));
|
||||
return getEntities().stream().filter(TextEntity::active).anyMatch(redactionEntity -> redactionEntity.type().equals(type));
|
||||
}
|
||||
|
||||
|
||||
@@ -253,7 +261,7 @@ public interface SemanticNode {
|
||||
*/
|
||||
default boolean hasEntitiesOfAnyType(String... types) {
|
||||
|
||||
return getEntities().stream().filter(TextEntity::active).anyMatch(redactionEntity -> Arrays.stream(types).anyMatch(type -> redactionEntity.getType().equals(type)));
|
||||
return getEntities().stream().filter(TextEntity::active).anyMatch(redactionEntity -> Arrays.stream(types).anyMatch(type -> redactionEntity.type().equals(type)));
|
||||
}
|
||||
|
||||
|
||||
@@ -266,11 +274,7 @@ public interface SemanticNode {
|
||||
*/
|
||||
default boolean hasEntitiesOfAllTypes(String... types) {
|
||||
|
||||
return getEntities().stream()
|
||||
.filter(TextEntity::active)
|
||||
.map(TextEntity::getType)
|
||||
.collect(Collectors.toUnmodifiableSet())
|
||||
.containsAll(Arrays.stream(types).toList());
|
||||
return getEntities().stream().filter(TextEntity::active).map(TextEntity::type).collect(Collectors.toUnmodifiableSet()).containsAll(Arrays.stream(types).toList());
|
||||
}
|
||||
|
||||
|
||||
@@ -283,7 +287,7 @@ public interface SemanticNode {
|
||||
*/
|
||||
default List<TextEntity> getEntitiesOfType(String type) {
|
||||
|
||||
return getEntities().stream().filter(TextEntity::active).filter(redactionEntity -> redactionEntity.getType().equals(type)).toList();
|
||||
return getEntities().stream().filter(TextEntity::active).filter(redactionEntity -> redactionEntity.type().equals(type)).toList();
|
||||
}
|
||||
|
||||
|
||||
@@ -425,6 +429,78 @@ public interface SemanticNode {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks whether this SemanticNode contains exactly the provided String as a word.
|
||||
*
|
||||
* @param word - String which the TextBlock might contain
|
||||
* @return true, if this node's TextBlock contains string
|
||||
*/
|
||||
default boolean containsWord(String word) {
|
||||
|
||||
return getTextBlock().getWords().stream().anyMatch(s -> s.equals(word));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks whether this SemanticNode contains exactly the provided String as a word ignoring case.
|
||||
*
|
||||
* @param word - String which the TextBlock might contain
|
||||
* @return true, if this node's TextBlock contains string
|
||||
*/
|
||||
default boolean containsWordIgnoreCase(String word) {
|
||||
|
||||
return getTextBlock().getWords().stream().map(String::toLowerCase).anyMatch(s -> s.equals(word.toLowerCase(Locale.ENGLISH)));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks whether this SemanticNode contains any of the provided Strings as a word.
|
||||
*
|
||||
* @param words - A List of Strings which the TextBlock might contain
|
||||
* @return true, if this node's TextBlock contains any of the provided strings
|
||||
*/
|
||||
default boolean containsAnyWord(String... words) {
|
||||
|
||||
return Arrays.stream(words).anyMatch(word -> getTextBlock().getWords().stream().anyMatch(word::equals));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks whether this SemanticNode contains any of the provided Strings as a word ignoring case.
|
||||
*
|
||||
* @param words - A List of Strings which the TextBlock might contain
|
||||
* @return true, if this node's TextBlock contains any of the provided strings
|
||||
*/
|
||||
default boolean containsAnyWordIgnoreCase(String... words) {
|
||||
|
||||
return Arrays.stream(words).map(String::toLowerCase).anyMatch(word -> getTextBlock().getWords().stream().map(String::toLowerCase).anyMatch(word::equals));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks whether this SemanticNode contains all the provided Strings as word.
|
||||
*
|
||||
* @param words - A List of Strings which the TextBlock might contain
|
||||
* @return true, if this node's TextBlock contains all the provided strings
|
||||
*/
|
||||
default boolean containsAllWords(String... words) {
|
||||
|
||||
return Arrays.stream(words).allMatch(word -> getTextBlock().getWords().stream().anyMatch(word::equals));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks whether this SemanticNode contains all the provided Strings as word ignoring case.
|
||||
*
|
||||
* @param words - A List of Strings which the TextBlock might contain
|
||||
* @return true, if this node's TextBlock contains all the provided strings
|
||||
*/
|
||||
default boolean containsAllWordsIgnoreCase(String... words) {
|
||||
|
||||
return Arrays.stream(words).map(String::toLowerCase).allMatch(word -> getTextBlock().getWords().stream().map(String::toLowerCase).anyMatch(word::equals));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks whether this SemanticNode matches the provided regex pattern.
|
||||
*
|
||||
@@ -451,10 +527,11 @@ public interface SemanticNode {
|
||||
|
||||
/**
|
||||
* Checks whether this SemanticNode intersects the provided rectangle.
|
||||
* @param x the lower left corner X value
|
||||
* @param y the lower left corner Y value
|
||||
* @param w width
|
||||
* @param h height
|
||||
*
|
||||
* @param x the lower left corner X value
|
||||
* @param y the lower left corner Y value
|
||||
* @param w width
|
||||
* @param h height
|
||||
* @param pageNumber the pageNumber of the rectangle
|
||||
* @return true if intersects, false otherwise
|
||||
*/
|
||||
@@ -551,6 +628,27 @@ public interface SemanticNode {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* For a given TextRange this function returns a List of rectangle around the text in the range.
|
||||
* These Rectangles are split either by a new line or by a large gap in the current line.
|
||||
* This is mainly used to find the positions of TextEntities
|
||||
*
|
||||
* @param textRange A TextRange to calculate the positions for.
|
||||
* @return A Map, where the keys are the pages and the values are a list of rectangles describing the position of words
|
||||
*/
|
||||
default Map<Page, List<Rectangle2D>> getPositionsPerPage(TextRange textRange) {
|
||||
|
||||
if (isLeaf()) {
|
||||
return getTextBlock().getPositionsPerPage(textRange);
|
||||
}
|
||||
Optional<SemanticNode> containingChildNode = streamChildren().filter(child -> child.getTextRange().contains(textRange)).findFirst();
|
||||
if (containingChildNode.isEmpty()) {
|
||||
return getTextBlock().getPositionsPerPage(textRange);
|
||||
}
|
||||
return containingChildNode.get().getPositionsPerPage(textRange);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* If this Node is a Leaf it will calculate the boundingBox of its LeafTextBlock, otherwise it will calculate the Union of the BoundingBoxes of all its Children.
|
||||
* If called on the Document, it will return the cropbox of each page
|
||||
@@ -559,12 +657,11 @@ public interface SemanticNode {
|
||||
*/
|
||||
default Map<Page, Rectangle2D> getBBox() {
|
||||
|
||||
Map<Page, Rectangle2D> bBoxPerPage = new HashMap<>();
|
||||
if (isLeaf()) {
|
||||
return getBBoxFromLeafTextBlock(bBoxPerPage);
|
||||
return getBBoxFromLeafTextBlock();
|
||||
}
|
||||
|
||||
return getBBoxFromChildren(bBoxPerPage);
|
||||
return getBBoxFromChildren();
|
||||
}
|
||||
|
||||
|
||||
@@ -587,28 +684,49 @@ public interface SemanticNode {
|
||||
|
||||
/**
|
||||
* TODO: this produces unwanted results for sections spanning multiple columns.
|
||||
* Computes the Union of the bounding boxes of all children recursively.
|
||||
*
|
||||
* @param bBoxPerPage initial empty BoundingBox
|
||||
* @return The union of the BoundingBoxes of all children
|
||||
*/
|
||||
private Map<Page, Rectangle2D> getBBoxFromChildren(Map<Page, Rectangle2D> bBoxPerPage) {
|
||||
private Map<Page, Rectangle2D> getBBoxFromChildren() {
|
||||
|
||||
return streamChildren().map(SemanticNode::getBBox).reduce((map1, map2) -> {
|
||||
map1.forEach((page, rectangle) -> map2.merge(page, rectangle, (rect1, rect2) -> rect1.createUnion(rect2).getBounds2D()));
|
||||
return map2;
|
||||
}).orElse(bBoxPerPage);
|
||||
Map<Page, Rectangle2D> bBoxPerPage = new HashMap<>();
|
||||
List<Map<Page, Rectangle2D>> childrenBBoxes = streamChildren().map(SemanticNode::getBBox).toList();
|
||||
Set<Page> pages = childrenBBoxes.stream().flatMap(map -> map.keySet().stream()).collect(Collectors.toSet());
|
||||
for (Page page : pages) {
|
||||
Rectangle2D bBoxOnPage = childrenBBoxes.stream()
|
||||
.filter(childBboxPerPage -> childBboxPerPage.containsKey(page))
|
||||
.map(childBboxPerPage -> childBboxPerPage.get(page))
|
||||
.collect(RectangleTransformations.collectBBox());
|
||||
bBoxPerPage.put(page, bBoxOnPage);
|
||||
}
|
||||
return bBoxPerPage;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param bBoxPerPage initial empty BoundingBox
|
||||
* @return The union of all BoundingBoxes of the TextBlock of this node
|
||||
*/
|
||||
private Map<Page, Rectangle2D> getBBoxFromLeafTextBlock(Map<Page, Rectangle2D> bBoxPerPage) {
|
||||
private Map<Page, Rectangle2D> getBBoxFromLeafTextBlock() {
|
||||
|
||||
Map<Page, Rectangle2D> bBoxPerPage = new HashMap<>();
|
||||
Map<Page, List<AtomicTextBlock>> atomicTextBlockPerPage = getTextBlock().getAtomicTextBlocks().stream().collect(Collectors.groupingBy(AtomicTextBlock::getPage));
|
||||
atomicTextBlockPerPage.forEach((page, atomicTextBlocks) -> bBoxPerPage.put(page, RectangleTransformations.atomicTextBlockBBox(atomicTextBlocks)));
|
||||
return bBoxPerPage;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts a {@link NodeVisitor} and initiates a depth-first traversal of the semantic tree rooted at this node.
|
||||
* The visitor's {@link NodeVisitor#visit(SemanticNode)} method is invoked for each node encountered during the traversal.
|
||||
*
|
||||
* @param visitor The {@link NodeVisitor} to accept and apply during the traversal.
|
||||
* @see NodeVisitor
|
||||
*/
|
||||
default void accept(NodeVisitor visitor) {
|
||||
|
||||
visitor.visit(this);
|
||||
streamChildren().forEach(childNode -> childNode.accept(visitor));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+36
-5
@@ -2,10 +2,12 @@ package com.iqser.red.service.redaction.v1.server.model.document.nodes;
|
||||
|
||||
import static java.lang.String.format;
|
||||
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.Stream;
|
||||
@@ -26,8 +28,10 @@ import lombok.experimental.FieldDefaults;
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
|
||||
public class Table implements SemanticNode {
|
||||
|
||||
@EqualsAndHashCode.Include
|
||||
List<Integer> treeId;
|
||||
DocumentTree documentTree;
|
||||
|
||||
@@ -37,9 +41,20 @@ public class Table implements SemanticNode {
|
||||
TextBlock textBlock;
|
||||
|
||||
@Builder.Default
|
||||
@EqualsAndHashCode.Exclude
|
||||
Set<TextEntity> entities = new HashSet<>();
|
||||
|
||||
Map<Page, Rectangle2D> bBoxCache;
|
||||
|
||||
|
||||
@Override
|
||||
public Map<Page, Rectangle2D> getBBox() {
|
||||
|
||||
if (bBoxCache == null) {
|
||||
bBoxCache = SemanticNode.super.getBBox();
|
||||
}
|
||||
return bBoxCache;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Streams all entities in this table, that appear in a row, which contains any of the provided strings.
|
||||
@@ -113,7 +128,7 @@ public class Table implements SemanticNode {
|
||||
|
||||
List<Integer> rowsWithEntityOfType = getEntities().stream()
|
||||
.filter(TextEntity::active)
|
||||
.filter(redactionEntity -> types.stream().anyMatch(type -> type.equals(redactionEntity.getType())))
|
||||
.filter(redactionEntity -> types.stream().anyMatch(type -> type.equals(redactionEntity.type())))
|
||||
.map(TextEntity::getIntersectingNodes)
|
||||
.filter(node -> node instanceof TableCell)
|
||||
.map(node -> (TableCell) node)
|
||||
@@ -138,7 +153,7 @@ public class Table implements SemanticNode {
|
||||
.filter(rowNumber -> streamRow(rowNumber).map(TableCell::getEntities)
|
||||
.flatMap(Collection::stream)
|
||||
.filter(TextEntity::active)
|
||||
.noneMatch(entity -> types.contains(entity.getType())))
|
||||
.noneMatch(entity -> types.contains(entity.type())))
|
||||
.flatMap(this::streamRow)
|
||||
.map(TableCell::getEntities)
|
||||
.flatMap(Collection::stream);
|
||||
@@ -173,6 +188,18 @@ public class Table implements SemanticNode {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Streams all TableCells that contain at least one entity of a given type in this Table row-wise.
|
||||
*
|
||||
* @param type The type of the entity
|
||||
* @return Stream of filtered TableCells
|
||||
*/
|
||||
public Stream<TableCell> streamTableCellsWhichContainType(String type) {
|
||||
|
||||
return streamTableCells().filter(tableCell -> tableCell.getEntities().stream().filter(TextEntity::active).anyMatch(entity -> entity.type().equals(type)));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Streams all TableCells in this Table which have the provided header row-wise.
|
||||
*
|
||||
@@ -255,7 +282,11 @@ public class Table implements SemanticNode {
|
||||
*/
|
||||
public boolean hasHeaderIgnoreCase(String header) {
|
||||
|
||||
return streamHeaders().anyMatch(tableCellNode -> tableCellNode.getTextBlock().getSearchText().strip().toLowerCase(Locale.ENGLISH).equals(header.toLowerCase(Locale.ENGLISH)));
|
||||
return streamHeaders().anyMatch(tableCellNode -> tableCellNode.getTextBlock()
|
||||
.getSearchText()
|
||||
.strip()
|
||||
.toLowerCase(Locale.ENGLISH)
|
||||
.equals(header.toLowerCase(Locale.ENGLISH)));
|
||||
}
|
||||
|
||||
|
||||
@@ -289,7 +320,7 @@ public class Table implements SemanticNode {
|
||||
* Finds all entities of the provided type, which appear in the same row that the provided entity appears in.
|
||||
* Ignores Entity with ignored == true or removed == true.
|
||||
*
|
||||
* @param type the type of entities to search for
|
||||
* @param type the type of entities to search for
|
||||
* @param textEntity the entity, which appears in the row to search
|
||||
* @return List of all entities of the provided type, which appear in the same row that the provided entity appears in.
|
||||
*/
|
||||
|
||||
+2
-2
@@ -23,8 +23,10 @@ import lombok.experimental.FieldDefaults;
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
|
||||
public class TableCell implements GenericSemanticNode {
|
||||
|
||||
@EqualsAndHashCode.Include
|
||||
List<Integer> treeId;
|
||||
int row;
|
||||
int col;
|
||||
@@ -36,11 +38,9 @@ public class TableCell implements GenericSemanticNode {
|
||||
|
||||
TextBlock textBlock;
|
||||
|
||||
@EqualsAndHashCode.Exclude
|
||||
DocumentTree documentTree;
|
||||
|
||||
@Builder.Default
|
||||
@EqualsAndHashCode.Exclude
|
||||
Set<TextEntity> entities = new HashSet<>();
|
||||
|
||||
|
||||
|
||||
+18
@@ -3,11 +3,14 @@ package com.iqser.red.service.redaction.v1.server.model.document.textblock;
|
||||
import static java.lang.String.format;
|
||||
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.text.BreakIterator;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -40,6 +43,7 @@ public class AtomicTextBlock implements TextBlock {
|
||||
//string coordinates
|
||||
TextRange textRange;
|
||||
String searchText;
|
||||
List<String> words;
|
||||
List<Integer> lineBreaks;
|
||||
|
||||
//position coordinates
|
||||
@@ -114,6 +118,20 @@ public class AtomicTextBlock implements TextBlock {
|
||||
return new TextRange(lineBreaks.get(lineNumber - 1) + textRange.start(), lineBreaks.get(lineNumber) + textRange.start());
|
||||
}
|
||||
|
||||
public List<String> getWords() {
|
||||
|
||||
if (words == null) {
|
||||
words = new ArrayList<>();
|
||||
BreakIterator iterator = BreakIterator.getWordInstance(Locale.ENGLISH);
|
||||
iterator.setText(searchText);
|
||||
int start = iterator.first();
|
||||
for (int end = iterator.next(); end != BreakIterator.DONE; start = end, end = iterator.next()) {
|
||||
words.add(searchText.substring(start, end));
|
||||
}
|
||||
}
|
||||
return words;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public List<AtomicTextBlock> getAtomicTextBlocks() {
|
||||
|
||||
+20
-3
@@ -7,6 +7,7 @@ import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -70,7 +71,16 @@ public class ConcatenatedTextBlock implements TextBlock {
|
||||
|
||||
private List<AtomicTextBlock> getAllAtomicTextBlocksPartiallyInStringBoundary(TextRange textRange) {
|
||||
|
||||
return atomicTextBlocks.stream().filter(tb -> tb.getTextRange().intersects(textRange)).toList();
|
||||
List<AtomicTextBlock> intersectingAtomicTextBlocks = new LinkedList<>();
|
||||
for (AtomicTextBlock atomicTextBlock : atomicTextBlocks) {
|
||||
if (atomicTextBlock.getTextRange().start() > textRange.end()) {
|
||||
break; // early stop, following TextBlocks will never intersect
|
||||
}
|
||||
if (atomicTextBlock.getTextRange().intersects(textRange)) {
|
||||
intersectingAtomicTextBlocks.add(atomicTextBlock);
|
||||
}
|
||||
}
|
||||
return intersectingAtomicTextBlocks;
|
||||
}
|
||||
|
||||
|
||||
@@ -86,6 +96,13 @@ public class ConcatenatedTextBlock implements TextBlock {
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public List<String> getWords() {
|
||||
|
||||
return atomicTextBlocks.stream().map(AtomicTextBlock::getWords).flatMap(Collection::stream).toList();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int numberOfLines() {
|
||||
|
||||
@@ -114,13 +131,13 @@ public class ConcatenatedTextBlock implements TextBlock {
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public Rectangle2D getPosition(int stringIdx) {
|
||||
|
||||
return getAtomicTextBlockByStringIndex(stringIdx).getPosition(stringIdx);
|
||||
}
|
||||
|
||||
|
||||
public TextRange getLineTextRange(int lineNumber) {
|
||||
|
||||
if (atomicTextBlocks.size() == 1) {
|
||||
@@ -136,10 +153,10 @@ public class ConcatenatedTextBlock implements TextBlock {
|
||||
return new TextRange(textRange.start(), textRange.start());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public List<Rectangle2D> getPositions(TextRange stringTextRange) {
|
||||
|
||||
|
||||
List<AtomicTextBlock> textBlocks = getAllAtomicTextBlocksPartiallyInStringBoundary(stringTextRange);
|
||||
|
||||
if (textBlocks.isEmpty()) {
|
||||
|
||||
+1
@@ -18,6 +18,7 @@ public interface TextBlock extends CharSequence {
|
||||
|
||||
String getSearchText();
|
||||
|
||||
List<String> getWords();
|
||||
|
||||
List<AtomicTextBlock> getAtomicTextBlocks();
|
||||
|
||||
|
||||
+4
-1
@@ -1,5 +1,6 @@
|
||||
package com.iqser.red.service.redaction.v1.server.model.drools;
|
||||
|
||||
import org.drools.drl.ast.descr.AttributeDescr;
|
||||
import org.drools.drl.ast.descr.RuleDescr;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
@@ -18,6 +19,7 @@ public class BasicRule {
|
||||
RuleIdentifier identifier;
|
||||
String name;
|
||||
String code;
|
||||
String agendaGroup;
|
||||
int line;
|
||||
|
||||
|
||||
@@ -26,7 +28,8 @@ public class BasicRule {
|
||||
RuleIdentifier identifier = RuleIdentifier.fromName(rule.getName());
|
||||
String nameWithoutIdentifier = rule.getName().replace(identifier + ":", "");
|
||||
String code = rulesString.substring(rule.getStartCharacter(), rule.getEndCharacter());
|
||||
return new BasicRule(identifier, nameWithoutIdentifier, code, rule.getLine());
|
||||
String agendaGroup = rule.getAttributes().getOrDefault("agenda-group", new AttributeDescr("agenda-group", "DEFAULT")).getValue();
|
||||
return new BasicRule(identifier, nameWithoutIdentifier, code, agendaGroup, rule.getLine());
|
||||
}
|
||||
|
||||
|
||||
|
||||
+8
@@ -1,10 +1,12 @@
|
||||
package com.iqser.red.service.redaction.v1.server.model.drools;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -39,6 +41,12 @@ public final class RuleFileBluePrint {
|
||||
}
|
||||
|
||||
|
||||
public Set<String> getImportSplitByKeyword() {
|
||||
|
||||
return Arrays.stream(imports.replaceAll("\n", "").split("import")).map(String::trim).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
|
||||
public List<BasicRule> findRulesByIdentifier(RuleIdentifier ruleIdentifier) {
|
||||
|
||||
if (Objects.isNull(ruleIdentifier.unit())) {
|
||||
|
||||
-1
@@ -2,7 +2,6 @@ package com.iqser.red.service.redaction.v1.server.model.drools;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
||||
public record RuleType(String name) {
|
||||
|
||||
static Pattern alphaNumeric = Pattern.compile("^[a-zA-Z0-9]+$");
|
||||
|
||||
+1
-3
@@ -1,13 +1,11 @@
|
||||
package com.iqser.red.service.redaction.v1.server.queue;
|
||||
|
||||
import static com.iqser.red.service.redaction.v1.server.queue.MessagingConfiguration.REDACTION_QUEUE;
|
||||
import static com.iqser.red.service.redaction.v1.model.QueueNames.REDACTION_QUEUE;
|
||||
|
||||
import org.springframework.amqp.core.Message;
|
||||
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
|
||||
import org.springframework.amqp.rabbit.annotation.RabbitListener;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
|
||||
+32
-6
@@ -1,6 +1,11 @@
|
||||
package com.iqser.red.service.redaction.v1.server.queue;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import static com.iqser.red.service.redaction.v1.model.QueueNames.MIGRATION_DLQ;
|
||||
import static com.iqser.red.service.redaction.v1.model.QueueNames.MIGRATION_QUEUE;
|
||||
import static com.iqser.red.service.redaction.v1.model.QueueNames.REDACTION_ANALYSIS_RESPONSE_QUEUE;
|
||||
import static com.iqser.red.service.redaction.v1.model.QueueNames.REDACTION_DQL;
|
||||
import static com.iqser.red.service.redaction.v1.model.QueueNames.REDACTION_PRIORITY_QUEUE;
|
||||
import static com.iqser.red.service.redaction.v1.model.QueueNames.REDACTION_QUEUE;
|
||||
|
||||
import org.springframework.amqp.core.Queue;
|
||||
import org.springframework.amqp.core.QueueBuilder;
|
||||
@@ -8,15 +13,12 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class MessagingConfiguration {
|
||||
|
||||
public static final String REDACTION_QUEUE = "redactionQueue";
|
||||
public static final String REDACTION_DQL = "redactionDQL";
|
||||
|
||||
public static final String REDACTION_PRIORITY_QUEUE = "redactionPriorityQueue";
|
||||
|
||||
public static final String X_ERROR_INFO_HEADER = "x-error-message";
|
||||
public static final String X_ERROR_INFO_TIMESTAMP_HEADER = "x-error-message-timestamp";
|
||||
|
||||
@@ -37,6 +39,20 @@ public class MessagingConfiguration {
|
||||
}
|
||||
|
||||
|
||||
@Bean
|
||||
public Queue migrationQueue() {
|
||||
|
||||
return QueueBuilder.durable(MIGRATION_QUEUE).withArgument("x-dead-letter-exchange", "").withArgument("x-dead-letter-routing-key", MIGRATION_DLQ).maxPriority(2).build();
|
||||
}
|
||||
|
||||
|
||||
@Bean
|
||||
public Queue migrationDLQ() {
|
||||
|
||||
return QueueBuilder.durable(MIGRATION_DLQ).build();
|
||||
}
|
||||
|
||||
|
||||
@Bean
|
||||
public Queue redactionQueue() {
|
||||
|
||||
@@ -54,6 +70,16 @@ public class MessagingConfiguration {
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Queue redactionAnalysisResponseQueue() {
|
||||
|
||||
return QueueBuilder.durable(REDACTION_ANALYSIS_RESPONSE_QUEUE)
|
||||
.withArgument("x-dead-letter-exchange", "")
|
||||
.withArgument("x-dead-letter-routing-key", REDACTION_DQL)
|
||||
.maxPriority(2)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@Bean
|
||||
public Queue redactionDeadLetterQueue() {
|
||||
|
||||
+1
-3
@@ -1,13 +1,11 @@
|
||||
package com.iqser.red.service.redaction.v1.server.queue;
|
||||
|
||||
import static com.iqser.red.service.redaction.v1.server.queue.MessagingConfiguration.REDACTION_PRIORITY_QUEUE;
|
||||
import static com.iqser.red.service.redaction.v1.model.QueueNames.REDACTION_PRIORITY_QUEUE;
|
||||
|
||||
import org.springframework.amqp.core.Message;
|
||||
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
|
||||
import org.springframework.amqp.rabbit.annotation.RabbitListener;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
|
||||
+24
-8
@@ -1,8 +1,8 @@
|
||||
package com.iqser.red.service.redaction.v1.server.queue;
|
||||
|
||||
import static com.iqser.red.service.redaction.v1.server.queue.MessagingConfiguration.REDACTION_DQL;
|
||||
import static com.iqser.red.service.redaction.v1.server.queue.MessagingConfiguration.REDACTION_PRIORITY_QUEUE;
|
||||
import static com.iqser.red.service.redaction.v1.server.queue.MessagingConfiguration.REDACTION_QUEUE;
|
||||
import static com.iqser.red.service.redaction.v1.model.QueueNames.REDACTION_DQL;
|
||||
import static com.iqser.red.service.redaction.v1.model.QueueNames.REDACTION_PRIORITY_QUEUE;
|
||||
import static com.iqser.red.service.redaction.v1.model.QueueNames.REDACTION_QUEUE;
|
||||
import static com.iqser.red.service.redaction.v1.server.queue.MessagingConfiguration.X_ERROR_INFO_HEADER;
|
||||
import static com.iqser.red.service.redaction.v1.server.queue.MessagingConfiguration.X_ERROR_INFO_TIMESTAMP_HEADER;
|
||||
import static java.lang.String.format;
|
||||
@@ -23,8 +23,9 @@ import com.iqser.red.service.persistence.service.v1.api.shared.model.AnalyzeResu
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.file.FileErrorInfo;
|
||||
import com.iqser.red.service.redaction.v1.server.client.FileStatusProcessingUpdateClient;
|
||||
import com.iqser.red.service.redaction.v1.server.client.RulesClient;
|
||||
import com.iqser.red.service.redaction.v1.server.utils.exception.DroolsTimeoutException;
|
||||
import com.iqser.red.service.redaction.v1.server.service.AnalyzeService;
|
||||
import com.iqser.red.service.redaction.v1.server.service.UnprocessedChangesService;
|
||||
import com.iqser.red.service.redaction.v1.server.utils.exception.DroolsTimeoutException;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
@@ -39,6 +40,7 @@ public class RedactionMessageReceiver {
|
||||
private final AnalyzeService analyzeService;
|
||||
private final FileStatusProcessingUpdateClient fileStatusProcessingUpdateClient;
|
||||
private final RulesClient rulesClient;
|
||||
private final UnprocessedChangesService unprocessedChangesService;
|
||||
|
||||
|
||||
@SneakyThrows
|
||||
@@ -70,7 +72,8 @@ public class RedactionMessageReceiver {
|
||||
log.info("");
|
||||
log.info("Processing AnalyzeRequest for file: {}, priority: {} ", analyzeRequest.getFileId(), priority);
|
||||
log.info("");
|
||||
AnalyzeResult result;
|
||||
AnalyzeResult result = new AnalyzeResult();
|
||||
boolean shouldRespond = true;
|
||||
|
||||
try {
|
||||
switch (analyzeRequest.getMessageType()) {
|
||||
@@ -98,13 +101,26 @@ public class RedactionMessageReceiver {
|
||||
format("%.2f", result.getDuration() / 1000.0));
|
||||
log.info("----------------------------------------------------------------------------------");
|
||||
break;
|
||||
case SURROUNDING_TEXT_ANALYSIS:
|
||||
log.info("------------------------------Surrounding Text Analysis------------------------------------------");
|
||||
log.info("Starting Surrounding Text Analysis for file {} in dossier {}", analyzeRequest.getFileId(), analyzeRequest.getDossierId());
|
||||
log.debug(analyzeRequest.getManualRedactions().toString());
|
||||
unprocessedChangesService.analyseSurroundingText(analyzeRequest);
|
||||
log.info("Successful Surrounding Text Analysis dossier {} file {} ",
|
||||
analyzeRequest.getDossierId(),
|
||||
analyzeRequest.getFileId());
|
||||
log.info("-------------------------------------------------------------------------------------------------");
|
||||
shouldRespond = false;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown MessageType: " + analyzeRequest.getMessageType());
|
||||
}
|
||||
log.info("");
|
||||
result.setMessageType(analyzeRequest.getMessageType());
|
||||
fileStatusProcessingUpdateClient.analysisSuccessful(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), result);
|
||||
if (shouldRespond) {
|
||||
result.setMessageType(analyzeRequest.getMessageType());
|
||||
fileStatusProcessingUpdateClient.analysisSuccessful(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), result);
|
||||
}
|
||||
} catch (DroolsTimeoutException droolsTimeoutException) {
|
||||
if (!droolsTimeoutException.isReported()) {
|
||||
rulesClient.setRulesTimeoutDetected(analyzeRequest.getDossierTemplateId(), droolsTimeoutException.getRuleFileType());
|
||||
@@ -128,7 +144,7 @@ public class RedactionMessageReceiver {
|
||||
|
||||
@RabbitHandler
|
||||
@RabbitListener(queues = REDACTION_DQL)
|
||||
public void receiveAnalyzeRequestDQL(Message in) throws IOException {
|
||||
public void receiveAnalyzeRequestDLQ(Message in) throws IOException {
|
||||
|
||||
var analyzeRequest = objectMapper.readValue(in.getBody(), AnalyzeRequest.class);
|
||||
log.info("Failed to process analyze request: {}", analyzeRequest);
|
||||
|
||||
+123
-201
@@ -1,14 +1,13 @@
|
||||
package com.iqser.red.service.redaction.v1.server.service;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
@@ -21,7 +20,7 @@ import com.iqser.red.service.persistence.service.v1.api.shared.model.RuleFileTyp
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.componentlog.ComponentLog;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntityLog;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntityLogChanges;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.Comment;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.imported.ImportedRedactions;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.file.FileType;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.legalbasis.LegalBasis;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLog;
|
||||
@@ -32,7 +31,7 @@ import com.iqser.red.service.redaction.v1.server.RedactionServiceSettings;
|
||||
import com.iqser.red.service.redaction.v1.server.client.LegalBasisClient;
|
||||
import com.iqser.red.service.redaction.v1.server.client.model.NerEntitiesModel;
|
||||
import com.iqser.red.service.redaction.v1.server.model.KieWrapper;
|
||||
import com.iqser.red.service.redaction.v1.server.model.ManualEntity;
|
||||
import com.iqser.red.service.redaction.v1.server.model.PrecursorEntity;
|
||||
import com.iqser.red.service.redaction.v1.server.model.NerEntities;
|
||||
import com.iqser.red.service.redaction.v1.server.model.component.Component;
|
||||
import com.iqser.red.service.redaction.v1.server.model.dictionary.Dictionary;
|
||||
@@ -41,15 +40,18 @@ import com.iqser.red.service.redaction.v1.server.model.dictionary.DictionaryVers
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.Document;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.SemanticNode;
|
||||
import com.iqser.red.service.redaction.v1.server.service.document.DocumentGraphMapper;
|
||||
import com.iqser.red.service.redaction.v1.server.service.document.ImportedRedactionEntryService;
|
||||
import com.iqser.red.service.redaction.v1.server.service.document.ManualRedactionEntryService;
|
||||
import com.iqser.red.service.redaction.v1.server.service.document.NerEntitiesAdapter;
|
||||
import com.iqser.red.service.redaction.v1.server.service.document.SectionFinderService;
|
||||
import com.iqser.red.service.redaction.v1.server.service.drools.ComponentDroolsExecutionService;
|
||||
import com.iqser.red.service.redaction.v1.server.service.drools.EntityDroolsExecutionService;
|
||||
import com.iqser.red.service.redaction.v1.server.service.drools.KieContainerCreationService;
|
||||
import com.iqser.red.service.redaction.v1.server.storage.ObservedStorageService;
|
||||
import com.iqser.red.service.redaction.v1.server.storage.RedactionStorageService;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import io.micrometer.observation.annotation.Observed;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
@@ -62,113 +64,123 @@ import lombok.extern.slf4j.Slf4j;
|
||||
@RequiredArgsConstructor
|
||||
public class AnalyzeService {
|
||||
|
||||
private static final String REDACTMANAGER_ANALYZE_PAGEWISE_METRIC_NAME = "redactmanager_analyze.pagewise";
|
||||
|
||||
DictionaryService dictionaryService;
|
||||
EntityDroolsExecutionService entityDroolsExecutionService;
|
||||
ComponentDroolsExecutionService componentDroolsExecutionService;
|
||||
KieContainerCreationService kieContainerCreationService;
|
||||
DictionarySearchService dictionarySearchService;
|
||||
RedactionLogCreatorService redactionLogCreatorService;
|
||||
EntityLogCreatorService entityLogCreatorService;
|
||||
ComponentLogCreatorService componentLogCreatorService;
|
||||
RedactionStorageService redactionStorageService;
|
||||
RedactionChangeLogService redactionChangeLogService;
|
||||
EntityChangeLogService entityChangeLogService;
|
||||
LegalBasisClient legalBasisClient;
|
||||
RedactionServiceSettings redactionServiceSettings;
|
||||
ImportedRedactionService importedRedactionService;
|
||||
NotFoundImportedEntitiesService notFoundImportedEntitiesService;
|
||||
SectionFinderService sectionFinderService;
|
||||
ManualRedactionEntryService manualRedactionEntryService;
|
||||
|
||||
ImportedRedactionEntryService importedRedactionEntryService;
|
||||
ObservedStorageService observedStorageService;
|
||||
FunctionTimerValues redactmanagerAnalyzePagewiseValues;
|
||||
|
||||
|
||||
@Timed("redactmanager_reanalyze")
|
||||
@SneakyThrows
|
||||
@Observed(name = "AnalyzeService", contextualName = "reanalyze")
|
||||
public AnalyzeResult reanalyze(@RequestBody AnalyzeRequest analyzeRequest) {
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
RedactionLog previousRedactionLog = redactionStorageService.getRedactionLog(analyzeRequest.getDossierId(), analyzeRequest.getFileId());
|
||||
EntityLog previousEntityLog = redactionStorageService.getEntityLog(analyzeRequest.getDossierId(), analyzeRequest.getFileId());
|
||||
log.info("Loaded previous entity log for file {} in dossier {}", analyzeRequest.getFileId(), analyzeRequest.getDossierId());
|
||||
ComponentLog previousComponentLog = new ComponentLog();
|
||||
|
||||
Document document = DocumentGraphMapper.toDocumentGraph(redactionStorageService.getDocumentData(analyzeRequest.getDossierId(), analyzeRequest.getFileId()));
|
||||
Document document = DocumentGraphMapper.toDocumentGraph(observedStorageService.getDocumentData(analyzeRequest.getDossierId(), analyzeRequest.getFileId()));
|
||||
log.info("Loaded Document Graph for file {} in dossier {}", analyzeRequest.getFileId(), analyzeRequest.getDossierId());
|
||||
|
||||
ImportedRedactions importedRedactions = redactionStorageService.getImportedRedactions(analyzeRequest.getDossierId(), analyzeRequest.getFileId());
|
||||
log.info("Loaded Imported Redactions for file {} in dossier {}", analyzeRequest.getFileId(), analyzeRequest.getDossierId());
|
||||
|
||||
// not yet ready for reanalysis
|
||||
if (previousEntityLog == null || document == null || document.getNumberOfPages() == 0) {
|
||||
return analyze(analyzeRequest);
|
||||
}
|
||||
|
||||
DictionaryIncrement dictionaryIncrement = dictionaryService.getDictionaryIncrements(analyzeRequest.getDossierTemplateId(),
|
||||
new DictionaryVersion(previousEntityLog.getDictionaryVersion(), previousEntityLog.getDossierDictionaryVersion()),
|
||||
analyzeRequest.getDossierId());
|
||||
new DictionaryVersion(previousEntityLog.getDictionaryVersion(),
|
||||
previousEntityLog.getDossierDictionaryVersion()),
|
||||
analyzeRequest.getDossierId());
|
||||
|
||||
Set<Integer> sectionsToReanalyseIds = getSectionsToReanalyseIds(analyzeRequest, previousEntityLog, document, dictionaryIncrement);
|
||||
Set<Integer> sectionsToReanalyseIds = getSectionsToReanalyseIds(analyzeRequest, previousEntityLog, document, dictionaryIncrement, importedRedactions);
|
||||
List<SemanticNode> sectionsToReAnalyse = getSectionsToReAnalyse(document, sectionsToReanalyseIds);
|
||||
log.info("{} Sections to reanalyze found for file {} in dossier {}", sectionsToReanalyseIds.size(), analyzeRequest.getFileId(), analyzeRequest.getDossierId());
|
||||
|
||||
if (sectionsToReAnalyse.isEmpty()) {
|
||||
|
||||
EntityLogChanges entityLogChanges = entityLogCreatorService.updateVersionsAndReturnChanges(previousEntityLog,
|
||||
dictionaryIncrement.getDictionaryVersion(),
|
||||
analyzeRequest,
|
||||
false);
|
||||
|
||||
return finalizeAnalysis(analyzeRequest,
|
||||
startTime,
|
||||
kieContainerCreationService.getLatestKieContainer(analyzeRequest.getDossierTemplateId(), RuleFileType.COMPONENT),
|
||||
entityLogCreatorService.updateVersionsAndReturnChanges(previousEntityLog,
|
||||
dictionaryIncrement.getDictionaryVersion(),
|
||||
analyzeRequest.getDossierTemplateId(), false), document,
|
||||
previousRedactionLog,
|
||||
document.getNumberOfPages(),
|
||||
dictionaryIncrement.getDictionaryVersion(),
|
||||
true,
|
||||
Collections.emptySet());
|
||||
startTime,
|
||||
kieContainerCreationService.getLatestKieContainer(analyzeRequest.getDossierTemplateId(), RuleFileType.COMPONENT),
|
||||
entityLogChanges,
|
||||
document,
|
||||
document.getNumberOfPages(),
|
||||
dictionaryIncrement.getDictionaryVersion(),
|
||||
true,
|
||||
Collections.emptySet());
|
||||
}
|
||||
|
||||
KieWrapper kieWrapperEntityRules = kieContainerCreationService.getLatestKieContainer(analyzeRequest.getDossierTemplateId(), RuleFileType.ENTITY);
|
||||
log.info("Updated entity rules to version {} for file {} in dossier {}", kieWrapperEntityRules.rulesVersion(), analyzeRequest.getFileId(), analyzeRequest.getDossierId());
|
||||
|
||||
NerEntities nerEntities = getEntityRecognitionEntitiesFilteredBySectionIds(analyzeRequest, document, sectionsToReanalyseIds);
|
||||
log.info("Loaded Ner Entities for file {} in dossier {}", analyzeRequest.getFileId(), analyzeRequest.getDossierId());
|
||||
|
||||
List<ManualEntity> notFoundManualRedactionEntries = manualRedactionEntryService.addManualRedactionEntriesAndReturnNotFoundEntries(analyzeRequest,
|
||||
document,
|
||||
analyzeRequest.getDossierTemplateId());
|
||||
var notFoundManualRedactionEntries = manualRedactionEntryService.addManualRedactionEntriesAndReturnNotFoundEntries(analyzeRequest,
|
||||
document,
|
||||
analyzeRequest.getDossierTemplateId());
|
||||
var notFoundImportedEntries = importedRedactionEntryService.addImportedEntriesAndReturnNotFoundEntries(analyzeRequest, importedRedactions, document);
|
||||
var notFoundManualOrImportedEntries = Stream.of(notFoundManualRedactionEntries, notFoundImportedEntries)
|
||||
.flatMap(Collection::stream)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Dictionary dictionary = dictionaryService.getDeepCopyDictionary(analyzeRequest.getDossierTemplateId(), analyzeRequest.getDossierId());
|
||||
log.info("Updated Dictionaries for file {} in dossier {}", analyzeRequest.getFileId(), analyzeRequest.getDossierId());
|
||||
|
||||
sectionsToReAnalyse.forEach(node -> dictionarySearchService.addDictionaryEntities(dictionary, node));
|
||||
dictionarySearchService.addDictionaryEntities(dictionary, sectionsToReAnalyse);
|
||||
log.info("Finished Dictionary Search for file {} in dossier {}", analyzeRequest.getFileId(), analyzeRequest.getDossierId());
|
||||
|
||||
// we could add the imported redactions similar to the manual redactions here as well for additional processing
|
||||
List<FileAttribute> allFileAttributes = entityDroolsExecutionService.executeRules(kieWrapperEntityRules.container(),
|
||||
document,
|
||||
sectionsToReAnalyse,
|
||||
dictionary,
|
||||
analyzeRequest.getFileAttributes(),
|
||||
analyzeRequest.getManualRedactions(),
|
||||
nerEntities);
|
||||
document,
|
||||
sectionsToReAnalyse,
|
||||
dictionary,
|
||||
analyzeRequest.getFileAttributes(),
|
||||
analyzeRequest.getManualRedactions(),
|
||||
nerEntities);
|
||||
log.info("Finished entity rule execution for file {} in dossier {}", analyzeRequest.getFileId(), analyzeRequest.getDossierId());
|
||||
|
||||
RedactionLog redactionLog = updatePreviousRedactionLog(analyzeRequest, document, notFoundManualRedactionEntries, previousRedactionLog, sectionsToReanalyseIds);
|
||||
EntityLogChanges entityLogChanges = entityLogCreatorService.updatePreviousEntityLog(analyzeRequest,
|
||||
document,
|
||||
notFoundManualRedactionEntries,
|
||||
previousEntityLog,
|
||||
sectionsToReanalyseIds,
|
||||
dictionary.getVersion());
|
||||
document,
|
||||
notFoundManualOrImportedEntries,
|
||||
previousEntityLog,
|
||||
sectionsToReanalyseIds,
|
||||
dictionary.getVersion());
|
||||
|
||||
notFoundImportedEntitiesService.processEntityLog(entityLogChanges.getEntityLog(), analyzeRequest, notFoundImportedEntries);
|
||||
|
||||
return finalizeAnalysis(analyzeRequest,
|
||||
startTime,
|
||||
kieContainerCreationService.getLatestKieContainer(analyzeRequest.getDossierTemplateId(), RuleFileType.COMPONENT), entityLogChanges, document,
|
||||
redactionLog,
|
||||
document.getNumberOfPages(),
|
||||
dictionaryIncrement.getDictionaryVersion(),
|
||||
true,
|
||||
new HashSet<>(allFileAttributes));
|
||||
startTime,
|
||||
kieContainerCreationService.getLatestKieContainer(analyzeRequest.getDossierTemplateId(), RuleFileType.COMPONENT),
|
||||
entityLogChanges,
|
||||
document,
|
||||
document.getNumberOfPages(),
|
||||
dictionaryIncrement.getDictionaryVersion(),
|
||||
true,
|
||||
new HashSet<>(allFileAttributes));
|
||||
}
|
||||
|
||||
|
||||
@Timed("redactmanager_analyze")
|
||||
@Observed(name = "AnalyzeService", contextualName = "analyze")
|
||||
public AnalyzeResult analyze(AnalyzeRequest analyzeRequest) {
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
@@ -179,9 +191,12 @@ public class AnalyzeService {
|
||||
var kieWrapperComponentRules = kieContainerCreationService.getLatestKieContainer(analyzeRequest.getDossierTemplateId(), RuleFileType.COMPONENT);
|
||||
log.info("Updated Rules to Version {} for file {} in dossier {}", kieWrapperEntityRules.rulesVersion(), analyzeRequest.getFileId(), analyzeRequest.getDossierId());
|
||||
|
||||
Document document = DocumentGraphMapper.toDocumentGraph(redactionStorageService.getDocumentData(analyzeRequest.getDossierId(), analyzeRequest.getFileId()));
|
||||
Document document = DocumentGraphMapper.toDocumentGraph(observedStorageService.getDocumentData(analyzeRequest.getDossierId(), analyzeRequest.getFileId()));
|
||||
log.info("Loaded Document Graph for file {} in dossier {}", analyzeRequest.getFileId(), analyzeRequest.getDossierId());
|
||||
|
||||
ImportedRedactions importedRedactions = redactionStorageService.getImportedRedactions(analyzeRequest.getDossierId(), analyzeRequest.getFileId());
|
||||
log.info("Loaded Imported Redactions for file {} in dossier {}", analyzeRequest.getFileId(), analyzeRequest.getDossierId());
|
||||
|
||||
NerEntities nerEntities = getEntityRecognitionEntities(analyzeRequest, document);
|
||||
log.info("Loaded Ner Entities for file {} in dossier {}", analyzeRequest.getFileId(), analyzeRequest.getDossierId());
|
||||
|
||||
@@ -189,73 +204,56 @@ public class AnalyzeService {
|
||||
Dictionary dictionary = dictionaryService.getDeepCopyDictionary(analyzeRequest.getDossierTemplateId(), analyzeRequest.getDossierId());
|
||||
log.info("Updated Dictionaries for file {} in dossier {}", analyzeRequest.getFileId(), analyzeRequest.getDossierId());
|
||||
|
||||
List<ManualEntity> notFoundManualRedactionEntries = manualRedactionEntryService.addManualRedactionEntriesAndReturnNotFoundEntries(analyzeRequest,
|
||||
document,
|
||||
analyzeRequest.getDossierTemplateId());
|
||||
var notFoundManualRedactionEntries = manualRedactionEntryService.addManualRedactionEntriesAndReturnNotFoundEntries(analyzeRequest,
|
||||
document,
|
||||
analyzeRequest.getDossierTemplateId());
|
||||
var notFoundImportedEntries = importedRedactionEntryService.addImportedEntriesAndReturnNotFoundEntries(analyzeRequest, importedRedactions, document);
|
||||
var notFoundManualOrImportedEntries = Stream.of(notFoundManualRedactionEntries, notFoundImportedEntries)
|
||||
.flatMap(Collection::stream)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
dictionarySearchService.addDictionaryEntities(dictionary, document);
|
||||
log.info("Finished Dictionary Search for file {} in dossier {}", analyzeRequest.getFileId(), analyzeRequest.getDossierId());
|
||||
|
||||
// we could add the imported redactions similar to the manual redactions here as well for additional processing
|
||||
List<FileAttribute> allFileAttributes = entityDroolsExecutionService.executeRules(kieWrapperEntityRules.container(),
|
||||
document,
|
||||
dictionary,
|
||||
analyzeRequest.getFileAttributes(),
|
||||
analyzeRequest.getManualRedactions(),
|
||||
nerEntities);
|
||||
document,
|
||||
dictionary,
|
||||
analyzeRequest.getFileAttributes(),
|
||||
analyzeRequest.getManualRedactions(),
|
||||
nerEntities);
|
||||
log.info("Finished entity rule execution for file {} in dossier {}", analyzeRequest.getFileId(), analyzeRequest.getDossierId());
|
||||
|
||||
RedactionLog redactionLog = createRedactionLog(analyzeRequest, document, notFoundManualRedactionEntries, dictionary, kieWrapperEntityRules);
|
||||
EntityLog entityLog = entityLogCreatorService.createInitialEntityLog(analyzeRequest,
|
||||
document,
|
||||
notFoundManualRedactionEntries,
|
||||
dictionary.getVersion(),
|
||||
kieWrapperEntityRules.rulesVersion());
|
||||
document,
|
||||
notFoundManualOrImportedEntries,
|
||||
dictionary.getVersion(),
|
||||
kieWrapperEntityRules.rulesVersion());
|
||||
|
||||
return finalizeAnalysis(analyzeRequest, startTime, kieWrapperComponentRules, new EntityLogChanges(entityLog, false), document,
|
||||
redactionLog,
|
||||
document.getNumberOfPages(),
|
||||
dictionary.getVersion(),
|
||||
false,
|
||||
new HashSet<>(allFileAttributes));
|
||||
notFoundImportedEntitiesService.processEntityLog(entityLog, analyzeRequest, notFoundImportedEntries);
|
||||
|
||||
return finalizeAnalysis(analyzeRequest,
|
||||
startTime,
|
||||
kieWrapperComponentRules,
|
||||
new EntityLogChanges(entityLog, false),
|
||||
document,
|
||||
document.getNumberOfPages(),
|
||||
dictionary.getVersion(),
|
||||
false,
|
||||
new HashSet<>(allFileAttributes));
|
||||
}
|
||||
|
||||
|
||||
@Deprecated(forRemoval = true)
|
||||
private RedactionLog updatePreviousRedactionLog(AnalyzeRequest analyzeRequest,
|
||||
Document document,
|
||||
List<ManualEntity> notFoundManualRedactionEntries,
|
||||
RedactionLog previousRedactionLog,
|
||||
Set<Integer> sectionsToReanalyseIds) {
|
||||
|
||||
List<RedactionLogEntry> newRedactionLogEntries = redactionLogCreatorService.createRedactionLog(document,
|
||||
analyzeRequest.getDossierTemplateId(),
|
||||
notFoundManualRedactionEntries,
|
||||
getComments(analyzeRequest));
|
||||
|
||||
newRedactionLogEntries = newRedactionLogEntries.stream().filter(distinctByKey(RedactionLogEntry::getId)).collect(Collectors.toList());
|
||||
|
||||
var importedRedactionFilteredEntries = importedRedactionService.processImportedRedactions(analyzeRequest.getDossierTemplateId(),
|
||||
analyzeRequest.getDossierId(),
|
||||
analyzeRequest.getFileId(),
|
||||
newRedactionLogEntries,
|
||||
false);
|
||||
|
||||
previousRedactionLog.getRedactionLogEntry()
|
||||
.removeIf(entry -> sectionsToReanalyseIds.contains(entry.getSectionNumber()) && !entry.getType().equals(ImportedRedactionService.IMPORTED_REDACTION_TYPE));
|
||||
previousRedactionLog.getRedactionLogEntry().addAll(importedRedactionFilteredEntries);
|
||||
return previousRedactionLog;
|
||||
}
|
||||
|
||||
|
||||
private AnalyzeResult finalizeAnalysis(AnalyzeRequest analyzeRequest, long startTime, KieWrapper kieWrapperComponentRules, EntityLogChanges entityLogChanges, Document document,
|
||||
RedactionLog redactionLog,
|
||||
private AnalyzeResult finalizeAnalysis(AnalyzeRequest analyzeRequest,
|
||||
long startTime,
|
||||
KieWrapper kieWrapperComponentRules,
|
||||
EntityLogChanges entityLogChanges,
|
||||
Document document,
|
||||
int numberOfPages,
|
||||
DictionaryVersion dictionaryVersion,
|
||||
boolean isReanalysis,
|
||||
Set<FileAttribute> addedFileAttributes) {
|
||||
|
||||
finalizeRedactionLog(analyzeRequest, redactionLog, dictionaryVersion);
|
||||
|
||||
EntityLog entityLog = entityLogChanges.getEntityLog();
|
||||
redactionStorageService.storeObject(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), FileType.ENTITY_LOG, entityLogChanges.getEntityLog());
|
||||
|
||||
@@ -290,7 +288,9 @@ public class AnalyzeService {
|
||||
}
|
||||
|
||||
|
||||
private void computeComponentsWhenRulesArePresent(AnalyzeRequest analyzeRequest, KieWrapper kieWrapperComponentRules, Document document,
|
||||
private void computeComponentsWhenRulesArePresent(AnalyzeRequest analyzeRequest,
|
||||
KieWrapper kieWrapperComponentRules,
|
||||
Document document,
|
||||
Set<FileAttribute> addedFileAttributes,
|
||||
EntityLogChanges entityLogChanges,
|
||||
DictionaryVersion dictionaryVersion) {
|
||||
@@ -299,8 +299,11 @@ public class AnalyzeService {
|
||||
return;
|
||||
}
|
||||
|
||||
List<Component> components = componentDroolsExecutionService.executeRules(kieWrapperComponentRules.container(), entityLogChanges.getEntityLog(), document,
|
||||
addedFileAttributes.stream().toList());
|
||||
List<Component> components = componentDroolsExecutionService.executeRules(kieWrapperComponentRules.container(),
|
||||
entityLogChanges.getEntityLog(),
|
||||
document,
|
||||
addedFileAttributes.stream()
|
||||
.toList());
|
||||
log.info("Finished component rule execution for file {} in dossier {}", analyzeRequest.getFileId(), analyzeRequest.getDossierId());
|
||||
|
||||
ComponentLog componentLog = componentLogCreatorService.buildComponentLog(analyzeRequest.getAnalysisNumber(), components, kieWrapperComponentRules.rulesVersion());
|
||||
@@ -310,34 +313,22 @@ public class AnalyzeService {
|
||||
}
|
||||
|
||||
|
||||
private RedactionLogChanges finalizeRedactionLog(AnalyzeRequest analyzeRequest, RedactionLog redactionLog, DictionaryVersion dictionaryVersion) {
|
||||
// TODO: remove redactionLog related stuff
|
||||
|
||||
RedactionLog previousRedactionLog = redactionStorageService.getRedactionLog(analyzeRequest.getDossierId(), analyzeRequest.getFileId());
|
||||
|
||||
redactionLog.setDictionaryVersion(dictionaryVersion.getDossierTemplateVersion());
|
||||
redactionLog.setDossierDictionaryVersion(dictionaryVersion.getDossierVersion());
|
||||
|
||||
excludeExcludedPages(redactionLog, analyzeRequest.getExcludedPages());
|
||||
RedactionLogChanges redactionLogChange = redactionChangeLogService.computeChanges(previousRedactionLog, redactionLog, analyzeRequest.getAnalysisNumber());
|
||||
|
||||
redactionStorageService.storeObject(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), FileType.REDACTION_LOG, redactionLogChange.getRedactionLog());
|
||||
|
||||
return redactionLogChange;
|
||||
}
|
||||
|
||||
|
||||
private static List<SemanticNode> getSectionsToReAnalyse(Document document, Set<Integer> sectionsToReanalyseIds) {
|
||||
|
||||
return document.streamChildren().filter(section -> sectionsToReanalyseIds.contains(section.getTreeId().get(0))).collect(Collectors.toList());
|
||||
return document.streamChildren()
|
||||
.filter(section -> sectionsToReanalyseIds.contains(section.getTreeId()
|
||||
.get(0)))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
private Set<Integer> getSectionsToReanalyseIds(AnalyzeRequest analyzeRequest, EntityLog entityLog, Document document, DictionaryIncrement dictionaryIncrement) {
|
||||
private Set<Integer> getSectionsToReanalyseIds(AnalyzeRequest analyzeRequest,
|
||||
EntityLog entityLog,
|
||||
Document document,
|
||||
DictionaryIncrement dictionaryIncrement,
|
||||
ImportedRedactions importedRedactions) {
|
||||
|
||||
return analyzeRequest.getSectionsToReanalyse().isEmpty() //
|
||||
? sectionFinderService.findSectionsToReanalyse(dictionaryIncrement, entityLog, document, analyzeRequest) //
|
||||
: analyzeRequest.getSectionsToReanalyse();
|
||||
return sectionFinderService.findSectionsToReanalyse(dictionaryIncrement, entityLog, document, analyzeRequest, importedRedactions);
|
||||
}
|
||||
|
||||
|
||||
@@ -357,9 +348,10 @@ public class AnalyzeService {
|
||||
|
||||
private static NerEntitiesModel filterNerEntitiesModelBySectionIds(Set<Integer> sectionsToReanalyseIds, NerEntitiesModel nerEntitiesModel) {
|
||||
|
||||
return new NerEntitiesModel(nerEntitiesModel.getData().entrySet().stream() //
|
||||
.filter(entry -> sectionsToReanalyseIds.contains(entry.getKey())) //
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));
|
||||
return new NerEntitiesModel(nerEntitiesModel.getData().entrySet()
|
||||
.stream() //
|
||||
.filter(entry -> sectionsToReanalyseIds.contains(entry.getKey())) //
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));
|
||||
}
|
||||
|
||||
|
||||
@@ -374,74 +366,4 @@ public class AnalyzeService {
|
||||
return nerEntities;
|
||||
}
|
||||
|
||||
|
||||
@Deprecated(forRemoval = true)
|
||||
private RedactionLog createRedactionLog(AnalyzeRequest analyzeRequest,
|
||||
Document document,
|
||||
List<ManualEntity> notFoundManualRedactionEntries,
|
||||
Dictionary dictionary,
|
||||
KieWrapper wrapper) {
|
||||
|
||||
List<RedactionLogEntry> redactionLogEntries = redactionLogCreatorService.createRedactionLog(document,
|
||||
analyzeRequest.getDossierTemplateId(),
|
||||
notFoundManualRedactionEntries,
|
||||
getComments(analyzeRequest));
|
||||
|
||||
redactionLogEntries = redactionLogEntries.stream().filter(distinctByKey(RedactionLogEntry::getId)).collect(Collectors.toList());
|
||||
|
||||
List<LegalBasis> legalBasis = legalBasisClient.getLegalBasisMapping(analyzeRequest.getDossierTemplateId());
|
||||
RedactionLog redactionLog = new RedactionLog(redactionServiceSettings.getAnalysisVersion(),
|
||||
analyzeRequest.getAnalysisNumber(),
|
||||
redactionLogEntries,
|
||||
toRedactionLogLegalBasis(legalBasis),
|
||||
dictionary.getVersion().getDossierTemplateVersion(),
|
||||
dictionary.getVersion().getDossierVersion(),
|
||||
wrapper.rulesVersion(),
|
||||
legalBasisClient.getVersion(analyzeRequest.getDossierTemplateId()));
|
||||
|
||||
List<RedactionLogEntry> importedRedactionFilteredEntries = importedRedactionService.processImportedRedactions(analyzeRequest.getDossierTemplateId(),
|
||||
analyzeRequest.getDossierId(),
|
||||
analyzeRequest.getFileId(),
|
||||
redactionLog.getRedactionLogEntry(),
|
||||
true);
|
||||
redactionLog.setRedactionLogEntry(importedRedactionFilteredEntries);
|
||||
return redactionLog;
|
||||
}
|
||||
|
||||
private <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
|
||||
|
||||
Set<Object> seen = ConcurrentHashMap.newKeySet();
|
||||
return t -> seen.add(keyExtractor.apply(t));
|
||||
}
|
||||
|
||||
|
||||
private static Map<String, List<Comment>> getComments(AnalyzeRequest analyzeRequest) {
|
||||
|
||||
if (analyzeRequest.getManualRedactions() == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
if (analyzeRequest.getManualRedactions().getComments() == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
return analyzeRequest.getManualRedactions().getComments();
|
||||
}
|
||||
|
||||
|
||||
public List<RedactionLogLegalBasis> toRedactionLogLegalBasis(List<LegalBasis> legalBasis) {
|
||||
|
||||
return legalBasis.stream().map(l -> new RedactionLogLegalBasis(l.getName(), l.getDescription(), l.getReason())).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
private void excludeExcludedPages(RedactionLog redactionLog, Set<Integer> excludedPages) {
|
||||
|
||||
if (excludedPages != null && !excludedPages.isEmpty()) {
|
||||
redactionLog.getRedactionLogEntry().forEach(entry -> entry.getPositions().forEach(pos -> {
|
||||
if (excludedPages.contains(pos.getPage())) {
|
||||
entry.setExcluded(true);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+18
-5
@@ -1,5 +1,8 @@
|
||||
package com.iqser.red.service.redaction.v1.server.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.Engine;
|
||||
@@ -10,6 +13,7 @@ import com.iqser.red.service.redaction.v1.server.model.dictionary.Dictionary;
|
||||
import com.iqser.red.service.redaction.v1.server.service.document.EntityCreationService;
|
||||
import com.iqser.red.service.redaction.v1.server.service.document.EntityEnrichmentService;
|
||||
|
||||
import io.micrometer.observation.annotation.Observed;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
@@ -24,6 +28,14 @@ public class DictionarySearchService {
|
||||
EntityEnrichmentService entityEnrichmentService;
|
||||
|
||||
|
||||
@Observed(name = "DictionarySearchService", contextualName = "add-dictionary-entries")
|
||||
public void addDictionaryEntities(Dictionary dictionary, List<SemanticNode> semanticNodes) {
|
||||
|
||||
semanticNodes.forEach(node -> addDictionaryEntities(dictionary, node));
|
||||
}
|
||||
|
||||
|
||||
@Observed(name = "DictionarySearchService", contextualName = "add-dictionary-entries")
|
||||
public void addDictionaryEntities(Dictionary dictionary, SemanticNode node) {
|
||||
|
||||
for (var model : dictionary.getDictionaryModels()) {
|
||||
@@ -42,11 +54,12 @@ public class DictionarySearchService {
|
||||
|
||||
EntityCreationService entityCreationService = new EntityCreationService(entityEnrichmentService);
|
||||
searchImplementation.getBoundaries(node.getTextBlock(), node.getTextRange())
|
||||
.stream().filter(boundary -> entityCreationService.isValidEntityTextRange(node.getTextBlock(), boundary))
|
||||
.map(bounds -> entityCreationService.forceByTextRange(bounds, type, entityType, node))
|
||||
.peek(entity -> entity.setDictionaryEntry(true))
|
||||
.peek(entity -> entity.setDossierDictionaryEntry(isDossierDictionaryEntry))
|
||||
.forEach(entity -> entity.addEngine(Engine.DICTIONARY));
|
||||
.stream()
|
||||
.filter(boundary -> entityCreationService.isValidEntityTextRange(node.getTextBlock(), boundary))
|
||||
.forEach(bounds -> entityCreationService.byTextRangeWithEngine(bounds, type, entityType, node, Set.of(Engine.DICTIONARY)).ifPresent(entity -> {
|
||||
entity.setDictionaryEntry(true);
|
||||
entity.setDossierDictionaryEntry(isDossierDictionaryEntry);
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+51
-21
@@ -23,20 +23,21 @@ import com.iqser.red.service.dictionarymerge.commons.DictionaryEntry;
|
||||
import com.iqser.red.service.dictionarymerge.commons.DictionaryEntryModel;
|
||||
import com.iqser.red.service.dictionarymerge.commons.DictionaryMergeService;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.configuration.Colors;
|
||||
import com.iqser.red.service.redaction.v1.server.RedactionServiceSettings;
|
||||
import com.iqser.red.service.redaction.v1.server.client.DictionaryClient;
|
||||
import com.iqser.red.service.redaction.v1.server.model.dictionary.Dictionary;
|
||||
import com.iqser.red.service.redaction.v1.server.model.dictionary.DictionaryIncrement;
|
||||
import com.iqser.red.service.redaction.v1.server.model.dictionary.DictionaryModel;
|
||||
import com.iqser.red.service.redaction.v1.server.model.dictionary.TenantDictionary;
|
||||
import com.iqser.red.service.redaction.v1.server.model.dictionary.DictionaryEntries;
|
||||
import com.iqser.red.service.redaction.v1.server.model.dictionary.DictionaryIncrement;
|
||||
import com.iqser.red.service.redaction.v1.server.model.dictionary.DictionaryIncrementValue;
|
||||
import com.iqser.red.service.redaction.v1.server.model.dictionary.DictionaryModel;
|
||||
import com.iqser.red.service.redaction.v1.server.model.dictionary.DictionaryRepresentation;
|
||||
import com.iqser.red.service.redaction.v1.server.model.dictionary.DictionaryVersion;
|
||||
import com.iqser.red.service.redaction.v1.server.RedactionServiceSettings;
|
||||
import com.iqser.red.service.redaction.v1.server.model.dictionary.TenantDictionary;
|
||||
import com.knecon.fforesight.tenantcommons.TenantContext;
|
||||
|
||||
import feign.FeignException;
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import io.micrometer.observation.annotation.Observed;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
@@ -47,7 +48,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
@RequiredArgsConstructor
|
||||
public class DictionaryService {
|
||||
|
||||
private static final String DEFAULT_COLOR = "#cccccc";
|
||||
public static final String DEFAULT_COLOR = "#cccccc";
|
||||
private final DictionaryClient dictionaryClient;
|
||||
|
||||
private final RedactionServiceSettings settings;
|
||||
@@ -73,6 +74,7 @@ public class DictionaryService {
|
||||
|
||||
|
||||
@SneakyThrows
|
||||
@Observed(name = "DictionaryService", contextualName = "update-dictionary")
|
||||
@Timed("redactmanager_updateDictionary")
|
||||
public DictionaryVersion updateDictionary(String dossierTemplateId, String dossierId) {
|
||||
|
||||
@@ -94,6 +96,7 @@ public class DictionaryService {
|
||||
|
||||
|
||||
@SneakyThrows
|
||||
@Observed(name = "DictionaryService", contextualName = "update-dictionary-increments")
|
||||
@Timed("redactmanager_getDictionaryIncrements")
|
||||
public DictionaryIncrement getDictionaryIncrements(String dossierTemplateId, DictionaryVersion fromVersion, String dossierId) {
|
||||
|
||||
@@ -101,6 +104,7 @@ public class DictionaryService {
|
||||
|
||||
Set<DictionaryIncrementValue> newValues = new HashSet<>();
|
||||
List<DictionaryModel> dictionaryModels = getDossierTemplateDictionary(dossierTemplateId).getDictionary();
|
||||
|
||||
dictionaryModels.forEach(dictionaryModel -> {
|
||||
dictionaryModel.getEntries().forEach(dictionaryEntry -> {
|
||||
if (dictionaryEntry.getVersion() > fromVersion.getDossierTemplateVersion()) {
|
||||
@@ -150,19 +154,25 @@ public class DictionaryService {
|
||||
try {
|
||||
DictionaryRepresentation dictionaryRepresentation = new DictionaryRepresentation();
|
||||
|
||||
var typeResponse = dossierId == null ? dictionaryClient.getAllTypesForDossierTemplate(dossierTemplateId, false) : dictionaryClient.getAllTypesForDossier(dossierId,
|
||||
false);
|
||||
var typeResponse = dossierId == null ? dictionaryClient.getAllTypesForDossierTemplate(dossierTemplateId, true) : dictionaryClient.getAllTypesForDossier(dossierId,
|
||||
true);
|
||||
if (CollectionUtils.isNotEmpty(typeResponse)) {
|
||||
|
||||
List<DictionaryModel> dictionary = typeResponse.stream().map(t -> {
|
||||
|
||||
Optional<DictionaryModel> oldModel;
|
||||
Optional<DictionaryModel> optionalOldModel;
|
||||
if (dossierId == null) {
|
||||
var representation = getDossierTemplateDictionary(dossierTemplateId);
|
||||
oldModel = representation != null ? representation.getDictionary().stream().filter(f -> f.getType().equals(t.getType())).findAny() : Optional.empty();
|
||||
optionalOldModel = representation != null ? representation.getDictionary()
|
||||
.stream()
|
||||
.filter(f -> f.getType().equals(t.getType()))
|
||||
.findAny() : Optional.empty();
|
||||
} else {
|
||||
var representation = getDossierDictionary(dossierId);
|
||||
oldModel = representation != null ? representation.getDictionary().stream().filter(f -> f.getType().equals(t.getType())).findAny() : Optional.empty();
|
||||
optionalOldModel = representation != null ? representation.getDictionary()
|
||||
.stream()
|
||||
.filter(f -> f.getType().equals(t.getType()))
|
||||
.findAny() : Optional.empty();
|
||||
}
|
||||
|
||||
Set<DictionaryEntryModel> entries = new HashSet<>();
|
||||
@@ -175,19 +185,38 @@ public class DictionaryService {
|
||||
var newFalsePositivesValues = newEntries.getFalsePositives().stream().map(DictionaryEntry::getValue).collect(Collectors.toSet());
|
||||
var newFalseRecommendationsValues = newEntries.getFalseRecommendations().stream().map(DictionaryEntry::getValue).collect(Collectors.toSet());
|
||||
|
||||
oldModel.ifPresent(oldDictionaryModel -> {
|
||||
optionalOldModel.ifPresent(oldDictionaryModel -> {
|
||||
|
||||
});
|
||||
// add old entries from existing DictionaryModel
|
||||
oldModel.ifPresent(dictionaryModel -> entries.addAll(dictionaryModel.getEntries().stream().filter(f -> !newValues.contains(f.getValue())).toList()));
|
||||
oldModel.ifPresent(dictionaryModel -> falsePositives.addAll(dictionaryModel.getFalsePositives()
|
||||
.stream()
|
||||
.filter(f -> !newFalsePositivesValues.contains(f.getValue()))
|
||||
.toList()));
|
||||
oldModel.ifPresent(dictionaryModel -> falseRecommendations.addAll(dictionaryModel.getFalseRecommendations()
|
||||
.stream()
|
||||
.filter(f -> !newFalseRecommendationsValues.contains(f.getValue()))
|
||||
.toList()));
|
||||
if (optionalOldModel.isPresent()) {
|
||||
var oldModel = optionalOldModel.get();
|
||||
if (oldModel.isCaseInsensitive() && !t.isCaseInsensitive()) {
|
||||
// add old entries from existing DictionaryModel but exclude lower case representation
|
||||
entries.addAll(oldModel.getEntries().stream().filter(f -> !newValues.stream().map(s -> s.toLowerCase(Locale.ROOT)).toList().contains(f.getValue())).toList());
|
||||
falsePositives.addAll(oldModel.getFalsePositives()
|
||||
.stream()
|
||||
.filter(f -> !newFalsePositivesValues.stream().map(s -> s.toLowerCase(Locale.ROOT)).toList().contains(f.getValue()))
|
||||
.toList());
|
||||
falseRecommendations.addAll(oldModel.getFalseRecommendations()
|
||||
.stream()
|
||||
.filter(f -> !newFalseRecommendationsValues.stream().map(s -> s.toLowerCase(Locale.ROOT)).toList().contains(f.getValue()))
|
||||
.toList());
|
||||
} else if (!oldModel.isCaseInsensitive() && t.isCaseInsensitive()) {
|
||||
// add old entries from existing DictionaryModel but exclude upper case representation
|
||||
entries.addAll(oldModel.getEntries().stream().filter(f -> !newValues.contains(f.getValue().toLowerCase(Locale.ROOT))).toList());
|
||||
falsePositives.addAll(oldModel.getFalsePositives().stream().filter(f -> !newFalsePositivesValues.contains(f.getValue().toLowerCase(Locale.ROOT))).toList());
|
||||
falseRecommendations.addAll(oldModel.getFalseRecommendations()
|
||||
.stream()
|
||||
.filter(f -> !newFalseRecommendationsValues.contains(f.getValue().toLowerCase(Locale.ROOT)))
|
||||
.toList());
|
||||
|
||||
} else {
|
||||
// add old entries from existing DictionaryModel
|
||||
entries.addAll(oldModel.getEntries().stream().filter(f -> !newValues.contains(f.getValue())).toList());
|
||||
falsePositives.addAll(oldModel.getFalsePositives().stream().filter(f -> !newFalsePositivesValues.contains(f.getValue())).toList());
|
||||
falseRecommendations.addAll(oldModel.getFalseRecommendations().stream().filter(f -> !newFalseRecommendationsValues.contains(f.getValue())).toList());
|
||||
}
|
||||
}
|
||||
|
||||
// Add Increments
|
||||
entries.addAll(newEntries.getEntries());
|
||||
@@ -292,6 +321,7 @@ public class DictionaryService {
|
||||
|
||||
@SneakyThrows
|
||||
@Timed("redactmanager_getDeepCopyDictionary")
|
||||
@Observed(name = "DictionaryService", contextualName = "deep-copy-dictionary")
|
||||
public Dictionary getDeepCopyDictionary(String dossierTemplateId, String dossierId) {
|
||||
|
||||
List<DictionaryModel> mergedDictionaries;
|
||||
|
||||
+48
-11
@@ -1,6 +1,7 @@
|
||||
package com.iqser.red.service.redaction.v1.server.service;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
@@ -12,23 +13,34 @@ import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.ChangeType;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntityLogEntry;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntryState;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.ManualChange;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.ManualRedactions;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.IdRemoval;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@FieldDefaults(makeFinal=true, level= AccessLevel.PRIVATE)
|
||||
public class EntityChangeLogService {
|
||||
|
||||
|
||||
@Timed("redactmanager_computeChanges")
|
||||
public boolean computeChanges(List<EntityLogEntry> previousEntityLogEntries, List<EntityLogEntry> newEntityLogEntries, int analysisNumber) {
|
||||
public boolean computeChanges(List<EntityLogEntry> previousEntityLogEntries, List<EntityLogEntry> newEntityLogEntries, ManualRedactions manualRedactions, int analysisNumber) {
|
||||
|
||||
var now = OffsetDateTime.now();
|
||||
if (previousEntityLogEntries.isEmpty()) {
|
||||
newEntityLogEntries.forEach(entry -> entry.getChanges().add(new Change(analysisNumber, ChangeType.ADDED, now)));
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean hasChanges = false;
|
||||
|
||||
var now = OffsetDateTime.now();
|
||||
for (EntityLogEntry entityLogEntry : newEntityLogEntries) {
|
||||
Optional<EntityLogEntry> optionalPreviousEntity = previousEntityLogEntries.stream().filter(entry -> entry.getId().equals(entityLogEntry.getId())).findAny();
|
||||
if (optionalPreviousEntity.isEmpty()) {
|
||||
@@ -36,24 +48,50 @@ public class EntityChangeLogService {
|
||||
entityLogEntry.getChanges().add(new Change(analysisNumber, ChangeType.ADDED, now));
|
||||
continue;
|
||||
}
|
||||
var previousEntity = optionalPreviousEntity.get();
|
||||
|
||||
EntityLogEntry previousEntity = optionalPreviousEntity.get();
|
||||
entityLogEntry.getChanges().addAll(previousEntity.getChanges());
|
||||
if (!previousEntity.getState().equals(entityLogEntry.getState())) {
|
||||
hasChanges = true;
|
||||
ChangeType changeType = calculateChangeType(entityLogEntry.getState(), previousEntity.getState());
|
||||
if (changeType != null) {
|
||||
entityLogEntry.getChanges().add(new Change(analysisNumber, changeType, now));
|
||||
}
|
||||
entityLogEntry.getChanges().add(new Change(analysisNumber, changeType, now));
|
||||
}
|
||||
}
|
||||
Set<String> existingIds = newEntityLogEntries.stream().map(EntityLogEntry::getId).collect(Collectors.toSet());
|
||||
List<EntityLogEntry> removedEntries = previousEntityLogEntries.stream().filter(entry -> !existingIds.contains(entry.getId())).toList();
|
||||
removedEntries.forEach(entry -> entry.getChanges().add(new Change(analysisNumber, ChangeType.REMOVED, now)));
|
||||
removedEntries.forEach(entry -> entry.setState(EntryState.REMOVED));
|
||||
addRemovedEntriesAsRemoved(previousEntityLogEntries, newEntityLogEntries, manualRedactions, analysisNumber, now);
|
||||
return hasChanges;
|
||||
}
|
||||
|
||||
|
||||
private void addRemovedEntriesAsRemoved(List<EntityLogEntry> previousEntityLogEntries,
|
||||
List<EntityLogEntry> newEntityLogEntries,
|
||||
ManualRedactions manualRedactions,
|
||||
int analysisNumber,
|
||||
OffsetDateTime now) {
|
||||
|
||||
Set<String> existingIds = newEntityLogEntries.stream().map(EntityLogEntry::getId).collect(Collectors.toSet());
|
||||
List<EntityLogEntry> removedEntries = previousEntityLogEntries.stream()
|
||||
.filter(entry -> !existingIds.contains(entry.getId()))
|
||||
.toList();
|
||||
removedEntries.forEach(entry -> entry.getChanges().add(new Change(analysisNumber, ChangeType.REMOVED, now)));
|
||||
removedEntries.forEach(entry -> entry.setState(EntryState.REMOVED));
|
||||
removedEntries.forEach(entry -> addManualChangeForDictionaryRemovals(entry, manualRedactions));
|
||||
newEntityLogEntries.addAll(removedEntries);
|
||||
}
|
||||
|
||||
|
||||
private void addManualChangeForDictionaryRemovals(EntityLogEntry entry, ManualRedactions manualRedactions) {
|
||||
|
||||
if (manualRedactions == null || manualRedactions.getIdsToRemove().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
manualRedactions.getIdsToRemove().stream()
|
||||
.filter(IdRemoval::isRemoveFromDictionary)//
|
||||
.filter(removed -> removed.getAnnotationId().equals(entry.getId()))//
|
||||
.findFirst()//
|
||||
.ifPresent(idRemove -> entry.getManualChanges().add(ManualChangeFactory.toManualChange(idRemove, false)));
|
||||
}
|
||||
|
||||
private ChangeType calculateChangeType(EntryState state, EntryState previousState) {
|
||||
|
||||
if (state.equals(previousState)) {
|
||||
@@ -74,5 +112,4 @@ public class EntityChangeLogService {
|
||||
return (state.equals(EntryState.REMOVED) || state.equals(EntryState.IGNORED));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
+103
-112
@@ -1,6 +1,5 @@
|
||||
package com.iqser.red.service.redaction.v1.server.service;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
@@ -12,8 +11,6 @@ import java.util.stream.Collectors;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.AnalyzeRequest;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.Change;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.ChangeType;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntityLog;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntityLogChanges;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntityLogEntry;
|
||||
@@ -21,10 +18,11 @@ import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntryState;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntryType;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.Position;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualRedactionEntry;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.legalbasis.LegalBasis;
|
||||
import com.iqser.red.service.redaction.v1.server.RedactionServiceSettings;
|
||||
import com.iqser.red.service.redaction.v1.server.client.LegalBasisClient;
|
||||
import com.iqser.red.service.redaction.v1.server.model.ManualEntity;
|
||||
import com.iqser.red.service.redaction.v1.server.model.PrecursorEntity;
|
||||
import com.iqser.red.service.redaction.v1.server.model.dictionary.DictionaryVersion;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.entity.EntityType;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.entity.IEntity;
|
||||
@@ -33,21 +31,24 @@ import com.iqser.red.service.redaction.v1.server.model.document.entity.TextEntit
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.Document;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.Image;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.ImageType;
|
||||
import com.iqser.red.service.redaction.v1.server.storage.RedactionStorageService;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
|
||||
public class EntityLogCreatorService {
|
||||
|
||||
private final DictionaryService dictionaryService;
|
||||
private final ManualChangeFactory manualChangeFactory;
|
||||
private final ImportedRedactionService importedRedactionService;
|
||||
private final RedactionServiceSettings redactionServiceSettings;
|
||||
private final LegalBasisClient legalBasisClient;
|
||||
private final EntityChangeLogService entityChangeLogService;
|
||||
DictionaryService dictionaryService;
|
||||
RedactionServiceSettings redactionServiceSettings;
|
||||
LegalBasisClient legalBasisClient;
|
||||
EntityChangeLogService entityChangeLogService;
|
||||
RedactionStorageService redactionStorageService;
|
||||
|
||||
|
||||
private static boolean notFalsePositiveOrFalseRecommendation(TextEntity textEntity) {
|
||||
@@ -58,14 +59,13 @@ public class EntityLogCreatorService {
|
||||
|
||||
public EntityLog createInitialEntityLog(AnalyzeRequest analyzeRequest,
|
||||
Document document,
|
||||
List<ManualEntity> notFoundManualRedactionEntries,
|
||||
List<PrecursorEntity> notFoundEntities,
|
||||
DictionaryVersion dictionaryVersion,
|
||||
long rulesVersion) {
|
||||
|
||||
List<EntityLogEntry> entityLogEntries = createEntityLogEntries(document, analyzeRequest.getDossierTemplateId(), notFoundManualRedactionEntries);
|
||||
List<EntityLogEntry> entityLogEntries = createEntityLogEntries(document, analyzeRequest, notFoundEntities);
|
||||
|
||||
List<LegalBasis> legalBasis = legalBasisClient.getLegalBasisMapping(analyzeRequest.getDossierTemplateId());
|
||||
|
||||
EntityLog entityLog = new EntityLog(redactionServiceSettings.getAnalysisVersion(),
|
||||
analyzeRequest.getAnalysisNumber(),
|
||||
entityLogEntries,
|
||||
@@ -75,27 +75,32 @@ public class EntityLogCreatorService {
|
||||
rulesVersion,
|
||||
legalBasisClient.getVersion(analyzeRequest.getDossierTemplateId()));
|
||||
|
||||
List<EntityLogEntry> importedRedactionFilteredEntries = importedRedactionService.processImportedEntities(analyzeRequest.getDossierTemplateId(),
|
||||
analyzeRequest.getDossierId(),
|
||||
analyzeRequest.getFileId(),
|
||||
entityLog.getEntityLogEntry(),
|
||||
true);
|
||||
List<EntityLogEntry> previousExistingEntityLogEntries = getPreviousEntityLogEntries(analyzeRequest.getDossierId(), analyzeRequest.getFileId());
|
||||
|
||||
entityChangeLogService.computeChanges(previousExistingEntityLogEntries, entityLogEntries, analyzeRequest.getManualRedactions(), analyzeRequest.getAnalysisNumber());
|
||||
|
||||
entityLog.setEntityLogEntry(importedRedactionFilteredEntries);
|
||||
var now = OffsetDateTime.now();
|
||||
entityLogEntries.forEach(entry -> entry.getChanges().add(new Change(analyzeRequest.getAnalysisNumber(), ChangeType.ADDED, now)));
|
||||
excludeExcludedPages(entityLog, analyzeRequest.getExcludedPages());
|
||||
return entityLog;
|
||||
}
|
||||
|
||||
|
||||
public EntityLogChanges updateVersionsAndReturnChanges(EntityLog entityLog, DictionaryVersion dictionaryVersion, String dossierTemplateId, boolean hasChanges) {
|
||||
private List<EntityLogEntry> getPreviousEntityLogEntries(String dossierId, String fileId) {
|
||||
|
||||
List<LegalBasis> legalBasis = legalBasisClient.getLegalBasisMapping(dossierTemplateId);
|
||||
entityLog.setLegalBasisVersion(legalBasisClient.getVersion(dossierTemplateId));
|
||||
if (redactionStorageService.entityLogExists(dossierId, fileId)) {
|
||||
return redactionStorageService.getEntityLog(dossierId, fileId).getEntityLogEntry();
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public EntityLogChanges updateVersionsAndReturnChanges(EntityLog entityLog, DictionaryVersion dictionaryVersion, AnalyzeRequest analyzeRequest, boolean hasChanges) {
|
||||
|
||||
List<LegalBasis> legalBasis = legalBasisClient.getLegalBasisMapping(analyzeRequest.getDossierTemplateId());
|
||||
entityLog.setLegalBasisVersion(legalBasisClient.getVersion(analyzeRequest.getDossierTemplateId()));
|
||||
entityLog.setLegalBasis(toEntityLogLegalBasis(legalBasis));
|
||||
entityLog.setDictionaryVersion(dictionaryVersion.getDossierTemplateVersion());
|
||||
entityLog.setDossierDictionaryVersion(dictionaryVersion.getDossierVersion());
|
||||
entityLog.setAnalysisNumber(analyzeRequest.getAnalysisNumber());
|
||||
|
||||
return new EntityLogChanges(entityLog, hasChanges);
|
||||
}
|
||||
@@ -103,82 +108,81 @@ public class EntityLogCreatorService {
|
||||
|
||||
public EntityLogChanges updatePreviousEntityLog(AnalyzeRequest analyzeRequest,
|
||||
Document document,
|
||||
List<ManualEntity> notFoundManualRedactionEntries,
|
||||
List<PrecursorEntity> notFoundEntries,
|
||||
EntityLog previousEntityLog,
|
||||
Set<Integer> sectionsToReanalyseIds,
|
||||
DictionaryVersion dictionaryVersion) {
|
||||
|
||||
List<EntityLogEntry> newEntityLogEntries = createEntityLogEntries(document, analyzeRequest.getDossierTemplateId(), notFoundManualRedactionEntries);
|
||||
List<EntityLogEntry> newEntityLogEntries = createEntityLogEntries(document, analyzeRequest, notFoundEntries).stream()
|
||||
.filter(entry -> entry.getContainingNodeId().isEmpty() || sectionsToReanalyseIds.contains(entry.getContainingNodeId().get(0)))
|
||||
.collect(Collectors.toList());
|
||||
Set<String> newEntityIds = newEntityLogEntries.stream().map(EntityLogEntry::getId).collect(Collectors.toSet());
|
||||
|
||||
List<EntityLogEntry> previousEntriesFromReAnalyzedSections = previousEntityLog.getEntityLogEntry()
|
||||
.stream()
|
||||
.filter(entry -> (newEntityIds.contains(entry.getId()) || sectionsToReanalyseIds.contains(entry.getContainingNodeId().get(0))) && !entry.getType()
|
||||
.equals(ImportedRedactionService.IMPORTED_REDACTION_TYPE))
|
||||
.filter(entry -> (newEntityIds.contains(entry.getId()) || entry.getContainingNodeId().isEmpty() || sectionsToReanalyseIds.contains(entry.getContainingNodeId()
|
||||
.get(0))))
|
||||
.toList();
|
||||
previousEntityLog.getEntityLogEntry().removeAll(previousEntriesFromReAnalyzedSections);
|
||||
|
||||
boolean hasChanges = entityChangeLogService.computeChanges(previousEntriesFromReAnalyzedSections, newEntityLogEntries, analyzeRequest.getAnalysisNumber());
|
||||
|
||||
var newEntityLogWithImportedEntities = importedRedactionService.processImportedEntities(analyzeRequest.getDossierTemplateId(),
|
||||
analyzeRequest.getDossierId(),
|
||||
analyzeRequest.getFileId(),
|
||||
boolean hasChanges = entityChangeLogService.computeChanges(previousEntriesFromReAnalyzedSections,
|
||||
newEntityLogEntries,
|
||||
false);
|
||||
analyzeRequest.getManualRedactions(),
|
||||
analyzeRequest.getAnalysisNumber());
|
||||
previousEntityLog.getEntityLogEntry().addAll(newEntityLogEntries);
|
||||
|
||||
previousEntityLog.getEntityLogEntry().addAll(newEntityLogWithImportedEntities);
|
||||
|
||||
excludeExcludedPages(previousEntityLog, analyzeRequest.getExcludedPages());
|
||||
|
||||
return updateVersionsAndReturnChanges(previousEntityLog, dictionaryVersion, analyzeRequest.getDossierTemplateId(), hasChanges);
|
||||
return updateVersionsAndReturnChanges(previousEntityLog, dictionaryVersion, analyzeRequest, hasChanges);
|
||||
}
|
||||
|
||||
|
||||
private List<EntityLogEntry> createEntityLogEntries(Document document, String dossierTemplateId, List<ManualEntity> notFoundManualRedactionEntries) {
|
||||
private List<EntityLogEntry> createEntityLogEntries(Document document, AnalyzeRequest analyzeRequest, List<PrecursorEntity> notFoundPrecursorEntries) {
|
||||
|
||||
String dossierTemplateId = analyzeRequest.getDossierTemplateId();
|
||||
|
||||
List<EntityLogEntry> entries = new ArrayList<>();
|
||||
|
||||
document.getEntities()
|
||||
.stream().filter(EntityLogCreatorService::notFalsePositiveOrFalseRecommendation).filter(entity -> !entity.removed())
|
||||
.forEach(entityNode -> entries.addAll(toEntityLogEntries(entityNode, dossierTemplateId)));
|
||||
.stream()
|
||||
.filter(entity -> !entity.getValue().isEmpty())
|
||||
.filter(EntityLogCreatorService::notFalsePositiveOrFalseRecommendation)
|
||||
.filter(entity -> !entity.removed())
|
||||
.forEach(entityNode -> entries.addAll(toEntityLogEntries(entityNode)));
|
||||
document.streamAllImages().filter(entity -> !entity.removed()).forEach(imageNode -> entries.add(createEntityLogEntry(imageNode, dossierTemplateId)));
|
||||
notFoundManualRedactionEntries.stream().filter(entity -> !entity.removed()).forEach(manualEntity -> entries.add(createEntityLogEntry(manualEntity, dossierTemplateId)));
|
||||
notFoundPrecursorEntries.stream().filter(entity -> !entity.removed()).forEach(precursorEntity -> entries.add(createEntityLogEntry(precursorEntity, dossierTemplateId)));
|
||||
return entries;
|
||||
}
|
||||
|
||||
|
||||
private List<EntityLogEntry> toEntityLogEntries(TextEntity textEntity, String dossierTemplateId) {
|
||||
private List<EntityLogEntry> toEntityLogEntries(TextEntity textEntity) {
|
||||
|
||||
Set<String> processedIds = new HashSet<>();
|
||||
List<EntityLogEntry> redactionLogEntities = new ArrayList<>();
|
||||
List<EntityLogEntry> entityLogEntries = new ArrayList<>();
|
||||
|
||||
// split entity into multiple entries if it occurs on multiple pages, since FE can't handle multi page entities
|
||||
for (PositionOnPage positionOnPage : textEntity.getPositionsOnPagePerPage()) {
|
||||
|
||||
// Duplicates should be removed. They might exist due to table extraction duplicating cells spanning multiple columns/rows.
|
||||
if (processedIds.contains(positionOnPage.getId())) {
|
||||
continue;
|
||||
}
|
||||
processedIds.add(positionOnPage.getId());
|
||||
EntityLogEntry entityLogEntry = createEntityLogEntry(textEntity);
|
||||
|
||||
EntityLogEntry redactionLogEntry = createEntityLogEntry(textEntity, dossierTemplateId);
|
||||
redactionLogEntry.setId(positionOnPage.getId());
|
||||
|
||||
List<Position> rectanglesPerLine = positionOnPage.getRectanglePerLine().stream().map(rectangle2D -> new Position(rectangle2D, positionOnPage.getPage().getNumber()))
|
||||
List<Position> rectanglesPerLine = positionOnPage.getRectanglePerLine()
|
||||
.stream()
|
||||
.map(rectangle2D -> new Position(rectangle2D, positionOnPage.getPage().getNumber()))
|
||||
.toList();
|
||||
|
||||
redactionLogEntry.setPositions(rectanglesPerLine);
|
||||
redactionLogEntities.add(redactionLogEntry);
|
||||
// set the ID from the positions, since it might contain a "-" with the page number if the entity is split across multiple pages
|
||||
entityLogEntry.setId(positionOnPage.getId());
|
||||
entityLogEntry.setPositions(rectanglesPerLine);
|
||||
entityLogEntries.add(entityLogEntry);
|
||||
}
|
||||
|
||||
return redactionLogEntities;
|
||||
return entityLogEntries;
|
||||
}
|
||||
|
||||
|
||||
public EntityLogEntry createEntityLogEntry(Image image, String dossierTemplateId) {
|
||||
private EntityLogEntry createEntityLogEntry(Image image, String dossierTemplateId) {
|
||||
|
||||
String imageType = image.getImageType().equals(ImageType.OTHER) ? "image" : image.getImageType().toString().toLowerCase(Locale.ENGLISH);
|
||||
boolean isHint = dictionaryService.isHint(imageType, dossierTemplateId);
|
||||
return EntityLogEntry.builder()
|
||||
.id(image.getId())
|
||||
.value(image.value()).color(getColor(imageType, dossierTemplateId, image.applied(), isHint))
|
||||
.value(image.value())
|
||||
.type(imageType)
|
||||
.reason(image.buildReasonWithManualChangeDescriptions())
|
||||
@@ -190,52 +194,62 @@ public class EntityLogCreatorService {
|
||||
.closestHeadline(image.getHeadline().getTextBlock().getSearchText())
|
||||
.section(image.getManualOverwrite().getSection().orElse(image.getParent().toString()))
|
||||
.imageHasTransparency(image.isTransparent())
|
||||
.manualChanges(manualChangeFactory.toManualChangeList(image.getManualOverwrite().getManualChangeLog(), isHint))
|
||||
.state(buildEntryState(image)).entryType(buildEntryType(image))
|
||||
.manualChanges(ManualChangeFactory.toManualChangeList(image.getManualOverwrite().getManualChangeLog(), isHint))
|
||||
.state(buildEntryState(image))
|
||||
.entryType(isHint ? EntryType.IMAGE_HINT : EntryType.IMAGE)
|
||||
.build();
|
||||
|
||||
}
|
||||
|
||||
|
||||
private EntityLogEntry createEntityLogEntry(ManualEntity manualEntity, String dossierTemplateId) {
|
||||
private EntityLogEntry createEntityLogEntry(PrecursorEntity precursorEntity, String dossierTemplateId) {
|
||||
|
||||
String type = manualEntity.getManualOverwrite().getType().orElse(manualEntity.getType());
|
||||
boolean isHint = isHint(manualEntity.getEntityType());
|
||||
String type = precursorEntity.getManualOverwrite().getType().orElse(precursorEntity.getType());
|
||||
boolean isHint = isHint(precursorEntity.getEntityType());
|
||||
return EntityLogEntry.builder()
|
||||
.id(manualEntity.getId()).color(getColor(type, dossierTemplateId, manualEntity.applied(), isHint))
|
||||
.reason(manualEntity.buildReasonWithManualChangeDescriptions())
|
||||
.legalBasis(manualEntity.legalBasis())
|
||||
.value(manualEntity.value())
|
||||
.id(precursorEntity.getId())
|
||||
.reason(precursorEntity.buildReasonWithManualChangeDescriptions())
|
||||
.legalBasis(precursorEntity.legalBasis())
|
||||
.value(precursorEntity.value())
|
||||
.type(type)
|
||||
.state(buildEntryState(manualEntity)).entryType(buildEntryType(manualEntity))
|
||||
.section(manualEntity.getManualOverwrite().getSection().orElse(manualEntity.getSection()))
|
||||
.state(buildEntryState(precursorEntity))
|
||||
.entryType(buildEntryType(precursorEntity))
|
||||
.section(precursorEntity.getManualOverwrite().getSection().orElse(precursorEntity.getSection()))
|
||||
.containingNodeId(Collections.emptyList())
|
||||
.closestHeadline("")
|
||||
.matchedRule(manualEntity.getMatchedRule().getRuleIdentifier().toString())
|
||||
.dictionaryEntry(manualEntity.isDictionaryEntry())
|
||||
.dossierDictionaryEntry(manualEntity.isDossierDictionaryEntry())
|
||||
.matchedRule(precursorEntity.getMatchedRule().getRuleIdentifier().toString())
|
||||
.dictionaryEntry(precursorEntity.isDictionaryEntry())
|
||||
.dossierDictionaryEntry(precursorEntity.isDossierDictionaryEntry())
|
||||
.textAfter("")
|
||||
.textBefore("")
|
||||
.startOffset(-1)
|
||||
.endOffset(-1)
|
||||
.positions(manualEntity.getEntityPosition().stream().map(entityPosition -> new Position(entityPosition.rectangle2D(), entityPosition.pageNumber())).toList())
|
||||
.engines(Collections.emptySet())
|
||||
.positions(precursorEntity.getManualOverwrite()
|
||||
.getPositions()
|
||||
.orElse(precursorEntity.getEntityPosition())
|
||||
.stream()
|
||||
.map(entityPosition -> new Position(entityPosition.rectangle2D(), entityPosition.pageNumber()))
|
||||
.toList())
|
||||
.engines(precursorEntity.getEngines())
|
||||
//imported is no longer used, frontend should check engines
|
||||
//(was .imported(precursorEntity.getEngines() != null && precursorEntity.getEngines().contains(Engine.IMPORTED)))
|
||||
.imported(false)
|
||||
.reference(Collections.emptySet())
|
||||
.manualChanges(manualChangeFactory.toManualChangeList(manualEntity.getManualOverwrite().getManualChangeLog(), isHint))
|
||||
.manualChanges(ManualChangeFactory.toManualChangeList(precursorEntity.getManualOverwrite().getManualChangeLog(), isHint))
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
private EntityLogEntry createEntityLogEntry(TextEntity entity, String dossierTemplateId) {
|
||||
private EntityLogEntry createEntityLogEntry(TextEntity entity) {
|
||||
|
||||
Set<String> referenceIds = new HashSet<>();
|
||||
entity.references().stream().filter(TextEntity::active).forEach(ref -> ref.getPositionsOnPagePerPage().forEach(pos -> referenceIds.add(pos.getId())));
|
||||
boolean isHint = isHint(entity.getEntityType());
|
||||
return EntityLogEntry.builder().color(getColor(entity.getType(), dossierTemplateId, entity.applied(), isHint))
|
||||
return EntityLogEntry.builder()
|
||||
.reason(entity.buildReasonWithManualChangeDescriptions())
|
||||
.legalBasis(entity.legalBasis())
|
||||
.value(entity.getManualOverwrite().getValue().orElse(entity.getMatchedRule().isWriteValueWithLineBreaks() ? entity.getValueWithLineBreaks() : entity.getValue()))
|
||||
.type(entity.getType())
|
||||
.type(entity.type())
|
||||
.section(entity.getManualOverwrite().getSection().orElse(entity.getDeepestFullyContainingNode().toString()))
|
||||
.containingNodeId(entity.getDeepestFullyContainingNode().getTreeId())
|
||||
.closestHeadline(entity.getDeepestFullyContainingNode().getHeadline().getTextBlock().getSearchText())
|
||||
@@ -247,9 +261,13 @@ public class EntityLogCreatorService {
|
||||
.endOffset(entity.getTextRange().end())
|
||||
.dossierDictionaryEntry(entity.isDossierDictionaryEntry())
|
||||
.engines(entity.getEngines() != null ? entity.getEngines() : Collections.emptySet())
|
||||
//imported is no longer used, frontend should check engines
|
||||
//(was .imported(entity.getEngines() != null && entity.getEngines().contains(Engine.IMPORTED)))
|
||||
.imported(false)
|
||||
.reference(referenceIds)
|
||||
.manualChanges(manualChangeFactory.toManualChangeList(entity.getManualOverwrite().getManualChangeLog(), isHint))
|
||||
.state(buildEntryState(entity)).entryType(buildEntryType(entity))
|
||||
.manualChanges(ManualChangeFactory.toManualChangeList(entity.getManualOverwrite().getManualChangeLog(), isHint))
|
||||
.state(buildEntryState(entity))
|
||||
.entryType(buildEntryType(entity))
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -260,15 +278,6 @@ public class EntityLogCreatorService {
|
||||
}
|
||||
|
||||
|
||||
private float[] getColor(String type, String dossierTemplateId, boolean isApplied, boolean isHint) {
|
||||
|
||||
if (!isApplied && !isHint) {
|
||||
return dictionaryService.getNotRedactedColor(dossierTemplateId);
|
||||
}
|
||||
return dictionaryService.getColor(type, dossierTemplateId);
|
||||
}
|
||||
|
||||
|
||||
private EntryState buildEntryState(IEntity entity) {
|
||||
|
||||
if (entity.applied() && entity.active()) {
|
||||
@@ -287,13 +296,8 @@ public class EntityLogCreatorService {
|
||||
|
||||
if (entity instanceof TextEntity textEntity) {
|
||||
return getEntryType(textEntity.getEntityType());
|
||||
} else if (entity instanceof ManualEntity manualEntity) {
|
||||
if (((ManualEntity) entity).isRectangle()) {
|
||||
return EntryType.AREA;
|
||||
}
|
||||
return getEntryType(manualEntity.getEntityType());
|
||||
} else if (entity instanceof Image) {
|
||||
return EntryType.IMAGE;
|
||||
} else if (entity instanceof PrecursorEntity precursorEntity) {
|
||||
return precursorEntity.getEntryType();
|
||||
}
|
||||
throw new UnsupportedOperationException(String.format("Entity subclass %s is not implemented!", entity.getClass()));
|
||||
}
|
||||
@@ -316,17 +320,4 @@ public class EntityLogCreatorService {
|
||||
return legalBasis.stream().map(l -> new EntityLogLegalBasis(l.getName(), l.getDescription(), l.getReason())).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
private void excludeExcludedPages(EntityLog entityLog, Set<Integer> excludedPages) {
|
||||
|
||||
if (excludedPages != null && !excludedPages.isEmpty()) {
|
||||
entityLog.getEntityLogEntry().forEach(entry -> entry.getPositions().forEach(pos -> {
|
||||
if (excludedPages.contains(pos.getPageNumber())) {
|
||||
entry.setExcluded(true);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-263
@@ -1,263 +0,0 @@
|
||||
package com.iqser.red.service.redaction.v1.server.service;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntityLogEntry;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntryState;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.Position;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Point;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Rectangle;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLogEntry;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.imported.ImportedRedaction;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.imported.ImportedRedactions;
|
||||
import com.iqser.red.service.redaction.v1.server.storage.RedactionStorageService;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ImportedRedactionService {
|
||||
|
||||
public static final String IMPORTED_REDACTION_TYPE = "imported_redaction";
|
||||
|
||||
private final DictionaryService dictionaryService;
|
||||
private final RedactionStorageService redactionStorageService;
|
||||
|
||||
|
||||
@Deprecated(forRemoval = true)
|
||||
@Timed("redactmanager_processImportedRedactions")
|
||||
public List<RedactionLogEntry> processImportedRedactions(String dossierTemplateId,
|
||||
String dossierId,
|
||||
String fileId,
|
||||
List<RedactionLogEntry> redactionLogEntries,
|
||||
boolean addImportedRedactions) {
|
||||
|
||||
var importedRedactions = redactionStorageService.getImportedRedactions(dossierId, fileId);
|
||||
if (importedRedactions == null) {
|
||||
return redactionLogEntries;
|
||||
}
|
||||
|
||||
redactionLogEntries.forEach(redactionLogEntry -> addIntersections(redactionLogEntry, importedRedactions));
|
||||
|
||||
if (addImportedRedactions) {
|
||||
return addImportedRedactionsRedactionLogEntries(dossierTemplateId, redactionLogEntries, importedRedactions);
|
||||
}
|
||||
|
||||
return redactionLogEntries;
|
||||
}
|
||||
|
||||
|
||||
@Timed("redactmanager_processImportedRedactions")
|
||||
public List<EntityLogEntry> processImportedEntities(String dossierTemplateId,
|
||||
String dossierId,
|
||||
String fileId,
|
||||
List<EntityLogEntry> redactionLogEntries,
|
||||
boolean addImportedRedactions) {
|
||||
|
||||
var importedRedactions = redactionStorageService.getImportedRedactions(dossierId, fileId);
|
||||
if (importedRedactions == null) {
|
||||
return redactionLogEntries;
|
||||
}
|
||||
|
||||
redactionLogEntries.forEach(redactionLogEntry -> addIntersections(redactionLogEntry, importedRedactions));
|
||||
|
||||
if (addImportedRedactions) {
|
||||
return addImportedRedactionsEntityLogEntries(dossierTemplateId, redactionLogEntries, importedRedactions);
|
||||
}
|
||||
|
||||
return redactionLogEntries;
|
||||
}
|
||||
|
||||
|
||||
@Deprecated(forRemoval = true)
|
||||
private List<RedactionLogEntry> addImportedRedactionsRedactionLogEntries(String dossierTemplateId,
|
||||
List<RedactionLogEntry> redactionLogEntries,
|
||||
ImportedRedactions importedRedactions) {
|
||||
|
||||
for (var importedRedactionsValues : importedRedactions.getImportedRedactions().values()) {
|
||||
for (var importedRedaction : importedRedactionsValues) {
|
||||
RedactionLogEntry redactionLogEntry = RedactionLogEntry.builder()
|
||||
.id(importedRedaction.getId())
|
||||
.type(IMPORTED_REDACTION_TYPE)
|
||||
.imported(true)
|
||||
.redacted(true)
|
||||
.positions(importedRedaction.getPositions())
|
||||
.color(getColor(IMPORTED_REDACTION_TYPE, dossierTemplateId))
|
||||
.build();
|
||||
|
||||
redactionLogEntries.add(redactionLogEntry);
|
||||
}
|
||||
}
|
||||
|
||||
return redactionLogEntries;
|
||||
}
|
||||
|
||||
|
||||
private List<EntityLogEntry> addImportedRedactionsEntityLogEntries(String dossierTemplateId, List<EntityLogEntry> entityLogEntries, ImportedRedactions importedRedactions) {
|
||||
|
||||
for (var importedRedactionsValues : importedRedactions.getImportedRedactions().values()) {
|
||||
for (var importedRedaction : importedRedactionsValues) {
|
||||
EntityLogEntry redactionLogEntry = EntityLogEntry.builder()
|
||||
.id(importedRedaction.getId())
|
||||
.type(IMPORTED_REDACTION_TYPE)
|
||||
.imported(true)
|
||||
.state(EntryState.APPLIED)
|
||||
.positions(toPositions(importedRedaction.getPositions()))
|
||||
.color(getColor(IMPORTED_REDACTION_TYPE, dossierTemplateId))
|
||||
.build();
|
||||
|
||||
entityLogEntries.add(redactionLogEntry);
|
||||
}
|
||||
}
|
||||
|
||||
return entityLogEntries;
|
||||
}
|
||||
|
||||
|
||||
private List<Position> toPositions(List<Rectangle> positions) {
|
||||
|
||||
return positions.stream().map(this::toPosition).toList();
|
||||
}
|
||||
|
||||
|
||||
private Position toPosition(Rectangle rectangle) {
|
||||
|
||||
return new Position(new float[]{rectangle.getTopLeft().getX(), rectangle.getTopLeft().getY(), rectangle.getWidth(), rectangle.getHeight()}, rectangle.getPage());
|
||||
}
|
||||
|
||||
|
||||
@Deprecated(forRemoval = true)
|
||||
private void addIntersections(RedactionLogEntry redactionLogEntry, ImportedRedactions importedRedactions) {
|
||||
|
||||
for (Rectangle rectangle : redactionLogEntry.getPositions()) {
|
||||
var normalizedRectangle = normalize(rectangle);
|
||||
if (importedRedactions.getImportedRedactions().containsKey(rectangle.getPage())) {
|
||||
var importedRedactionsOnPage = importedRedactions.getImportedRedactions().get(rectangle.getPage());
|
||||
for (ImportedRedaction importedRedaction : importedRedactionsOnPage) {
|
||||
for (Rectangle importedRedactionPosition : importedRedaction.getPositions()) {
|
||||
if (rectOverlap(normalizedRectangle, normalize(importedRedactionPosition))) {
|
||||
if (redactionLogEntry.getImportedRedactionIntersections() == null) {
|
||||
redactionLogEntry.setImportedRedactionIntersections(new HashSet<>());
|
||||
}
|
||||
redactionLogEntry.getImportedRedactionIntersections().add(importedRedaction.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void addIntersections(EntityLogEntry redactionLogEntry, ImportedRedactions importedRedactions) {
|
||||
|
||||
for (Position rectangle : redactionLogEntry.getPositions()) {
|
||||
var normalizedRectangle = normalize(rectangle);
|
||||
if (importedRedactions.getImportedRedactions().containsKey(rectangle.getPageNumber())) {
|
||||
var importedRedactionsOnPage = importedRedactions.getImportedRedactions().get(rectangle.getPageNumber());
|
||||
for (ImportedRedaction importedRedaction : importedRedactionsOnPage) {
|
||||
for (Rectangle importedRedactionPosition : importedRedaction.getPositions()) {
|
||||
if (rectOverlap(normalizedRectangle, normalize(importedRedactionPosition))) {
|
||||
if (redactionLogEntry.getImportedRedactionIntersections() == null) {
|
||||
redactionLogEntry.setImportedRedactionIntersections(new HashSet<>());
|
||||
}
|
||||
redactionLogEntry.getImportedRedactionIntersections().add(importedRedaction.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
boolean valueInRange(float value, float min, float max) {
|
||||
|
||||
return round(value) >= round(min) && round(value) <= round(max);
|
||||
}
|
||||
|
||||
|
||||
boolean rectOverlap(Rectangle a, Rectangle b) {
|
||||
|
||||
boolean xOverlap = valueInRange(a.getTopLeft().getX(), b.getTopLeft().getX(), b.getTopLeft().getX() + b.getWidth()) || valueInRange(b.getTopLeft().getX(),
|
||||
a.getTopLeft().getX(),
|
||||
a.getTopLeft().getX() + a.getWidth());
|
||||
|
||||
boolean yOverlap = valueInRange(a.getTopLeft().getY(), b.getTopLeft().getY(), b.getTopLeft().getY() + b.getHeight()) || valueInRange(b.getTopLeft().getY(),
|
||||
a.getTopLeft().getY(),
|
||||
a.getTopLeft().getY() + a.getHeight());
|
||||
|
||||
return xOverlap && yOverlap;
|
||||
}
|
||||
|
||||
|
||||
@Deprecated(forRemoval = true)
|
||||
private Rectangle normalize(Rectangle rectangle) {
|
||||
|
||||
Rectangle r = new Rectangle();
|
||||
Point p = new Point();
|
||||
|
||||
if (rectangle.getWidth() < 0) {
|
||||
p.setX(rectangle.getTopLeft().getX() + rectangle.getWidth());
|
||||
r.setWidth(Math.abs(rectangle.getWidth()));
|
||||
} else {
|
||||
p.setX(rectangle.getTopLeft().getX());
|
||||
r.setWidth(rectangle.getWidth());
|
||||
}
|
||||
|
||||
if (rectangle.getHeight() < 0) {
|
||||
p.setY(rectangle.getTopLeft().getY() + rectangle.getHeight());
|
||||
r.setHeight(Math.abs(rectangle.getHeight()));
|
||||
} else {
|
||||
p.setY(rectangle.getTopLeft().getY());
|
||||
r.setHeight(rectangle.getHeight());
|
||||
}
|
||||
|
||||
r.setTopLeft(p);
|
||||
r.setPage(rectangle.getPage());
|
||||
return r;
|
||||
}
|
||||
|
||||
|
||||
private Rectangle normalize(Position position) {
|
||||
|
||||
Rectangle r = new Rectangle();
|
||||
Point p = new Point();
|
||||
|
||||
if (position.getRectangle()[2] < 0) {
|
||||
p.setX(position.getRectangle()[0] + position.getRectangle()[2]);
|
||||
r.setWidth(Math.abs(position.getRectangle()[2]));
|
||||
} else {
|
||||
p.setX(position.getRectangle()[0]);
|
||||
r.setWidth(position.getRectangle()[2]);
|
||||
}
|
||||
|
||||
if (position.getRectangle()[3] < 0) {
|
||||
p.setY(position.getRectangle()[1] + position.getRectangle()[3]);
|
||||
r.setHeight(Math.abs(position.getRectangle()[3]));
|
||||
} else {
|
||||
p.setY(position.getRectangle()[1]);
|
||||
r.setHeight(position.getRectangle()[3]);
|
||||
}
|
||||
|
||||
r.setTopLeft(p);
|
||||
r.setPage(position.getPageNumber());
|
||||
return r;
|
||||
}
|
||||
|
||||
|
||||
private float round(float value) {
|
||||
|
||||
double d = Math.pow(10, 0);
|
||||
return (float) (Math.round(value * d) / d);
|
||||
}
|
||||
|
||||
|
||||
private float[] getColor(String type, String dossierTemplateId) {
|
||||
|
||||
return dictionaryService.getColor(type, dossierTemplateId);
|
||||
}
|
||||
|
||||
}
|
||||
+12
-8
@@ -2,9 +2,10 @@ package com.iqser.red.service.redaction.v1.server.service;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.ManualChange;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.ManualRedactionType;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.BaseAnnotation;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.IdRemoval;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualForceRedaction;
|
||||
@@ -12,19 +13,21 @@ import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualRecategorization;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualRedactionEntry;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualResizeRedaction;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.ManualChange;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.ManualRedactionType;
|
||||
|
||||
@Service
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
@UtilityClass
|
||||
public class ManualChangeFactory {
|
||||
|
||||
public List<ManualChange> toManualChangeList(List<BaseAnnotation> manualChanges, boolean isHint) {
|
||||
|
||||
return manualChanges.stream().map(baseAnnotation -> toManualChange(baseAnnotation, isHint)).toList();
|
||||
return manualChanges.stream()
|
||||
.map(baseAnnotation -> toManualChange(baseAnnotation, isHint))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
private ManualChange toManualChange(BaseAnnotation baseAnnotation, boolean isHint) {
|
||||
public ManualChange toManualChange(BaseAnnotation baseAnnotation, boolean isHint) {
|
||||
|
||||
ManualChange manualChange = ManualChange.from(baseAnnotation);
|
||||
if (baseAnnotation instanceof ManualRecategorization imageRecategorization) {
|
||||
@@ -34,7 +37,8 @@ public class ManualChangeFactory {
|
||||
} else if (baseAnnotation instanceof ManualForceRedaction) {
|
||||
manualChange.withManualRedactionType(isHint ? ManualRedactionType.FORCE_HINT : ManualRedactionType.FORCE_REDACT);
|
||||
} else if (baseAnnotation instanceof ManualResizeRedaction manualResizeRedact) {
|
||||
manualChange.withManualRedactionType(ManualRedactionType.RESIZE).withChange("value", manualResizeRedact.getValue());
|
||||
manualChange.withManualRedactionType(manualResizeRedact.getUpdateDictionary() ? ManualRedactionType.RESIZE_IN_DICTIONARY : ManualRedactionType.RESIZE)
|
||||
.withChange("value", manualResizeRedact.getValue());
|
||||
} else if (baseAnnotation instanceof ManualRedactionEntry manualRedactionEntry) {
|
||||
manualChange.withManualRedactionType(manualRedactionEntry.isAddToDictionary() ? ManualRedactionType.ADD_TO_DICTIONARY : ManualRedactionType.ADD_LOCALLY)
|
||||
.withChange("value", manualRedactionEntry.getValue());
|
||||
|
||||
+84
-33
@@ -1,47 +1,70 @@
|
||||
package com.iqser.red.service.redaction.v1.server.service;
|
||||
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.google.common.collect.Sets;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualRecategorization;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualResizeRedaction;
|
||||
import com.iqser.red.service.redaction.v1.server.model.PrecursorEntity;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.entity.IEntity;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.entity.PositionOnPage;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.entity.TextEntity;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.Image;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.ImageType;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.Page;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.SemanticNode;
|
||||
import com.iqser.red.service.redaction.v1.server.service.document.EntityCreationService;
|
||||
import com.iqser.red.service.redaction.v1.server.service.document.EntityFindingUtility;
|
||||
import com.iqser.red.service.redaction.v1.server.utils.RectangleTransformations;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
|
||||
public class ManualChangesApplicationService {
|
||||
|
||||
EntityCreationService entityCreationService;
|
||||
static double MATCH_THRESHOLD = 10;
|
||||
|
||||
public void recategorize(IEntity IEntityToBeReCategorized, ManualRecategorization manualRecategorization) {
|
||||
EntityFindingUtility entityFindingUtility;
|
||||
|
||||
if (IEntityToBeReCategorized instanceof Image image) {
|
||||
|
||||
@Deprecated
|
||||
public void recategorize(IEntity entityToBeReCategorized, ManualRecategorization manualRecategorization) {
|
||||
|
||||
entityToBeReCategorized.getMatchedRuleList().clear();
|
||||
entityToBeReCategorized.getManualOverwrite().addChange(manualRecategorization);
|
||||
|
||||
if (entityToBeReCategorized instanceof Image image) {
|
||||
image.setImageType(ImageType.fromString(manualRecategorization.getType()));
|
||||
return;
|
||||
}
|
||||
// need to create a new entity and copy over all values, since type is part of the primary key for entities and should never be changed!
|
||||
if (IEntityToBeReCategorized instanceof TextEntity textEntity) {
|
||||
TextEntity recategorizedEntity = entityCreationService.copyEntityWithoutRules(textEntity, manualRecategorization.getType(), textEntity.getEntityType(), textEntity.getDeepestFullyContainingNode());
|
||||
recategorizedEntity.setPositionsOnPagePerPage(textEntity.getPositionsOnPagePerPage());
|
||||
recategorizedEntity.getManualOverwrite().addChange(manualRecategorization);
|
||||
textEntity.remove("FINAL.0.0", "removed by manual recategorization");
|
||||
|
||||
if (entityToBeReCategorized instanceof TextEntity textEntity) {
|
||||
textEntity.setType(manualRecategorization.getType());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void resize(TextEntity entityToBeResized, ManualResizeRedaction manualResizeRedaction) {
|
||||
|
||||
resizeEntityAndReinsert(entityToBeResized, manualResizeRedaction);
|
||||
}
|
||||
|
||||
|
||||
@Deprecated
|
||||
public void resizeEntityAndReinsert(TextEntity entityToBeResized, ManualResizeRedaction manualResizeRedaction) {
|
||||
|
||||
PositionOnPage positionOnPageToBeResized = entityToBeResized.getPositionsOnPagePerPage()
|
||||
@@ -50,32 +73,60 @@ public class ManualChangesApplicationService {
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new NoSuchElementException("No redaction position with matching annotation id found!"));
|
||||
|
||||
positionOnPageToBeResized.setRectanglePerLine(manualResizeRedaction.getPositions().stream().map(ManualChangesApplicationService::toRectangle2D).toList());
|
||||
positionOnPageToBeResized.setRectanglePerLine(manualResizeRedaction.getPositions()
|
||||
.stream()
|
||||
.map(ManualChangesApplicationService::toRectangle2D)
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
int newStartOffset;
|
||||
if (manualResizeRedaction.getValue().length() > entityToBeResized.getValue().length()) {
|
||||
newStartOffset = entityToBeResized.getTextRange().start() - manualResizeRedaction.getValue().indexOf(entityToBeResized.getValue());
|
||||
} else {
|
||||
newStartOffset = entityToBeResized.getTextRange().start() + entityToBeResized.getValue().indexOf(manualResizeRedaction.getValue());
|
||||
}
|
||||
|
||||
// need to reinsert the entity, due to the boundary having changed.
|
||||
removeAndUpdateAndReInsertEntity(entityToBeResized, manualResizeRedaction, newStartOffset);
|
||||
entityToBeResized.getManualOverwrite().addChange(manualResizeRedaction);
|
||||
|
||||
SemanticNode node = entityToBeResized.getDeepestFullyContainingNode();
|
||||
PrecursorEntity searchEntity = PrecursorEntity.fromManualResizeRedaction(manualResizeRedaction);
|
||||
// Loop through nodes starting from the deepest fully containing node all the way to the document node
|
||||
while (node != null) {
|
||||
|
||||
Map<String, List<TextEntity>> possibleEntities = entityFindingUtility.findAllPossibleEntitiesAndGroupByValue(node, List.of(searchEntity));
|
||||
Optional<TextEntity> closestEntity = entityFindingUtility.findClosestEntityAndReturnEmptyIfNotFound(searchEntity, possibleEntities, MATCH_THRESHOLD);
|
||||
|
||||
if (closestEntity.isPresent()) {
|
||||
copyValuesFromClosestEntity(entityToBeResized, manualResizeRedaction, closestEntity.get());
|
||||
possibleEntities.values().stream().flatMap(Collection::stream).forEach(TextEntity::removeFromGraph);
|
||||
return;
|
||||
}
|
||||
|
||||
possibleEntities.values().stream().flatMap(Collection::stream).forEach(TextEntity::removeFromGraph);
|
||||
|
||||
if (node.hasParent()) {
|
||||
node = node.getParent();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void removeAndUpdateAndReInsertEntity(TextEntity entityToBeResized, ManualResizeRedaction manualResizeRedaction, int newStartOffset) {
|
||||
private static void copyValuesFromClosestEntity(TextEntity entityToBeResized, ManualResizeRedaction manualResizeRedaction, TextEntity closestEntity) {
|
||||
|
||||
SemanticNode nodeToInsertInto = entityToBeResized.getDeepestFullyContainingNode().getDocumentTree().getRoot().getNode();
|
||||
entityToBeResized.getIntersectingNodes().forEach(node -> node.getEntities().remove(this));
|
||||
entityToBeResized.getPages().forEach(page -> page.getEntities().remove(this));
|
||||
entityToBeResized.setIntersectingNodes(new LinkedList<>());
|
||||
entityToBeResized.setDeepestFullyContainingNode(null);
|
||||
entityToBeResized.setPages(new HashSet<>());
|
||||
entityToBeResized.getTextRange().setStart(newStartOffset);
|
||||
entityToBeResized.getTextRange().setEnd(newStartOffset + manualResizeRedaction.getValue().length());
|
||||
entityCreationService.addEntityToGraph(entityToBeResized, nodeToInsertInto);
|
||||
Set<SemanticNode> currentIntersectingNodes = new HashSet<>(entityToBeResized.getIntersectingNodes());
|
||||
Set<SemanticNode> newIntersectingNodes = new HashSet<>(closestEntity.getIntersectingNodes());
|
||||
|
||||
Sets.difference(currentIntersectingNodes, newIntersectingNodes).forEach(removedNode -> removedNode.getEntities().remove(entityToBeResized));
|
||||
Sets.difference(newIntersectingNodes, currentIntersectingNodes).forEach(addedNode -> addedNode.getEntities().add(entityToBeResized));
|
||||
|
||||
Set<Page> currentIntersectingPages = new HashSet<>(entityToBeResized.getPages());
|
||||
Set<Page> newIntersectingPages = new HashSet<>(closestEntity.getPages());
|
||||
|
||||
Sets.difference(currentIntersectingPages, newIntersectingPages).forEach(removedPage -> removedPage.getEntities().remove(entityToBeResized));
|
||||
Sets.difference(newIntersectingPages, currentIntersectingPages).forEach(addedPage -> addedPage.getEntities().add(entityToBeResized));
|
||||
|
||||
entityToBeResized.setDeepestFullyContainingNode(closestEntity.getDeepestFullyContainingNode());
|
||||
entityToBeResized.setIntersectingNodes(new ArrayList<>(newIntersectingNodes));
|
||||
entityToBeResized.setTextRange(closestEntity.getTextRange());
|
||||
entityToBeResized.setTextAfter(closestEntity.getTextAfter());
|
||||
entityToBeResized.setTextBefore(closestEntity.getTextBefore());
|
||||
entityToBeResized.setDuplicateTextRanges(new ArrayList<>(closestEntity.getDuplicateTextRanges()));
|
||||
entityToBeResized.setValue(closestEntity.getValue());
|
||||
entityToBeResized.setPages(newIntersectingPages);
|
||||
}
|
||||
|
||||
|
||||
@@ -90,9 +141,9 @@ public class ManualChangesApplicationService {
|
||||
}
|
||||
|
||||
|
||||
private static Rectangle2D toRectangle2D(com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.Rectangle rect) {
|
||||
public static Rectangle2D toRectangle2D(com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.Rectangle rect) {
|
||||
|
||||
return new Rectangle2D.Double(rect.getTopLeftX() - rect.getWidth(), rect.getTopLeftY() - rect.getHeight(), rect.getWidth(), rect.getHeight());
|
||||
return new Rectangle2D.Double(rect.getTopLeftX(), rect.getTopLeftY(), rect.getWidth(), rect.getHeight());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
package com.iqser.red.service.redaction.v1.server.service;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.AnalyzeRequest;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.Change;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.ChangeType;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.Engine;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntityLog;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntityLogEntry;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntryState;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.Position;
|
||||
import com.iqser.red.service.redaction.v1.server.model.PrecursorEntity;
|
||||
import com.iqser.red.service.redaction.v1.server.model.RectangleWithPage;
|
||||
import com.iqser.red.service.redaction.v1.server.storage.RedactionStorageService;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class NotFoundImportedEntitiesService {
|
||||
|
||||
public static final String IMPORTED_REDACTION_TYPE = "imported_redaction";
|
||||
|
||||
|
||||
@Timed("redactmanager_processEntityLog")
|
||||
public void processEntityLog(EntityLog entityLog, AnalyzeRequest analyzeRequest, List<PrecursorEntity> notFoundEntities) {
|
||||
// recreate imported redactions with manual changes
|
||||
if (notFoundEntities == null || notFoundEntities.isEmpty()) {
|
||||
log.debug("checkForImportedRedactions - no not found imported entities");
|
||||
return;
|
||||
}
|
||||
|
||||
// remove all intersections and update imported entities
|
||||
calculateIntersectionsForNotFound(notFoundEntities, entityLog, analyzeRequest.getAnalysisNumber());
|
||||
}
|
||||
|
||||
|
||||
private void calculateIntersectionsForNotFound(List<PrecursorEntity> notFoundEntities, EntityLog entityLog, int analysisNumber) {
|
||||
|
||||
if (!notFoundEntities.isEmpty()) {
|
||||
// imported redactions present, intersections must be added with merged imported redactions
|
||||
Map<Integer, List<PrecursorEntity>> importedRedactionsMap = mapImportedRedactionsOnPage(notFoundEntities);
|
||||
entityLog.getEntityLogEntry().stream().filter(entry -> !entry.getEngines().contains(Engine.IMPORTED)).forEach(redactionLogEntry -> {
|
||||
redactionLogEntry.setImportedRedactionIntersections(new HashSet<>());
|
||||
addIntersections(redactionLogEntry, importedRedactionsMap, analysisNumber);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public Map<Integer, List<PrecursorEntity>> mapImportedRedactionsOnPage(List<PrecursorEntity> importedEntities) {
|
||||
|
||||
Map<Integer, List<PrecursorEntity>> importedRedactionsMap = new HashMap<>();
|
||||
Set<Integer> pageNumbers = importedEntities.stream()
|
||||
.map(PrecursorEntity::getEntityPosition)
|
||||
.flatMap(Collection::stream)
|
||||
.map(RectangleWithPage::pageNumber)
|
||||
.collect(Collectors.toSet());
|
||||
pageNumbers.forEach(pageNumber -> importedRedactionsMap.put(pageNumber,
|
||||
importedEntities.stream().filter(i -> pageNumber == i.getEntityPosition().get(0).pageNumber()).collect(Collectors.toList())));
|
||||
return importedRedactionsMap;
|
||||
}
|
||||
|
||||
|
||||
public void addIntersections(EntityLogEntry entityLogEntry, Map<Integer, List<PrecursorEntity>> importedEntitiesMap, int analysisNumber) {
|
||||
|
||||
for (Position rectangle : entityLogEntry.getPositions()) {
|
||||
if (importedEntitiesMap.containsKey(rectangle.getPageNumber())) {
|
||||
var importedEntitiesOnPage = importedEntitiesMap.get(rectangle.getPageNumber());
|
||||
for (PrecursorEntity precursorEntity : importedEntitiesOnPage) {
|
||||
for (RectangleWithPage rectangleWithPage : precursorEntity.getEntityPosition()) {
|
||||
if (rectangle.toRectangle2D().intersects(rectangleWithPage.rectangle2D())) {
|
||||
if (entityLogEntry.getImportedRedactionIntersections() == null) {
|
||||
entityLogEntry.setImportedRedactionIntersections(new HashSet<>());
|
||||
}
|
||||
entityLogEntry.getImportedRedactionIntersections().add(precursorEntity.getId());
|
||||
|
||||
if (entityLogEntry.getState() != EntryState.REMOVED) {
|
||||
entityLogEntry.setState(EntryState.REMOVED);
|
||||
entityLogEntry.getChanges().add(new Change(analysisNumber, ChangeType.REMOVED, OffsetDateTime.now()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
-115
@@ -1,115 +0,0 @@
|
||||
package com.iqser.red.service.redaction.v1.server.service;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Change;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.ChangeType;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLog;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLogChanges;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLogEntry;
|
||||
import com.iqser.red.service.redaction.v1.server.storage.RedactionStorageService;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class RedactionChangeLogService {
|
||||
|
||||
private final RedactionStorageService redactionStorageService;
|
||||
|
||||
|
||||
@Timed("redactmanager_computeChanges")
|
||||
public RedactionLogChanges computeChanges(RedactionLog previousRedactionLog, RedactionLog currentRedactionLog, int analysisNumber) {
|
||||
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
if (previousRedactionLog == null) {
|
||||
currentRedactionLog.getRedactionLogEntry().forEach(entry -> {
|
||||
entry.getChanges().add(new Change(analysisNumber, ChangeType.ADDED, OffsetDateTime.now()));
|
||||
});
|
||||
return new RedactionLogChanges(currentRedactionLog, false);
|
||||
}
|
||||
|
||||
List<RedactionLogEntry> previouslyExistingEntries = previousRedactionLog.getRedactionLogEntry().stream().filter(entry -> !entry.lastChangeIsRemoved()).toList();
|
||||
|
||||
Map<String, RedactionLogEntry> addedEntryIds = getEntriesThatExistInCurrentButNotInPreviousRedactionLog(currentRedactionLog, previouslyExistingEntries);
|
||||
Set<String> removedIds = getEntryIdsThatExistInPreviousButNotInCurrentRedactionLog(currentRedactionLog, previouslyExistingEntries);
|
||||
|
||||
List<RedactionLogEntry> newRedactionLogEntries = previousRedactionLog.getRedactionLogEntry();
|
||||
|
||||
List<RedactionLogEntry> toRemove = new ArrayList<>();
|
||||
newRedactionLogEntries.forEach(entry -> {
|
||||
if (removedIds.contains(entry.getId()) && addedEntryIds.containsKey(entry.getId())) {
|
||||
List<Change> changes = entry.getChanges();
|
||||
changes.add(new Change(analysisNumber, ChangeType.CHANGED, OffsetDateTime.now()));
|
||||
var newEntry = addedEntryIds.get(entry.getId());
|
||||
newEntry.setChanges(changes);
|
||||
addedEntryIds.put(entry.getId(), newEntry);
|
||||
toRemove.add(entry);
|
||||
} else if (removedIds.contains(entry.getId())) {
|
||||
entry.getChanges().add(new Change(analysisNumber, ChangeType.REMOVED, OffsetDateTime.now()));
|
||||
} else if (addedEntryIds.containsKey(entry.getId())) {
|
||||
List<Change> changes = entry.getChanges();
|
||||
changes.add(new Change(analysisNumber, ChangeType.ADDED, OffsetDateTime.now()));
|
||||
var newEntry = addedEntryIds.get(entry.getId());
|
||||
newEntry.setChanges(changes);
|
||||
addedEntryIds.put(entry.getId(), newEntry);
|
||||
toRemove.add(entry);
|
||||
}
|
||||
});
|
||||
|
||||
newRedactionLogEntries.removeAll(toRemove);
|
||||
|
||||
addedEntryIds.forEach((k, v) -> {
|
||||
if (v.getChanges().isEmpty()) {
|
||||
v.getChanges().add(new Change(analysisNumber, ChangeType.ADDED, OffsetDateTime.now()));
|
||||
}
|
||||
newRedactionLogEntries.add(v);
|
||||
});
|
||||
|
||||
currentRedactionLog.setRedactionLogEntry(newRedactionLogEntries);
|
||||
|
||||
log.debug("Change computation took: {}", System.currentTimeMillis() - start);
|
||||
return new RedactionLogChanges(currentRedactionLog, !addedEntryIds.isEmpty() || !removedIds.isEmpty());
|
||||
}
|
||||
|
||||
|
||||
private static Set<String> getEntryIdsThatExistInPreviousButNotInCurrentRedactionLog(RedactionLog currentRedactionLog, List<RedactionLogEntry> previouslyExistingEntries) {
|
||||
|
||||
Set<RedactionLogEntry> removed = new HashSet<>(previouslyExistingEntries);
|
||||
currentRedactionLog.getRedactionLogEntry().forEach(removed::remove);
|
||||
|
||||
Set<String> removedIds = removed.stream().map(RedactionLogEntry::getId).collect(Collectors.toSet());
|
||||
return removedIds;
|
||||
}
|
||||
|
||||
|
||||
private static Map<String, RedactionLogEntry> getEntriesThatExistInCurrentButNotInPreviousRedactionLog(RedactionLog currentRedactionLog,
|
||||
List<RedactionLogEntry> previouslyExistingEntries) {
|
||||
|
||||
Set<RedactionLogEntry> currentExistingEntries = currentRedactionLog.getRedactionLogEntry()
|
||||
.stream()
|
||||
.filter(entry -> entry.getChanges().isEmpty() || !entry.lastChangeIsRemoved())
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
previouslyExistingEntries.forEach(currentExistingEntries::remove);
|
||||
Map<String, RedactionLogEntry> addedIds = new HashMap<>();
|
||||
currentExistingEntries.forEach(entry -> {
|
||||
addedIds.put(entry.getId(), entry);
|
||||
});
|
||||
return addedIds;
|
||||
}
|
||||
|
||||
}
|
||||
-291
@@ -1,291 +0,0 @@
|
||||
package com.iqser.red.service.redaction.v1.server.service;
|
||||
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.Comment;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Engine;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.ManualChange;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.ManualRedactionType;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Point;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Rectangle;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLogComment;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLogEntry;
|
||||
import com.iqser.red.service.redaction.v1.server.model.ManualEntity;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.entity.EntityType;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.entity.IEntity;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.entity.ManualChangeOverwrite;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.entity.PositionOnPage;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.entity.TextEntity;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.Document;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.Image;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.ImageType;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class RedactionLogCreatorService {
|
||||
|
||||
private final DictionaryService dictionaryService;
|
||||
private final ManualChangeFactory manualChangeFactory;
|
||||
|
||||
|
||||
public List<RedactionLogEntry> createRedactionLog(Document document,
|
||||
String dossierTemplateId,
|
||||
List<ManualEntity> notFoundManualRedactionEntries,
|
||||
Map<String, List<Comment>> comments) {
|
||||
|
||||
List<RedactionLogEntry> entries = new ArrayList<>();
|
||||
document.getEntities()
|
||||
.stream().filter(RedactionLogCreatorService::notFalsePositiveOrFalseRecommendation)
|
||||
.filter(IEntity::active)
|
||||
.forEach(entityNode -> entries.addAll(toRedactionLogEntries(entityNode, dossierTemplateId, comments)));
|
||||
document.streamAllImages().filter(image -> !image.removed()).forEach(imageNode -> entries.add(createRedactionLogEntry(imageNode, dossierTemplateId, comments)));
|
||||
notFoundManualRedactionEntries.forEach(entityIdentifier -> entries.add(createRedactionLogEntry(entityIdentifier, dossierTemplateId, comments)));
|
||||
return entries;
|
||||
}
|
||||
|
||||
|
||||
private static boolean notFalsePositiveOrFalseRecommendation(TextEntity textEntity) {
|
||||
|
||||
return !(textEntity.getEntityType() == EntityType.FALSE_POSITIVE || textEntity.getEntityType() == EntityType.FALSE_RECOMMENDATION);
|
||||
}
|
||||
|
||||
|
||||
private List<RedactionLogEntry> toRedactionLogEntries(TextEntity textEntity, String dossierTemplateId, Map<String, List<Comment>> comments) {
|
||||
|
||||
Set<String> processedIds = new HashSet<>();
|
||||
List<RedactionLogEntry> redactionLogEntities = new ArrayList<>();
|
||||
|
||||
for (PositionOnPage positionOnPage : textEntity.getPositionsOnPagePerPage()) {
|
||||
|
||||
// Duplicates should be removed. They might exist due to table extraction duplicating cells spanning multiple columns/rows.
|
||||
if (processedIds.contains(positionOnPage.getId())) {
|
||||
continue;
|
||||
}
|
||||
processedIds.add(positionOnPage.getId());
|
||||
|
||||
RedactionLogEntry redactionLogEntry = createRedactionLogEntry(textEntity, dossierTemplateId);
|
||||
redactionLogEntry.setId(positionOnPage.getId());
|
||||
redactionLogEntry.setComments(buildRedactionLogComments(comments, positionOnPage.getId()));
|
||||
|
||||
List<Rectangle> rectanglesPerLine = positionOnPage.getRectanglePerLine()
|
||||
.stream()
|
||||
.map(rectangle2D -> toRedactionLogRectangle(rectangle2D, positionOnPage.getPage().getNumber()))
|
||||
.toList();
|
||||
|
||||
redactionLogEntry.setPositions(rectanglesPerLine);
|
||||
redactionLogEntities.add(redactionLogEntry);
|
||||
}
|
||||
|
||||
return redactionLogEntities;
|
||||
}
|
||||
|
||||
|
||||
private List<RedactionLogComment> buildRedactionLogComments(Map<String, List<Comment>> commentsPerId, String id) {
|
||||
|
||||
if (!commentsPerId.containsKey(id)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<Comment> comments = commentsPerId.get(id);
|
||||
if (comments == null || comments.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return toRedactionLogComments(comments);
|
||||
}
|
||||
|
||||
|
||||
private List<RedactionLogComment> toRedactionLogComments(List<Comment> comments) {
|
||||
|
||||
return comments.stream().map(this::toRedactionLogComment).toList();
|
||||
}
|
||||
|
||||
|
||||
private RedactionLogComment toRedactionLogComment(Comment comment) {
|
||||
|
||||
return new RedactionLogComment(comment.getId(),
|
||||
comment.getUser(),
|
||||
comment.getText(),
|
||||
comment.getAnnotationId(),
|
||||
comment.getFileId(),
|
||||
comment.getDate(),
|
||||
comment.getSoftDeletedTime());
|
||||
}
|
||||
|
||||
|
||||
private RedactionLogEntry createRedactionLogEntry(TextEntity entity, String dossierTemplateId) {
|
||||
|
||||
Set<String> referenceIds = new HashSet<>();
|
||||
entity.references().stream().filter(TextEntity::active).forEach(ref -> ref.getPositionsOnPagePerPage().forEach(pos -> referenceIds.add(pos.getId())));
|
||||
int sectionNumber = entity.getDeepestFullyContainingNode().getTreeId().isEmpty() ? 0 : entity.getDeepestFullyContainingNode().getTreeId().get(0);
|
||||
boolean isHint = isHint(entity.getEntityType());
|
||||
return RedactionLogEntry.builder().color(getColor(entity.getType(), dossierTemplateId, entity.applied(), isHint))
|
||||
.reason(entity.buildReasonWithManualChangeDescriptions())
|
||||
.legalBasis(entity.legalBasis())
|
||||
.value(entity.getManualOverwrite().getValue().orElse(entity.getMatchedRule().isWriteValueWithLineBreaks() ? entity.getValueWithLineBreaks() : entity.getValue()))
|
||||
.type(entity.getType())
|
||||
.redacted(entity.applied())
|
||||
.isHint(isHint)
|
||||
.isRecommendation(entity.getEntityType().equals(EntityType.RECOMMENDATION))
|
||||
.isFalsePositive(entity.getEntityType().equals(EntityType.FALSE_POSITIVE) || entity.getEntityType().equals(EntityType.FALSE_RECOMMENDATION))
|
||||
.section(entity.getManualOverwrite().getSection().orElse(entity.getDeepestFullyContainingNode().toString()))
|
||||
.sectionNumber(sectionNumber)
|
||||
.matchedRule(entity.getMatchedRule().getRuleIdentifier().toString())
|
||||
.isDictionaryEntry(entity.isDictionaryEntry())
|
||||
.textAfter(entity.getTextAfter())
|
||||
.textBefore(entity.getTextBefore())
|
||||
.startOffset(entity.getTextRange().start())
|
||||
.endOffset(entity.getTextRange().end())
|
||||
.isDossierDictionaryEntry(entity.isDossierDictionaryEntry())
|
||||
.engines(getEngines(entity))
|
||||
.reference(referenceIds)
|
||||
.manualChanges(mapManualChanges(entity.getManualOverwrite(), isHint))
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
private Set<Engine> getEngines(TextEntity entity) {
|
||||
|
||||
return entity.getEngines() != null ? mapToEngines(entity.getEngines()) : Collections.emptySet();
|
||||
}
|
||||
|
||||
|
||||
private Set<Engine> mapToEngines(Set<com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.Engine> engines) {
|
||||
|
||||
return engines.stream().map(engine -> switch (engine) {
|
||||
case NER -> Engine.NER;
|
||||
case RULE -> Engine.RULE;
|
||||
case DICTIONARY -> Engine.DICTIONARY;
|
||||
default -> null;
|
||||
}).filter(Objects::nonNull).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
|
||||
private boolean isHint(EntityType entityType) {
|
||||
|
||||
return entityType.equals(EntityType.HINT);
|
||||
}
|
||||
|
||||
|
||||
private List<ManualChange> mapManualChanges(ManualChangeOverwrite manualEntity, boolean isHint) {
|
||||
|
||||
return manualChangeFactory.toManualChangeList(manualEntity.getManualChangeLog(), isHint).stream().map(this::mapManualChange).toList();
|
||||
}
|
||||
|
||||
|
||||
private ManualChange mapManualChange(com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.ManualChange manualChange) {
|
||||
|
||||
ManualChange manualChange1 = new ManualChange();
|
||||
BeanUtils.copyProperties(manualChange, manualChange1);
|
||||
manualChange1.setManualRedactionType(switch (manualChange.getManualRedactionType()) {
|
||||
case ADD_LOCALLY -> ManualRedactionType.ADD_LOCALLY;
|
||||
case ADD_TO_DICTIONARY -> ManualRedactionType.ADD_TO_DICTIONARY;
|
||||
case REMOVE_LOCALLY -> ManualRedactionType.REMOVE_LOCALLY;
|
||||
case REMOVE_FROM_DICTIONARY -> ManualRedactionType.REMOVE_FROM_DICTIONARY;
|
||||
case FORCE_REDACT -> ManualRedactionType.FORCE_REDACT;
|
||||
case FORCE_HINT -> ManualRedactionType.FORCE_HINT;
|
||||
case RECATEGORIZE -> ManualRedactionType.RECATEGORIZE;
|
||||
case LEGAL_BASIS_CHANGE -> ManualRedactionType.LEGAL_BASIS_CHANGE;
|
||||
case RESIZE -> ManualRedactionType.RESIZE;
|
||||
});
|
||||
manualChange1.setProcessedDate(manualChange.getProcessedDate());
|
||||
manualChange1.setRequestedDate(manualChange.getRequestedDate());
|
||||
manualChange1.setAnnotationStatus(manualChange.getAnnotationStatus());
|
||||
manualChange1.setPropertyChanges(manualChange.getPropertyChanges());
|
||||
return manualChange1;
|
||||
}
|
||||
|
||||
|
||||
private float[] getColor(String type, String dossierTemplateId, boolean isRedaction, boolean isHint) {
|
||||
|
||||
if (!isRedaction && isHint) {
|
||||
return dictionaryService.getNotRedactedColor(dossierTemplateId);
|
||||
}
|
||||
return dictionaryService.getColor(type, dossierTemplateId);
|
||||
}
|
||||
|
||||
|
||||
public RedactionLogEntry createRedactionLogEntry(ManualEntity manualEntity, String dossierTemplateId, Map<String, List<Comment>> comments) {
|
||||
|
||||
String type = manualEntity.getManualOverwrite().getType().orElse(manualEntity.getType());
|
||||
boolean isHint = isHint(manualEntity.getEntityType());
|
||||
return RedactionLogEntry.builder()
|
||||
.id(manualEntity.getId())
|
||||
.color(getColor(type, dossierTemplateId, manualEntity.applied(), isHint))
|
||||
.reason(manualEntity.buildReasonWithManualChangeDescriptions())
|
||||
.legalBasis(manualEntity.legalBasis())
|
||||
.value(manualEntity.value())
|
||||
.type(type)
|
||||
.redacted(manualEntity.applied())
|
||||
.isHint(isHint)
|
||||
.isRecommendation(manualEntity.getEntityType().equals(EntityType.RECOMMENDATION))
|
||||
.isFalsePositive(manualEntity.getEntityType().equals(EntityType.FALSE_POSITIVE) || manualEntity.getEntityType().equals(EntityType.FALSE_RECOMMENDATION))
|
||||
.section(manualEntity.getManualOverwrite().getSection().orElse(manualEntity.getSection()))
|
||||
.sectionNumber(0)
|
||||
.matchedRule(manualEntity.getMatchedRule().getRuleIdentifier().toString())
|
||||
.rectangle(manualEntity.isRectangle())
|
||||
.isDictionaryEntry(manualEntity.isDictionaryEntry())
|
||||
.isDossierDictionaryEntry(manualEntity.isDossierDictionaryEntry())
|
||||
.textAfter("")
|
||||
.textBefore("")
|
||||
.startOffset(-1)
|
||||
.endOffset(-1)
|
||||
.positions(manualEntity.getEntityPosition()
|
||||
.stream()
|
||||
.map(entityPosition -> toRedactionLogRectangle(entityPosition.rectangle2D(), entityPosition.pageNumber()))
|
||||
.collect(Collectors.toList()))
|
||||
.engines(Collections.emptySet())
|
||||
.reference(Collections.emptySet())
|
||||
.manualChanges(mapManualChanges(manualEntity.getManualOverwrite(), isHint))
|
||||
.comments(buildRedactionLogComments(comments, manualEntity.getId()))
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
private Rectangle toRedactionLogRectangle(Rectangle2D rectangle2D, int pageNumber) {
|
||||
|
||||
return new Rectangle(new Point((float) rectangle2D.getMinX(), (float) rectangle2D.getMinY()), (float) rectangle2D.getWidth(), (float) rectangle2D.getHeight(), pageNumber);
|
||||
}
|
||||
|
||||
|
||||
public RedactionLogEntry createRedactionLogEntry(Image image, String dossierTemplateId, Map<String, List<Comment>> comments) {
|
||||
|
||||
String imageType = image.getImageType().equals(ImageType.OTHER) ? "image" : image.getImageType().toString().toLowerCase(Locale.ENGLISH);
|
||||
boolean isHint = dictionaryService.isHint(imageType, dossierTemplateId);
|
||||
return RedactionLogEntry.builder()
|
||||
.id(image.getId()).color(getColor(imageType, dossierTemplateId, image.applied(), isHint))
|
||||
.isImage(true)
|
||||
.value(image.value())
|
||||
.type(imageType)
|
||||
.redacted(image.applied())
|
||||
.reason(image.buildReasonWithManualChangeDescriptions())
|
||||
.legalBasis(image.legalBasis())
|
||||
.matchedRule(image.getMatchedRule().getRuleIdentifier().toString())
|
||||
.isHint(isHint)
|
||||
.isDictionaryEntry(false)
|
||||
.isRecommendation(false)
|
||||
.positions(List.of(toRedactionLogRectangle(image.getPosition(), image.getPage().getNumber())))
|
||||
.sectionNumber(image.getTreeId().get(0))
|
||||
.section(image.getManualOverwrite().getSection().orElse(image.getParent().toString()))
|
||||
.imageHasTransparency(image.isTransparent())
|
||||
.manualChanges(mapManualChanges(image.getManualOverwrite(), isHint))
|
||||
.comments(buildRedactionLogComments(comments, image.getId()))
|
||||
.build();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
package com.iqser.red.service.redaction.v1.server.service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.amqp.rabbit.core.RabbitTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.AnalyzeRequest;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntityLog;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.Position;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.ManualRedactions;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualRedactionEntry;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualResizeRedaction;
|
||||
import com.iqser.red.service.redaction.v1.model.AnalyzeResponse;
|
||||
import com.iqser.red.service.redaction.v1.model.QueueNames;
|
||||
import com.iqser.red.service.redaction.v1.model.UnprocessedManualEntity;
|
||||
import com.iqser.red.service.redaction.v1.server.model.PrecursorEntity;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.entity.TextEntity;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.Document;
|
||||
import com.iqser.red.service.redaction.v1.server.service.document.DocumentGraphMapper;
|
||||
import com.iqser.red.service.redaction.v1.server.service.document.EntityCreationService;
|
||||
import com.iqser.red.service.redaction.v1.server.service.document.EntityEnrichmentService;
|
||||
import com.iqser.red.service.redaction.v1.server.service.document.EntityFindingUtility;
|
||||
import com.iqser.red.service.redaction.v1.server.service.document.EntityFromPrecursorCreationService;
|
||||
import com.iqser.red.service.redaction.v1.server.storage.ObservedStorageService;
|
||||
import com.iqser.red.service.redaction.v1.server.storage.RedactionStorageService;
|
||||
|
||||
import io.micrometer.observation.annotation.Observed;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
public class UnprocessedChangesService {
|
||||
|
||||
private static final double THRESHOLD = 10;
|
||||
|
||||
final RabbitTemplate rabbitTemplate;
|
||||
final ObservedStorageService observedStorageService;
|
||||
final EntityFindingUtility entityFindingUtility;
|
||||
final RedactionStorageService redactionStorageService;
|
||||
final EntityEnrichmentService entityEnrichmentService;
|
||||
final EntityFromPrecursorCreationService entityFromPrecursorCreationService;
|
||||
final DictionaryService dictionaryService;
|
||||
final ManualChangesApplicationService manualChangesApplicationService;
|
||||
|
||||
EntityCreationService entityCreationService;
|
||||
|
||||
|
||||
@PostConstruct
|
||||
public void initEntityCreationService() {
|
||||
|
||||
entityCreationService = new EntityCreationService(entityEnrichmentService);
|
||||
}
|
||||
|
||||
|
||||
@Observed(name = "UnprocessedChangesService", contextualName = "analyse-surrounding-text")
|
||||
public void analyseSurroundingText(AnalyzeRequest analyzeRequest) {
|
||||
|
||||
List<UnprocessedManualEntity> unprocessedManualEntities = new ArrayList<>();
|
||||
|
||||
dictionaryService.updateDictionary(analyzeRequest.getDossierTemplateId(), analyzeRequest.getDossierId());
|
||||
|
||||
EntityLog previousEntityLog = redactionStorageService.getEntityLog(analyzeRequest.getDossierId(), analyzeRequest.getFileId());
|
||||
Document document = DocumentGraphMapper.toDocumentGraph(observedStorageService.getDocumentData(analyzeRequest.getDossierId(), analyzeRequest.getFileId()));
|
||||
|
||||
Set<String> allAnnotationIds = analyzeRequest.getManualRedactions().getEntriesToAdd().stream().map(ManualRedactionEntry::getAnnotationId).collect(Collectors.toSet());
|
||||
Set<String> resizeIds = analyzeRequest.getManualRedactions().getResizeRedactions().stream().map(ManualResizeRedaction::getAnnotationId).collect(Collectors.toSet());
|
||||
allAnnotationIds.addAll(resizeIds);
|
||||
|
||||
List<ManualResizeRedaction> manualResizeRedactions = analyzeRequest.getManualRedactions().getResizeRedactions().stream().toList();
|
||||
List<PrecursorEntity> manualEntitiesToBeResized = previousEntityLog.getEntityLogEntry()
|
||||
.stream()
|
||||
.filter(entityLogEntry -> resizeIds.contains(entityLogEntry.getId()))
|
||||
.toList()
|
||||
.stream()
|
||||
.map(PrecursorEntity::fromEntityLogEntry)
|
||||
.toList();
|
||||
|
||||
if (!manualResizeRedactions.isEmpty()) {
|
||||
processManualResizeRedactions(document, manualEntitiesToBeResized, unprocessedManualEntities, manualResizeRedactions);
|
||||
}
|
||||
|
||||
List<PrecursorEntity> notFoundManualEntities = new ArrayList<>();
|
||||
List<PrecursorEntity> manualEntities = manualEntitiesConverter(analyzeRequest.getManualRedactions(), analyzeRequest.getDossierTemplateId());
|
||||
if (!manualEntities.isEmpty()) {
|
||||
notFoundManualEntities = entityFromPrecursorCreationService.toTextEntity(manualEntities, document);
|
||||
}
|
||||
|
||||
document.getEntities().forEach(textEntity -> {
|
||||
Set<String> processedIds = new HashSet<>();
|
||||
for (var positionsOnPerPage : textEntity.getPositionsOnPagePerPage()) {
|
||||
if (processedIds.contains(positionsOnPerPage.getId())) {
|
||||
continue;
|
||||
}
|
||||
processedIds.add(positionsOnPerPage.getId());
|
||||
List<Position> positions = positionsOnPerPage.getRectanglePerLine()
|
||||
.stream()
|
||||
.map(rectangle2D -> new Position(rectangle2D, positionsOnPerPage.getPage().getNumber()))
|
||||
.collect(Collectors.toList());
|
||||
unprocessedManualEntities.add(UnprocessedManualEntity.builder()
|
||||
.annotationId(allAnnotationIds.stream().filter(textEntity::matchesAnnotationId).findFirst().orElse(""))
|
||||
.textBefore(textEntity.getTextBefore())
|
||||
.textAfter(textEntity.getTextAfter())
|
||||
.section(textEntity.getManualOverwrite().getSection().orElse(textEntity.getDeepestFullyContainingNode().toString()))
|
||||
.positions(positions)
|
||||
.build());
|
||||
}
|
||||
});
|
||||
|
||||
notFoundManualEntities.forEach(manualEntity -> unprocessedManualEntities.add(builDefaultUnprocessedManualEntity(manualEntity)));
|
||||
|
||||
rabbitTemplate.convertAndSend(QueueNames.REDACTION_ANALYSIS_RESPONSE_QUEUE,
|
||||
AnalyzeResponse.builder().fileId(analyzeRequest.getFileId()).unprocessedManualEntities(unprocessedManualEntities).build());
|
||||
}
|
||||
|
||||
|
||||
private void processManualResizeRedactions(Document document,
|
||||
List<PrecursorEntity> manualEntities,
|
||||
List<UnprocessedManualEntity> unprocessedManualEntities,
|
||||
List<ManualResizeRedaction> manualResizeRedactions) {
|
||||
|
||||
Map<String, List<TextEntity>> tempEntities = entityFindingUtility.findAllPossibleEntitiesAndGroupByValue(document, manualEntities);
|
||||
|
||||
for (PrecursorEntity precursorEntity : manualEntities) {
|
||||
|
||||
Optional<TextEntity> optionalTextEntity = entityFindingUtility.findClosestEntityAndReturnEmptyIfNotFound(precursorEntity, tempEntities, THRESHOLD);
|
||||
|
||||
if (optionalTextEntity.isEmpty()) {
|
||||
unprocessedManualEntities.add(builDefaultUnprocessedManualEntity(precursorEntity));
|
||||
continue;
|
||||
}
|
||||
|
||||
TextEntity correctEntity = createCorrectEntity(precursorEntity, optionalTextEntity.get());
|
||||
Optional<ManualResizeRedaction> optionalManualResizeRedaction = manualResizeRedactions.stream()
|
||||
.filter(manualResizeRedaction -> manualResizeRedaction.getAnnotationId().equals(precursorEntity.getId()))
|
||||
.findFirst();
|
||||
if (optionalManualResizeRedaction.isPresent()) {
|
||||
ManualResizeRedaction manualResizeRedaction = optionalManualResizeRedaction.get();
|
||||
manualChangesApplicationService.resizeEntityAndReinsert(correctEntity, manualResizeRedaction);
|
||||
|
||||
// If the entity's value is not the same as the manual resize request's value it means we didn't find it anywhere and we want to remove it
|
||||
// from the graph, so it does not get processed and sent back to persistence-service to update its value.
|
||||
if (!correctEntity.getValue().equals(manualResizeRedaction.getValue())) {
|
||||
correctEntity.removeFromGraph();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remove all temp entities from the graph
|
||||
tempEntities.values().stream().flatMap(Collection::stream).forEach(TextEntity::removeFromGraph);
|
||||
}
|
||||
|
||||
|
||||
private TextEntity createCorrectEntity(PrecursorEntity precursorEntity, TextEntity closestEntity) {
|
||||
|
||||
TextEntity correctEntity = TextEntity.initialEntityNode(closestEntity.getTextRange(), precursorEntity.type(), precursorEntity.getEntityType(), precursorEntity.getId());
|
||||
|
||||
correctEntity.setDeepestFullyContainingNode(closestEntity.getDeepestFullyContainingNode());
|
||||
correctEntity.setIntersectingNodes(new ArrayList<>(closestEntity.getIntersectingNodes()));
|
||||
correctEntity.setDuplicateTextRanges(new ArrayList<>(closestEntity.getDuplicateTextRanges()));
|
||||
correctEntity.setPages(new HashSet<>(closestEntity.getPages()));
|
||||
|
||||
correctEntity.setValue(closestEntity.getValue());
|
||||
correctEntity.setTextAfter(closestEntity.getTextAfter());
|
||||
correctEntity.setTextBefore(closestEntity.getTextBefore());
|
||||
|
||||
correctEntity.getIntersectingNodes().forEach(n -> n.getEntities().add(correctEntity));
|
||||
correctEntity.getPages().forEach(page -> page.getEntities().add(correctEntity));
|
||||
|
||||
correctEntity.addMatchedRules(precursorEntity.getMatchedRuleList());
|
||||
correctEntity.setDictionaryEntry(precursorEntity.isDictionaryEntry());
|
||||
correctEntity.setDossierDictionaryEntry(precursorEntity.isDossierDictionaryEntry());
|
||||
correctEntity.getManualOverwrite().addChanges(precursorEntity.getManualOverwrite().getManualChangeLog());
|
||||
|
||||
return correctEntity;
|
||||
}
|
||||
|
||||
|
||||
private UnprocessedManualEntity builDefaultUnprocessedManualEntity(PrecursorEntity precursorEntity) {
|
||||
|
||||
return UnprocessedManualEntity.builder()
|
||||
.annotationId(precursorEntity.getId())
|
||||
.textAfter("")
|
||||
.textBefore("")
|
||||
.section("")
|
||||
.positions(precursorEntity.getManualOverwrite()
|
||||
.getPositions()
|
||||
.orElse(precursorEntity.getEntityPosition())
|
||||
.stream()
|
||||
.map(entityPosition -> new Position(entityPosition.rectangle2D(), entityPosition.pageNumber()))
|
||||
.toList())
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
private List<PrecursorEntity> manualEntitiesConverter(ManualRedactions manualRedactions, String dossierTemplateId) {
|
||||
|
||||
return manualRedactions.getEntriesToAdd()
|
||||
.stream()
|
||||
.filter(manualRedactionEntry -> manualRedactionEntry.getPositions() != null && !manualRedactionEntry.getPositions().isEmpty())
|
||||
.map(manualRedactionEntry -> PrecursorEntity.fromManualRedactionEntry(manualRedactionEntry,
|
||||
dictionaryService.isHint(manualRedactionEntry.getType(), dossierTemplateId)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
}
|
||||
+52
-9
@@ -19,6 +19,7 @@ import org.kie.api.runtime.KieSession;
|
||||
|
||||
import com.iqser.red.service.redaction.v1.server.model.component.Component;
|
||||
import com.iqser.red.service.redaction.v1.server.model.component.Entity;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.Paragraph;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.SemanticNode;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.Table;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.TableCell;
|
||||
@@ -102,7 +103,11 @@ public class ComponentCreationService {
|
||||
|
||||
referencedEntities.addAll(references);
|
||||
|
||||
kieSession.insert(Component.builder().matchedRule(RuleIdentifier.fromString(ruleIdentifier)).name(name).value(value).valueDescription(valueDescription)
|
||||
kieSession.insert(Component.builder()
|
||||
.matchedRule(RuleIdentifier.fromString(ruleIdentifier))
|
||||
.name(name)
|
||||
.value(value)
|
||||
.valueDescription(valueDescription)
|
||||
.references(new LinkedList<>(references))
|
||||
.build());
|
||||
}
|
||||
@@ -293,6 +298,30 @@ public class ComponentCreationService {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Counts the distinct rows where any of the provided entities occur and creates a component for each distinct table with its respective row count.
|
||||
*
|
||||
* @param ruleIdentifier the identifier of the rule
|
||||
* @param name the name of the record
|
||||
* @param entities the collection of entities to count row values from
|
||||
*/
|
||||
public void rowValueCount(String ruleIdentifier, String name, Collection<Entity> entities) {
|
||||
|
||||
entities.stream().collect(Collectors.groupingBy(this::getFirstTable)).forEach((optionalTable, groupedEntities) -> {
|
||||
|
||||
if (optionalTable.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
long count = groupedEntities.stream()
|
||||
.collect(Collectors.groupingBy(entity -> getFirstTableCell(entity).map(TableCell::getRow).orElse(-1)))
|
||||
.size();
|
||||
|
||||
create(ruleIdentifier, name, String.valueOf(count), "Count rows with values in the entity references in same table", entities);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a component for each sentence in the collection of entities.
|
||||
*
|
||||
@@ -354,7 +383,9 @@ public class ComponentCreationService {
|
||||
*/
|
||||
public void createComponentsForUnMappedEntities(String ruleIdentifier, Collection<Entity> entities) {
|
||||
|
||||
entities.stream().filter(entity -> !referencedEntities.contains(entity)).sorted(EntityComparators.first())
|
||||
entities.stream()
|
||||
.filter(entity -> !referencedEntities.contains(entity))
|
||||
.sorted(EntityComparators.first())
|
||||
.forEach(entity -> create(ruleIdentifier, entity.getType(), entity.getValue(), "Unmapped Entity", List.of(entity)));
|
||||
}
|
||||
|
||||
@@ -406,7 +437,7 @@ public class ComponentCreationService {
|
||||
|
||||
|
||||
/**
|
||||
* Joins values from entities that are in the same table row. If entities are not in a table cell they are added as a single component.
|
||||
* Joins values from entities that are in the same table row. They are sorted from left to right. If entities are not in a table cell they are added as a single component.
|
||||
*
|
||||
* @param ruleIdentifier the identifier of the rule
|
||||
* @param name the name of the entity
|
||||
@@ -421,10 +452,6 @@ public class ComponentCreationService {
|
||||
groupedEntities.forEach(entity -> create(ruleIdentifier, name, entity.getValue(), valueDescription, entity));
|
||||
}
|
||||
|
||||
groupedEntities.stream()
|
||||
.filter(entity -> !(entity.getContainingNode() instanceof TableCell))
|
||||
.forEach(entity -> create(ruleIdentifier, name, entity.getValue(), valueDescription, entity));
|
||||
|
||||
groupedEntities.stream()
|
||||
.filter(entity -> entity.getContainingNode() instanceof TableCell)
|
||||
.collect(Collectors.groupingBy(entity -> ((TableCell) entity.getContainingNode()).getRow()))
|
||||
@@ -434,7 +461,7 @@ public class ComponentCreationService {
|
||||
.map(Map.Entry::getValue)
|
||||
.forEach(entitiesInSameRow -> create(ruleIdentifier,
|
||||
name,
|
||||
entitiesInSameRow.stream().sorted(Comparator.comparing(Entity::getType).reversed()).map(Entity::getValue).collect(Collectors.joining(", ")),
|
||||
entitiesInSameRow.stream().sorted(EntityComparators.first()).map(Entity::getValue).collect(Collectors.joining(", ")),
|
||||
valueDescription,
|
||||
entitiesInSameRow));
|
||||
});
|
||||
@@ -455,6 +482,20 @@ public class ComponentCreationService {
|
||||
}
|
||||
|
||||
|
||||
private Optional<TableCell> getFirstTableCell(Entity entity) {
|
||||
|
||||
SemanticNode node = entity.getContainingNode();
|
||||
while (!(node instanceof TableCell)) {
|
||||
if (!node.hasParent()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
node = node.getParent();
|
||||
}
|
||||
|
||||
return Optional.of((TableCell) node);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new component with the given rule identifier, name, value, and value description.
|
||||
* If the component is part of a table, it also takes a list of entities that belong to the same table row.
|
||||
@@ -479,7 +520,9 @@ public class ComponentCreationService {
|
||||
*/
|
||||
public void create(String ruleIdentifier, String name, String value) {
|
||||
|
||||
kieSession.insert(Component.builder().matchedRule(RuleIdentifier.fromString(ruleIdentifier)).name(name)
|
||||
kieSession.insert(Component.builder()
|
||||
.matchedRule(RuleIdentifier.fromString(ruleIdentifier))
|
||||
.name(name)
|
||||
.value(value)
|
||||
.valueDescription("")
|
||||
.references(Collections.emptyList())
|
||||
|
||||
+169
-105
@@ -1,5 +1,6 @@
|
||||
package com.iqser.red.service.redaction.v1.server.service.document;
|
||||
|
||||
import static com.iqser.red.service.redaction.v1.server.service.document.EntityCreationUtility.*;
|
||||
import static com.iqser.red.service.redaction.v1.server.utils.SeparatorUtils.boundaryIsSurroundedBySeparators;
|
||||
|
||||
import java.util.Collection;
|
||||
@@ -18,24 +19,22 @@ import org.kie.api.runtime.KieSession;
|
||||
|
||||
import com.google.common.base.Functions;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.Engine;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.TextRange;
|
||||
import com.iqser.red.service.redaction.v1.server.model.NerEntities;
|
||||
import com.iqser.red.service.redaction.v1.server.model.dictionary.SearchImplementation;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.ConsecutiveBoundaryCollector;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.DocumentTree;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.TextRange;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.entity.EntityType;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.entity.ManualChangeOverwrite;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.entity.TextEntity;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.entity.PositionOnPage;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.NodeType;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.Page;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.SemanticNode;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.Table;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.TableCell;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.textblock.TextBlock;
|
||||
import com.iqser.red.service.redaction.v1.server.utils.RectangleTransformations;
|
||||
import com.iqser.red.service.redaction.v1.server.utils.RedactionSearchUtility;
|
||||
import com.iqser.red.service.redaction.v1.server.model.NerEntities;
|
||||
import com.iqser.red.service.redaction.v1.server.model.dictionary.SearchImplementation;
|
||||
import com.iqser.red.service.redaction.v1.server.utils.IdBuilder;
|
||||
import com.iqser.red.service.redaction.v1.server.utils.exception.NotFoundException;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -57,6 +56,8 @@ public class EntityCreationService {
|
||||
|
||||
public Stream<TextEntity> betweenStrings(String start, String stop, String type, EntityType entityType, SemanticNode node) {
|
||||
|
||||
checkIfBothStartAndEndAreEmpty(start, stop);
|
||||
|
||||
List<TextRange> startTextRanges = RedactionSearchUtility.findTextRangesByString(start, node.getTextBlock());
|
||||
List<TextRange> stopTextRanges = RedactionSearchUtility.findTextRangesByString(stop, node.getTextBlock());
|
||||
|
||||
@@ -66,6 +67,8 @@ public class EntityCreationService {
|
||||
|
||||
public Stream<TextEntity> betweenStringsIgnoreCase(String start, String stop, String type, EntityType entityType, SemanticNode node) {
|
||||
|
||||
checkIfBothStartAndEndAreEmpty(start, stop);
|
||||
|
||||
List<TextRange> startBoundaries = RedactionSearchUtility.findTextRangesByStringIgnoreCase(start, node.getTextBlock());
|
||||
List<TextRange> stopBoundaries = RedactionSearchUtility.findTextRangesByStringIgnoreCase(stop, node.getTextBlock());
|
||||
|
||||
@@ -75,6 +78,8 @@ public class EntityCreationService {
|
||||
|
||||
public Stream<TextEntity> betweenStringsIncludeStart(String start, String stop, String type, EntityType entityType, SemanticNode node) {
|
||||
|
||||
checkIfBothStartAndEndAreEmpty(start, stop);
|
||||
|
||||
List<TextRange> startBoundaries = RedactionSearchUtility.findTextRangesByString(start, node.getTextBlock());
|
||||
List<TextRange> stopBoundaries = RedactionSearchUtility.findTextRangesByString(stop, node.getTextBlock());
|
||||
|
||||
@@ -89,6 +94,8 @@ public class EntityCreationService {
|
||||
|
||||
public Stream<TextEntity> betweenStringsIncludeStartIgnoreCase(String start, String stop, String type, EntityType entityType, SemanticNode node) {
|
||||
|
||||
checkIfBothStartAndEndAreEmpty(start, stop);
|
||||
|
||||
List<TextRange> startBoundaries = RedactionSearchUtility.findTextRangesByStringIgnoreCase(start, node.getTextBlock());
|
||||
List<TextRange> stopBoundaries = RedactionSearchUtility.findTextRangesByStringIgnoreCase(stop, node.getTextBlock());
|
||||
|
||||
@@ -103,6 +110,8 @@ public class EntityCreationService {
|
||||
|
||||
public Stream<TextEntity> betweenStringsIncludeEnd(String start, String stop, String type, EntityType entityType, SemanticNode node) {
|
||||
|
||||
checkIfBothStartAndEndAreEmpty(start, stop);
|
||||
|
||||
List<TextRange> startBoundaries = RedactionSearchUtility.findTextRangesByString(start, node.getTextBlock());
|
||||
List<TextRange> stopBoundaries = RedactionSearchUtility.findTextRangesByString(stop, node.getTextBlock());
|
||||
|
||||
@@ -117,6 +126,8 @@ public class EntityCreationService {
|
||||
|
||||
public Stream<TextEntity> betweenStringsIncludeEndIgnoreCase(String start, String stop, String type, EntityType entityType, SemanticNode node) {
|
||||
|
||||
checkIfBothStartAndEndAreEmpty(start, stop);
|
||||
|
||||
List<TextRange> startBoundaries = RedactionSearchUtility.findTextRangesByStringIgnoreCase(start, node.getTextBlock());
|
||||
List<TextRange> stopBoundaries = RedactionSearchUtility.findTextRangesByStringIgnoreCase(stop, node.getTextBlock());
|
||||
|
||||
@@ -131,6 +142,8 @@ public class EntityCreationService {
|
||||
|
||||
public Stream<TextEntity> betweenStringsIncludeStartAndEnd(String start, String stop, String type, EntityType entityType, SemanticNode node) {
|
||||
|
||||
checkIfBothStartAndEndAreEmpty(start, stop);
|
||||
|
||||
List<TextRange> startBoundaries = RedactionSearchUtility.findTextRangesByString(start, node.getTextBlock());
|
||||
List<TextRange> stopBoundaries = RedactionSearchUtility.findTextRangesByString(stop, node.getTextBlock());
|
||||
|
||||
@@ -149,6 +162,8 @@ public class EntityCreationService {
|
||||
|
||||
public Stream<TextEntity> betweenStringsIncludeStartAndEndIgnoreCase(String start, String stop, String type, EntityType entityType, SemanticNode node) {
|
||||
|
||||
checkIfBothStartAndEndAreEmpty(start, stop);
|
||||
|
||||
List<TextRange> startBoundaries = RedactionSearchUtility.findTextRangesByStringIgnoreCase(start, node.getTextBlock());
|
||||
List<TextRange> stopBoundaries = RedactionSearchUtility.findTextRangesByStringIgnoreCase(stop, node.getTextBlock());
|
||||
|
||||
@@ -167,6 +182,8 @@ public class EntityCreationService {
|
||||
|
||||
public Stream<TextEntity> shortestBetweenAnyString(List<String> starts, List<String> stops, String type, EntityType entityType, SemanticNode node) {
|
||||
|
||||
checkIfBothStartAndEndAreEmpty(starts, stops);
|
||||
|
||||
List<TextRange> startTextRanges = RedactionSearchUtility.findTextRangesByList(starts, node.getTextBlock());
|
||||
List<TextRange> stopTextRanges = RedactionSearchUtility.findTextRangesByList(stops, node.getTextBlock());
|
||||
|
||||
@@ -176,6 +193,8 @@ public class EntityCreationService {
|
||||
|
||||
public Stream<TextEntity> shortestBetweenAnyStringIgnoreCase(List<String> starts, List<String> stops, String type, EntityType entityType, SemanticNode node) {
|
||||
|
||||
checkIfBothStartAndEndAreEmpty(starts, stops);
|
||||
|
||||
List<TextRange> startTextRanges = RedactionSearchUtility.findTextRangesByListIgnoreCase(starts, node.getTextBlock());
|
||||
List<TextRange> stopTextRanges = RedactionSearchUtility.findTextRangesByListIgnoreCase(stops, node.getTextBlock());
|
||||
|
||||
@@ -183,6 +202,17 @@ public class EntityCreationService {
|
||||
}
|
||||
|
||||
|
||||
public Stream<TextEntity> shortestBetweenAnyStringIgnoreCase(List<String> starts, List<String> stops, String type, EntityType entityType, SemanticNode node, int limit) {
|
||||
|
||||
checkIfBothStartAndEndAreEmpty(starts, stops);
|
||||
|
||||
List<TextRange> startTextRanges = RedactionSearchUtility.findTextRangesByListIgnoreCase(starts, node.getTextBlock());
|
||||
List<TextRange> stopTextRanges = RedactionSearchUtility.findTextRangesByListIgnoreCase(stops, node.getTextBlock());
|
||||
|
||||
return betweenTextRanges(startTextRanges, stopTextRanges, type, entityType, node, limit);
|
||||
}
|
||||
|
||||
|
||||
public Stream<TextEntity> betweenRegexes(String regexStart, String regexStop, String type, EntityType entityType, SemanticNode node) {
|
||||
|
||||
TextBlock textBlock = node.getTextBlock();
|
||||
@@ -205,11 +235,15 @@ public class EntityCreationService {
|
||||
|
||||
public Stream<TextEntity> betweenTextRanges(List<TextRange> startBoundaries, List<TextRange> stopBoundaries, String type, EntityType entityType, SemanticNode node) {
|
||||
|
||||
if (startBoundaries.isEmpty() || stopBoundaries.isEmpty()) {
|
||||
return Stream.empty();
|
||||
}
|
||||
return betweenTextRanges(startBoundaries, stopBoundaries, type, entityType, node, 0);
|
||||
}
|
||||
|
||||
|
||||
public Stream<TextEntity> betweenTextRanges(List<TextRange> startBoundaries, List<TextRange> stopBoundaries, String type, EntityType entityType, SemanticNode node, int limit) {
|
||||
|
||||
List<TextRange> entityBoundaries = findNonOverlappingBoundariesBetweenBoundariesWithMinimalDistances(startBoundaries, stopBoundaries);
|
||||
return entityBoundaries.stream()
|
||||
.filter(range -> (limit == 0 || range.length() <= limit))
|
||||
.map(textRange -> textRange.trim(node.getTextBlock()))
|
||||
.filter(textRange -> isValidEntityTextRange(node.getTextBlock(), textRange))
|
||||
.map(textRange -> byTextRange(textRange, type, entityType, node))
|
||||
@@ -251,7 +285,8 @@ public class EntityCreationService {
|
||||
public Stream<TextEntity> bySearchImplementation(SearchImplementation searchImplementation, String type, EntityType entityType, SemanticNode node) {
|
||||
|
||||
return searchImplementation.getBoundaries(node.getTextBlock(), node.getTextRange())
|
||||
.stream().filter(boundary -> isValidEntityTextRange(node.getTextBlock(), boundary))
|
||||
.stream()
|
||||
.filter(boundary -> isValidEntityTextRange(node.getTextBlock(), boundary))
|
||||
.map(bounds -> byTextRange(bounds, type, entityType, node))
|
||||
.filter(Optional::isPresent)
|
||||
.map(Optional::get);
|
||||
@@ -263,7 +298,9 @@ public class EntityCreationService {
|
||||
TextBlock textBlock = node.getTextBlock();
|
||||
SearchImplementation searchImplementation = new SearchImplementation(strings, false);
|
||||
return searchImplementation.getBoundaries(textBlock, node.getTextRange())
|
||||
.stream().map(boundary -> toLineAfterTextRange(textBlock, boundary)).filter(boundary -> isValidEntityTextRange(textBlock, boundary))
|
||||
.stream()
|
||||
.map(boundary -> toLineAfterTextRange(textBlock, boundary))
|
||||
.filter(boundary -> isValidEntityTextRange(textBlock, boundary))
|
||||
.map(boundary -> byTextRange(boundary, type, entityType, node))
|
||||
.filter(Optional::isPresent)
|
||||
.map(Optional::get);
|
||||
@@ -275,7 +312,9 @@ public class EntityCreationService {
|
||||
TextBlock textBlock = node.getTextBlock();
|
||||
SearchImplementation searchImplementation = new SearchImplementation(strings, true);
|
||||
return searchImplementation.getBoundaries(textBlock, node.getTextRange())
|
||||
.stream().map(boundary -> toLineAfterTextRange(textBlock, boundary)).filter(boundary -> isValidEntityTextRange(textBlock, boundary))
|
||||
.stream()
|
||||
.map(boundary -> toLineAfterTextRange(textBlock, boundary))
|
||||
.filter(boundary -> isValidEntityTextRange(textBlock, boundary))
|
||||
.map(boundary -> byTextRange(boundary, type, entityType, node))
|
||||
.filter(Optional::isPresent)
|
||||
.map(Optional::get);
|
||||
@@ -286,7 +325,9 @@ public class EntityCreationService {
|
||||
|
||||
TextBlock textBlock = node.getTextBlock();
|
||||
return RedactionSearchUtility.findTextRangesByString(string, textBlock)
|
||||
.stream().map(boundary -> toLineAfterTextRange(textBlock, boundary)).filter(boundary -> isValidEntityTextRange(textBlock, boundary))
|
||||
.stream()
|
||||
.map(boundary -> toLineAfterTextRange(textBlock, boundary))
|
||||
.filter(boundary -> isValidEntityTextRange(textBlock, boundary))
|
||||
.map(boundary -> byTextRange(boundary, type, entityType, node))
|
||||
.filter(Optional::isPresent)
|
||||
.map(Optional::get);
|
||||
@@ -297,7 +338,9 @@ public class EntityCreationService {
|
||||
|
||||
TextBlock textBlock = node.getTextBlock();
|
||||
return RedactionSearchUtility.findTextRangesByStringIgnoreCase(string, textBlock)
|
||||
.stream().map(boundary -> toLineAfterTextRange(textBlock, boundary)).filter(boundary -> isValidEntityTextRange(textBlock, boundary))
|
||||
.stream()
|
||||
.map(boundary -> toLineAfterTextRange(textBlock, boundary))
|
||||
.filter(boundary -> isValidEntityTextRange(textBlock, boundary))
|
||||
.map(boundary -> byTextRange(boundary, type, entityType, node))
|
||||
.filter(Optional::isPresent)
|
||||
.map(Optional::get);
|
||||
@@ -306,7 +349,8 @@ public class EntityCreationService {
|
||||
|
||||
public Stream<TextEntity> lineAfterStringAcrossColumns(String string, String type, EntityType entityType, Table tableNode) {
|
||||
|
||||
return tableNode.streamTableCells().flatMap(tableCell -> lineAfterBoundariesAcrossColumns(RedactionSearchUtility.findTextRangesByString(string, tableCell.getTextBlock()),
|
||||
return tableNode.streamTableCells()
|
||||
.flatMap(tableCell -> lineAfterBoundariesAcrossColumns(RedactionSearchUtility.findTextRangesByString(string, tableCell.getTextBlock()),
|
||||
tableCell,
|
||||
type,
|
||||
entityType,
|
||||
@@ -499,7 +543,7 @@ public class EntityCreationService {
|
||||
public Optional<TextEntity> byPrefixExpansionRegex(TextEntity entity, String regexPattern) {
|
||||
|
||||
int expandedStart = RedactionSearchUtility.getExpandedStartByRegex(entity, regexPattern);
|
||||
return byTextRange(new TextRange(expandedStart, entity.getTextRange().end()), entity.getType(), entity.getEntityType(), entity.getDeepestFullyContainingNode());
|
||||
return byTextRange(new TextRange(expandedStart, entity.getTextRange().end()), entity.type(), entity.getEntityType(), entity.getDeepestFullyContainingNode());
|
||||
}
|
||||
|
||||
|
||||
@@ -507,16 +551,7 @@ public class EntityCreationService {
|
||||
|
||||
int expandedEnd = RedactionSearchUtility.getExpandedEndByRegex(entity, regexPattern);
|
||||
expandedEnd = truncateEndIfLineBreakIsBetween(entity.getTextRange().end(), expandedEnd, entity.getDeepestFullyContainingNode().getTextBlock());
|
||||
return byTextRange(new TextRange(entity.getTextRange().start(), expandedEnd), entity.getType(), entity.getEntityType(), entity.getDeepestFullyContainingNode());
|
||||
}
|
||||
|
||||
|
||||
private int truncateEndIfLineBreakIsBetween(int end, int expandedEnd, TextBlock textBlock) {
|
||||
|
||||
if (textBlock.getNextLinebreak(end) < expandedEnd) {
|
||||
return end;
|
||||
}
|
||||
return expandedEnd;
|
||||
return byTextRange(new TextRange(entity.getTextRange().start(), expandedEnd), entity.type(), entity.getEntityType(), entity.getDeepestFullyContainingNode());
|
||||
}
|
||||
|
||||
|
||||
@@ -533,25 +568,68 @@ public class EntityCreationService {
|
||||
*/
|
||||
public Optional<TextEntity> byTextRange(TextRange textRange, String type, EntityType entityType, SemanticNode node) {
|
||||
|
||||
return byTextRangeWithEngine(textRange, type, entityType, node, Set.of(Engine.RULE));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a redaction entity based on the given boundary, type, entity type, and semantic node.
|
||||
* If the document already contains an equal redaction entity, then the original Entity is returned.
|
||||
* Also inserts the Entity into the kieSession.
|
||||
*
|
||||
* @param textRange The boundary of the redaction entity.
|
||||
* @param type The type of the redaction entity.
|
||||
* @param entityType The entity type of the redaction entity.
|
||||
* @param node The semantic node to associate with the redaction entity.
|
||||
* @param engines Set of engines to be used.
|
||||
* @return An Optional containing the redaction entity, or the previous entity if the entity already exists.
|
||||
*/
|
||||
public Optional<TextEntity> byTextRangeWithEngine(TextRange textRange, String type, EntityType entityType, SemanticNode node, Set<Engine> engines) {
|
||||
|
||||
if (!node.getTextRange().contains(textRange)) {
|
||||
throw new IllegalArgumentException(String.format("%s is not in the %s of the provided semantic node %s", textRange, node.getTextRange(), node));
|
||||
}
|
||||
TextRange trimmedTextRange = textRange.trim(node.getTextBlock());
|
||||
TextEntity entity = TextEntity.initialEntityNode(trimmedTextRange, type, entityType);
|
||||
TextEntity entity = TextEntity.initialEntityNode(trimmedTextRange, type, entityType, node);
|
||||
if (node.getEntities().contains(entity)) {
|
||||
return node.getEntities().stream().filter(entity::equals).peek(e -> e.addEngine(Engine.RULE)).findAny();
|
||||
Optional<TextEntity> optionalTextEntity = node.getEntities().stream().filter(e -> e.equals(entity) && e.type().equals(type)).peek(e -> e.addEngines(engines)).findAny();
|
||||
if (optionalTextEntity.isEmpty()) {
|
||||
return optionalTextEntity; // Entity has been recategorized and should not be created at all.
|
||||
}
|
||||
TextEntity existingEntity = optionalTextEntity.get();
|
||||
if (existingEntity.getTextRange().equals(textRange)) {
|
||||
return optionalTextEntity; // exactly the same entity, return directly
|
||||
}
|
||||
if (!existingEntity.resized()) {
|
||||
addDuplicateEntityToGraph(existingEntity, textRange, node);
|
||||
return optionalTextEntity; // If Entity has not been resized, insert as duplicate at appropriate position
|
||||
}
|
||||
return Optional.empty(); // Entity has been resized, if there are duplicates they should be treated there
|
||||
}
|
||||
addEntityToGraph(entity, node);
|
||||
entity.addEngine(Engine.RULE);
|
||||
entity.addEngines(engines);
|
||||
insertToKieSession(entity);
|
||||
return Optional.of(entity);
|
||||
}
|
||||
|
||||
|
||||
// Do not use anymore. This might not work correctly due to duplicate textranges not being taken into account here.
|
||||
@Deprecated(forRemoval = true)
|
||||
public TextEntity forceByTextRange(TextRange textRange, String type, EntityType entityType, SemanticNode node) {
|
||||
|
||||
TextRange trimmedTextRange = textRange.trim(node.getTextBlock());
|
||||
TextEntity entity = TextEntity.initialEntityNode(trimmedTextRange, type, entityType);
|
||||
TextEntity entity = TextEntity.initialEntityNode(trimmedTextRange, type, entityType, node);
|
||||
addEntityToGraph(entity, node);
|
||||
return entity;
|
||||
}
|
||||
|
||||
|
||||
// Do not use anymore. This might not work correctly due to duplicate textranges not being taken into account here.
|
||||
@Deprecated(forRemoval = true)
|
||||
public TextEntity forceByTextRange(TextRange textRange, String type, EntityType entityType, SemanticNode node, String id) {
|
||||
|
||||
TextRange trimmedTextRange = textRange.trim(node.getTextBlock());
|
||||
TextEntity entity = TextEntity.initialEntityNode(trimmedTextRange, type, entityType, id);
|
||||
addEntityToGraph(entity, node);
|
||||
return entity;
|
||||
}
|
||||
@@ -569,7 +647,7 @@ public class EntityCreationService {
|
||||
return entitiesToMerge.get(0);
|
||||
}
|
||||
|
||||
TextEntity mergedEntity = TextEntity.initialEntityNode(TextRange.merge(entitiesToMerge.stream().map(TextEntity::getTextRange).toList()), type, entityType);
|
||||
TextEntity mergedEntity = TextEntity.initialEntityNode(TextRange.merge(entitiesToMerge.stream().map(TextEntity::getTextRange).toList()), type, entityType, node);
|
||||
mergedEntity.addEngines(entitiesToMerge.stream().flatMap(entityNode -> entityNode.getEngines().stream()).collect(Collectors.toSet()));
|
||||
entitiesToMerge.stream().map(TextEntity::getMatchedRuleList).flatMap(Collection::stream).forEach(matchedRule -> mergedEntity.getMatchedRuleList().add(matchedRule));
|
||||
entitiesToMerge.stream()
|
||||
@@ -600,15 +678,14 @@ public class EntityCreationService {
|
||||
return newEntity;
|
||||
}
|
||||
|
||||
|
||||
public TextEntity copyEntityWithoutRules(TextEntity entity, String type, EntityType entityType, SemanticNode node) {
|
||||
|
||||
TextEntity newEntity = TextEntity.initialEntityNode(entity.getTextRange(), type, entityType);
|
||||
newEntity.addEngines(entity.getEngines());
|
||||
TextEntity newEntity = byTextRangeWithEngine(entity.getTextRange(), type, entityType, node, entity.getEngines()).orElseThrow(() -> new NotFoundException(
|
||||
"No entity present!"));
|
||||
newEntity.getManualOverwrite().addChanges(entity.getManualOverwrite().getManualChangeLog());
|
||||
newEntity.setDictionaryEntry(entity.isDictionaryEntry());
|
||||
newEntity.setDossierDictionaryEntry(entity.isDossierDictionaryEntry());
|
||||
addEntityToGraph(newEntity, node);
|
||||
insertToKieSession(newEntity);
|
||||
return newEntity;
|
||||
}
|
||||
|
||||
@@ -623,42 +700,35 @@ public class EntityCreationService {
|
||||
|
||||
public TextEntity byNerEntity(NerEntities.NerEntity nerEntity, EntityType entityType, SemanticNode semanticNode) {
|
||||
|
||||
var entity = forceByTextRange(nerEntity.textRange(), nerEntity.type(), entityType, semanticNode);
|
||||
entity.addEngine(Engine.NER);
|
||||
insertToKieSession(entity);
|
||||
return entity;
|
||||
return byTextRangeWithEngine(nerEntity.textRange(), nerEntity.type(), entityType, semanticNode, Set.of(Engine.NER)).orElseThrow(() -> new NotFoundException(
|
||||
"No entity present!"));
|
||||
}
|
||||
|
||||
|
||||
public TextEntity byNerEntity(NerEntities.NerEntity nerEntity, String type, EntityType entityType, SemanticNode semanticNode) {
|
||||
|
||||
var entity = forceByTextRange(nerEntity.textRange(), type, entityType, semanticNode);
|
||||
entity.addEngine(Engine.NER);
|
||||
insertToKieSession(entity);
|
||||
return entity;
|
||||
return byTextRangeWithEngine(nerEntity.textRange(), type, entityType, semanticNode, Set.of(Engine.NER)).orElseThrow(() -> new NotFoundException("No entity present!"));
|
||||
}
|
||||
|
||||
|
||||
public Optional<TextEntity> optionalByNerEntity(NerEntities.NerEntity nerEntity, EntityType entityType, SemanticNode semanticNode) {
|
||||
|
||||
return byTextRangeWithEngine(nerEntity.textRange(), nerEntity.type(), entityType, semanticNode, Set.of(Engine.NER));
|
||||
}
|
||||
|
||||
|
||||
public Optional<TextEntity> optionalByNerEntity(NerEntities.NerEntity nerEntity, String type, EntityType entityType, SemanticNode semanticNode) {
|
||||
|
||||
return byTextRangeWithEngine(nerEntity.textRange(), type, entityType, semanticNode, Set.of(Engine.NER));
|
||||
}
|
||||
|
||||
|
||||
public Stream<TextEntity> combineNerEntitiesToCbiAddressDefaults(NerEntities nerEntities, String type, EntityType entityType, SemanticNode semanticNode) {
|
||||
|
||||
return NerEntitiesAdapter.combineNerEntitiesToCbiAddressDefaults(nerEntities).map(boundary -> forceByTextRange(boundary, type, entityType, semanticNode))
|
||||
.peek(entity -> entity.addEngine(Engine.NER))
|
||||
.peek(this::insertToKieSession);
|
||||
}
|
||||
|
||||
|
||||
public TextEntity byTableCellAsHighlight(TableCell tableCell, String type, EntityType entityType) {
|
||||
|
||||
TextEntity highlightEntity = TextEntity.initialEntityNode(new TextRange(tableCell.getTextRange().start(), tableCell.getTextRange().start()), type, entityType);
|
||||
|
||||
String positionId = IdBuilder.buildId(tableCell.getBBox().keySet(), tableCell.getBBox().values().stream().toList(), type, entityType.name());
|
||||
highlightEntity.setPositionsOnPagePerPage(tableCell.getBBox()
|
||||
.entrySet()
|
||||
.stream()
|
||||
.map(entry -> new PositionOnPage(positionId, entry.getKey(), List.of(entry.getValue())))
|
||||
.toList());
|
||||
addEntityToGraph(highlightEntity, tableCell);
|
||||
return highlightEntity;
|
||||
return NerEntitiesAdapter.combineNerEntitiesToCbiAddressDefaults(nerEntities)
|
||||
.map(boundary -> byTextRangeWithEngine(boundary, type, entityType, semanticNode, Set.of(Engine.NER)))
|
||||
.filter(Optional::isPresent)
|
||||
.map(Optional::get);
|
||||
}
|
||||
|
||||
|
||||
@@ -672,9 +742,17 @@ public class EntityCreationService {
|
||||
|
||||
DocumentTree documentTree = node.getDocumentTree();
|
||||
try {
|
||||
addEntityToGraph(entity, documentTree);
|
||||
entity.addIntersectingNode(documentTree.getRoot().getNode());
|
||||
documentTree.getRoot().getNode().getEntities().add(entity);
|
||||
if (node.getEntities().contains(entity)) {
|
||||
// If entity already exists and it has a different text range, we add the text range to the list of duplicated text ranges
|
||||
node.getEntities().stream()//
|
||||
.filter(e -> e.equals(entity))//
|
||||
.filter(e -> !e.getTextRange().equals(entity.getTextRange()))//
|
||||
.findAny()//
|
||||
.ifPresent(entityToDuplicate -> addDuplicateEntityToGraph(entityToDuplicate, entity.getTextRange(), node));
|
||||
} else {
|
||||
entity.addIntersectingNode(documentTree.getRoot().getNode());
|
||||
addEntityToGraph(entity, documentTree);
|
||||
}
|
||||
} catch (NoSuchElementException e) {
|
||||
entity.setDeepestFullyContainingNode(documentTree.getRoot().getNode());
|
||||
entityEnrichmentService.enrichEntity(entity, entity.getDeepestFullyContainingNode().getTextBlock());
|
||||
@@ -685,6 +763,33 @@ public class EntityCreationService {
|
||||
}
|
||||
|
||||
|
||||
private void addDuplicateEntityToGraph(TextEntity entityToDuplicate, TextRange newTextRange, SemanticNode node) {
|
||||
|
||||
entityToDuplicate.addTextRange(newTextRange);
|
||||
|
||||
SemanticNode deepestSharedNode = entityToDuplicate.getIntersectingNodes()
|
||||
.stream()
|
||||
.sorted(Comparator.comparingInt(n -> -n.getTreeId().size()))
|
||||
.filter(intersectingNode -> entityToDuplicate.getDuplicateTextRanges().stream().allMatch(tr -> intersectingNode.getTextRange().contains(tr)) && //
|
||||
intersectingNode.getTextRange().contains(entityToDuplicate.getTextRange()))
|
||||
.findFirst()
|
||||
.orElse(node.getDocumentTree().getRoot().getNode());
|
||||
|
||||
entityToDuplicate.setDeepestFullyContainingNode(deepestSharedNode);
|
||||
|
||||
Set<SemanticNode> additionalIntersectingNodes = findIntersectingSubNodes(deepestSharedNode, newTextRange);
|
||||
|
||||
additionalIntersectingNodes.forEach(additionalIntersectingNode -> {
|
||||
if (entityToDuplicate.getIntersectingNodes().contains(additionalIntersectingNode)) {
|
||||
return;
|
||||
}
|
||||
additionalIntersectingNode.getEntities().add(entityToDuplicate);
|
||||
additionalIntersectingNode.getPages(newTextRange).forEach(page -> page.getEntities().add(entityToDuplicate));
|
||||
entityToDuplicate.addIntersectingNode(additionalIntersectingNode);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private void addEntityToGraph(TextEntity entity, DocumentTree documentTree) {
|
||||
|
||||
SemanticNode containingNode = documentTree.childNodes(Collections.emptyList())
|
||||
@@ -702,45 +807,4 @@ public class EntityCreationService {
|
||||
}
|
||||
|
||||
|
||||
private static void addToPages(TextEntity entity) {
|
||||
|
||||
Set<Page> pages = entity.getDeepestFullyContainingNode().getPages(entity.getTextRange());
|
||||
entity.getPages().addAll(pages);
|
||||
pages.forEach(page -> page.getEntities().add(entity));
|
||||
}
|
||||
|
||||
|
||||
private static void addEntityToNodeEntitySets(TextEntity entity) {
|
||||
|
||||
entity.getIntersectingNodes().forEach(node -> node.getEntities().add(entity));
|
||||
}
|
||||
|
||||
|
||||
private static boolean allEntitiesIntersectAndHaveSameTypes(List<TextEntity> entitiesToMerge) {
|
||||
|
||||
if (entitiesToMerge.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
TextEntity previousEntity = entitiesToMerge.get(0);
|
||||
for (TextEntity textEntity : entitiesToMerge.subList(1, entitiesToMerge.size())) {
|
||||
boolean typeMatches = textEntity.getType().equals(previousEntity.getType());
|
||||
boolean entityTypeMatches = textEntity.getEntityType().equals(previousEntity.getEntityType());
|
||||
boolean intersects = textEntity.intersects(previousEntity);
|
||||
if (!typeMatches || !entityTypeMatches || !intersects) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private static TextRange toLineAfterTextRange(TextBlock textBlock, TextRange textRange) {
|
||||
|
||||
if (textBlock.getTextRange().end() == textRange.end()) {
|
||||
return new TextRange(textRange.end(), textRange.end());
|
||||
}
|
||||
|
||||
return new TextRange(textRange.end(), textBlock.getNextLinebreak(textRange.end())).trim(textBlock);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
package com.iqser.red.service.redaction.v1.server.service.document;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.TextRange;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.entity.TextEntity;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.Page;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.SemanticNode;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.textblock.TextBlock;
|
||||
|
||||
public class EntityCreationUtility {
|
||||
|
||||
|
||||
public static void checkIfBothStartAndEndAreEmpty(String start, String end) {
|
||||
|
||||
checkIfBothStartAndEndAreEmpty(List.of(start), List.of(end));
|
||||
}
|
||||
|
||||
|
||||
public static <T> void checkIfBothStartAndEndAreEmpty(List<T> start, List<T> end) {
|
||||
|
||||
if ((start == null || start.isEmpty()) && (end == null || end.isEmpty())) {
|
||||
throw new IllegalArgumentException("Start and end values are empty!");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static int truncateEndIfLineBreakIsBetween(int end, int expandedEnd, TextBlock textBlock) {
|
||||
|
||||
if (textBlock.getNextLinebreak(end) < expandedEnd) {
|
||||
return end;
|
||||
}
|
||||
return expandedEnd;
|
||||
}
|
||||
|
||||
|
||||
public static Set<SemanticNode> findIntersectingSubNodes(SemanticNode initialIntersectingNode, TextRange textRange) {
|
||||
|
||||
IntersectingNodeVisitor visitor = new IntersectingNodeVisitor(textRange);
|
||||
|
||||
if (initialIntersectingNode.getTextRange().intersects(textRange)) {
|
||||
initialIntersectingNode.accept(visitor);
|
||||
}
|
||||
|
||||
return visitor.getIntersectingNodes();
|
||||
}
|
||||
|
||||
|
||||
public static void addToPages(TextEntity entity) {
|
||||
|
||||
Set<Page> pages = entity.getDeepestFullyContainingNode().getPages(entity.getTextRange());
|
||||
entity.getPages().addAll(pages);
|
||||
pages.forEach(page -> page.getEntities().add(entity));
|
||||
}
|
||||
|
||||
|
||||
public static void addEntityToNodeEntitySets(TextEntity entity) {
|
||||
|
||||
entity.getIntersectingNodes().forEach(node -> node.getEntities().add(entity));
|
||||
}
|
||||
|
||||
|
||||
public static boolean allEntitiesIntersectAndHaveSameTypes(List<TextEntity> entitiesToMerge) {
|
||||
|
||||
if (entitiesToMerge.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
TextEntity previousEntity = entitiesToMerge.get(0);
|
||||
for (TextEntity textEntity : entitiesToMerge.subList(1, entitiesToMerge.size())) {
|
||||
boolean typeMatches = textEntity.type().equals(previousEntity.type());
|
||||
boolean entityTypeMatches = textEntity.getEntityType().equals(previousEntity.getEntityType());
|
||||
boolean intersects = textEntity.intersects(previousEntity);
|
||||
if (!typeMatches || !entityTypeMatches || !intersects) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public static TextRange toLineAfterTextRange(TextBlock textBlock, TextRange textRange) {
|
||||
|
||||
if (textBlock.getTextRange().end() == textRange.end()) {
|
||||
return new TextRange(textRange.end(), textRange.end());
|
||||
}
|
||||
|
||||
return new TextRange(textRange.end(), textBlock.getNextLinebreak(textRange.end())).trim(textBlock);
|
||||
}
|
||||
|
||||
}
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
package com.iqser.red.service.redaction.v1.server.service.document;
|
||||
|
||||
import static java.lang.String.format;
|
||||
import static java.util.stream.Collectors.groupingBy;
|
||||
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.iqser.red.service.redaction.v1.server.model.ClosestEntity;
|
||||
import com.iqser.red.service.redaction.v1.server.model.PrecursorEntity;
|
||||
import com.iqser.red.service.redaction.v1.server.model.RectangleWithPage;
|
||||
import com.iqser.red.service.redaction.v1.server.model.dictionary.SearchImplementation;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.entity.EntityType;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.entity.PositionOnPage;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.entity.TextEntity;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.Page;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.SemanticNode;
|
||||
import com.iqser.red.service.redaction.v1.server.service.DictionaryService;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class EntityFindingUtility {
|
||||
|
||||
EntityCreationService entityCreationService;
|
||||
DictionaryService dictionaryService;
|
||||
|
||||
|
||||
@Autowired
|
||||
public EntityFindingUtility(EntityEnrichmentService entityEnrichmentService, DictionaryService dictionaryService) {
|
||||
|
||||
entityCreationService = new EntityCreationService(entityEnrichmentService);
|
||||
this.dictionaryService = dictionaryService;
|
||||
}
|
||||
|
||||
|
||||
public Optional<TextEntity> findClosestEntityAndReturnEmptyIfNotFound(PrecursorEntity precursorEntity, Map<String, List<TextEntity>> entitiesWithSameValue, double matchThreshold) {
|
||||
|
||||
if (precursorEntity.getValue() == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
List<TextEntity> possibleEntities = entitiesWithSameValue.get(precursorEntity.getValue().toLowerCase(Locale.ENGLISH));
|
||||
|
||||
if (entityIdentifierValueNotFound(possibleEntities)) {
|
||||
log.warn("Entity could not be created with precursorEntity: {}, due to the value {} not being found anywhere.", precursorEntity, precursorEntity.getValue());
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
Optional<ClosestEntity> optionalClosestEntity = possibleEntities.stream()
|
||||
.filter(entity -> pagesMatch(entity, precursorEntity.getEntityPosition()))
|
||||
.map(entity -> ClosestEntity.builder().distance(calculateMinDistance(precursorEntity.getEntityPosition(), entity)).textEntity(entity).build())
|
||||
.min(Comparator.comparingDouble(ClosestEntity::getDistance));
|
||||
|
||||
if (optionalClosestEntity.isEmpty()) {
|
||||
log.warn("No Entity with value {} found on page {}", precursorEntity.getValue(), precursorEntity.getEntityPosition());
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
ClosestEntity closestEntity = optionalClosestEntity.get();
|
||||
if (closestEntity.getDistance() > matchThreshold) {
|
||||
log.warn("For entity {} on page {} with positions {} distance to closest found entity is {} and therefore higher than the threshold of {}",
|
||||
precursorEntity.getValue(),
|
||||
precursorEntity.getEntityPosition().get(0).pageNumber(),
|
||||
precursorEntity.getEntityPosition().stream().map(RectangleWithPage::rectangle2D).toList(),
|
||||
closestEntity.getDistance(),
|
||||
matchThreshold);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(closestEntity.getTextEntity());
|
||||
}
|
||||
|
||||
|
||||
private static boolean entityIdentifierValueNotFound(List<TextEntity> possibleEntities) {
|
||||
|
||||
return possibleEntities == null || possibleEntities.isEmpty();
|
||||
}
|
||||
|
||||
|
||||
private static boolean pagesMatch(TextEntity entity, List<RectangleWithPage> originalPositions) {
|
||||
|
||||
Set<Integer> entityPageNumbers = entity.getPositionsOnPagePerPage().stream().map(PositionOnPage::getPage).map(Page::getNumber).collect(Collectors.toSet());
|
||||
Set<Integer> originalPageNumbers = originalPositions.stream().map(RectangleWithPage::pageNumber).collect(Collectors.toSet());
|
||||
return entityPageNumbers.containsAll(originalPageNumbers);
|
||||
}
|
||||
|
||||
|
||||
public static double calculateMinDistance(List<RectangleWithPage> originalPositions, TextEntity entity) {
|
||||
|
||||
if (originalPositions.size() != countRectangles(entity)) {
|
||||
return Double.MAX_VALUE;
|
||||
}
|
||||
return originalPositions.stream()
|
||||
.mapToDouble(rectangleWithPage -> calculateMinDistancePerRectangle(entity, rectangleWithPage.pageNumber(), rectangleWithPage.rectangle2D()))
|
||||
.average()
|
||||
.orElse(Double.MAX_VALUE);
|
||||
}
|
||||
|
||||
|
||||
private static long countRectangles(TextEntity entity) {
|
||||
|
||||
return entity.getPositionsOnPagePerPage().stream().mapToLong(redactionPosition -> redactionPosition.getRectanglePerLine().size()).sum();
|
||||
}
|
||||
|
||||
|
||||
private static double calculateMinDistancePerRectangle(TextEntity entity, int pageNumber, Rectangle2D originalRectangle) {
|
||||
|
||||
return entity.getPositionsOnPagePerPage()
|
||||
.stream()
|
||||
.filter(redactionPosition -> redactionPosition.getPage().getNumber() == pageNumber)
|
||||
.map(PositionOnPage::getRectanglePerLine)
|
||||
.flatMap(Collection::stream)
|
||||
.mapToDouble(rectangle -> calculateDistance(rectangle, originalRectangle))
|
||||
.min()
|
||||
.orElse(Double.MAX_VALUE);
|
||||
}
|
||||
|
||||
|
||||
public static double calculateDistance(Rectangle2D rectangle1, Rectangle2D rectangle2) {
|
||||
|
||||
// mirrored coordinates safe comparison
|
||||
double minX1 = Math.min(rectangle1.getMinX(), rectangle1.getMaxX());
|
||||
double minY1 = Math.min(rectangle1.getMinY(), rectangle1.getMaxY());
|
||||
double maxX1 = Math.max(rectangle1.getMinX(), rectangle1.getMaxX());
|
||||
double maxY1 = Math.max(rectangle1.getMinY(), rectangle1.getMaxY());
|
||||
|
||||
double minX2 = Math.min(rectangle2.getMinX(), rectangle2.getMaxX());
|
||||
double minY2 = Math.min(rectangle2.getMinY(), rectangle2.getMaxY());
|
||||
double maxX2 = Math.max(rectangle2.getMinX(), rectangle2.getMaxX());
|
||||
double maxY2 = Math.max(rectangle2.getMinY(), rectangle2.getMaxY());
|
||||
|
||||
return Math.abs(minX1 - minX2) //
|
||||
+ Math.abs(minY1 - minY2) //
|
||||
+ Math.abs(maxX1 - maxX2) //
|
||||
+ Math.abs(maxY1 - maxY2);
|
||||
}
|
||||
|
||||
|
||||
public Map<String, List<TextEntity>> findAllPossibleEntitiesAndGroupByValue(SemanticNode node, List<PrecursorEntity> manualEntities) {
|
||||
|
||||
Set<Integer> pageNumbers = manualEntities.stream().flatMap(entry -> entry.getEntityPosition().stream().map(RectangleWithPage::pageNumber)).collect(Collectors.toSet());
|
||||
Set<String> entryValues = manualEntities.stream().map(PrecursorEntity::getValue).filter(Objects::nonNull).map(String::toLowerCase).collect(Collectors.toSet());
|
||||
|
||||
if (!pageNumbers.stream().allMatch(node::onPage)) {
|
||||
throw new IllegalArgumentException(format("SemanticNode \"%s\" does not contain these pages %s, it has pages: %s",
|
||||
node,
|
||||
pageNumbers.stream().filter(pageNumber -> !node.onPage(pageNumber)).toList(),
|
||||
node.getPages()));
|
||||
}
|
||||
SearchImplementation searchImplementation = new SearchImplementation(entryValues, true);
|
||||
|
||||
return searchImplementation.getBoundaries(node.getTextBlock(), node.getTextRange())
|
||||
.stream()
|
||||
.map(boundary -> entityCreationService.byTextRangeWithEngine(boundary, "temp", EntityType.ENTITY, node, Collections.emptySet()))
|
||||
.filter(Optional::isPresent)
|
||||
.map(Optional::get)
|
||||
.distinct()
|
||||
.collect(groupingBy(entity -> entity.getValue().toLowerCase(Locale.ROOT)));
|
||||
}
|
||||
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
package com.iqser.red.service.redaction.v1.server.service.document;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.imported.ImportedRedactions;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.ManualRedactions;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.BaseAnnotation;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.IdRemoval;
|
||||
import com.iqser.red.service.redaction.v1.server.model.PrecursorEntity;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.entity.EntityType;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.entity.TextEntity;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.SemanticNode;
|
||||
import com.iqser.red.service.redaction.v1.server.service.DictionaryService;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
|
||||
public class EntityFromPrecursorCreationService {
|
||||
|
||||
static double MATCH_THRESHOLD = 10; // Is compared to the average sum of distances in pdf coordinates for each corner of the bounding box of the entities
|
||||
EntityFindingUtility entityFindingUtility;
|
||||
EntityCreationService entityCreationService;
|
||||
DictionaryService dictionaryService;
|
||||
|
||||
|
||||
@Autowired
|
||||
public EntityFromPrecursorCreationService(EntityEnrichmentService entityEnrichmentService, DictionaryService dictionaryService, EntityFindingUtility entityFindingUtility) {
|
||||
|
||||
this.entityFindingUtility = entityFindingUtility;
|
||||
entityCreationService = new EntityCreationService(entityEnrichmentService);
|
||||
this.dictionaryService = dictionaryService;
|
||||
}
|
||||
|
||||
|
||||
public List<PrecursorEntity> createEntitiesIfFoundAndReturnNotFoundEntries(ManualRedactions manualRedactions, SemanticNode node, String dossierTemplateId) {
|
||||
|
||||
Set<IdRemoval> idRemovals = manualRedactions.getIdsToRemove();
|
||||
List<PrecursorEntity> manualEntities = manualRedactions.getEntriesToAdd()
|
||||
.stream()
|
||||
.filter(manualRedactionEntry -> !(idRemovals.stream()
|
||||
.map(BaseAnnotation::getAnnotationId)
|
||||
.toList()
|
||||
.contains(manualRedactionEntry.getAnnotationId()) && manualRedactionEntry.getRequestDate()
|
||||
.isBefore(idRemovals.stream()
|
||||
.filter(idRemoval -> idRemoval.getAnnotationId().equals(manualRedactionEntry.getAnnotationId()))
|
||||
.findFirst()
|
||||
.get()
|
||||
.getRequestDate())))
|
||||
.filter(manualRedactionEntry -> !(manualRedactionEntry.isAddToDictionary() || manualRedactionEntry.isAddToDossierDictionary()))
|
||||
.map(manualRedactionEntry -> PrecursorEntity.fromManualRedactionEntry(manualRedactionEntry,
|
||||
dictionaryService.isHint(manualRedactionEntry.getType(), dossierTemplateId)))
|
||||
.peek(manualEntity -> {
|
||||
if (manualEntity.getEntityType().equals(EntityType.HINT)) {
|
||||
manualEntity.skip("MAN.5.1", "manual hint is skipped by default");
|
||||
} else {
|
||||
manualEntity.apply("MAN.5.0", "manual entries are applied by default", manualEntity.getLegalBasis());
|
||||
}
|
||||
})
|
||||
.toList();
|
||||
|
||||
return toTextEntity(manualEntities, node);
|
||||
}
|
||||
|
||||
|
||||
public List<PrecursorEntity> createEntitiesIfFoundAndReturnNotFoundEntries(ImportedRedactions importedRedactions, SemanticNode node) {
|
||||
|
||||
List<PrecursorEntity> importedEntities = importedRedactions.getImportedRedactions()
|
||||
.stream()
|
||||
.map(PrecursorEntity::fromImportedEntry)
|
||||
.peek(importedEntity -> importedEntity.apply("IMP.0.0", importedEntity.getReason(), importedEntity.getLegalBasis()))
|
||||
.toList();
|
||||
//we could add image handling here as well and create not only text entities
|
||||
return toTextEntity(importedEntities, node);
|
||||
}
|
||||
|
||||
|
||||
public List<PrecursorEntity> toTextEntity(List<PrecursorEntity> precursorEntities, SemanticNode node) {
|
||||
|
||||
var notFoundEntities = precursorEntities.stream().filter(PrecursorEntity::isRectangle).collect(Collectors.toList());
|
||||
var findableEntities = precursorEntities.stream().filter(precursorEntity -> !precursorEntity.isRectangle()).toList();
|
||||
Map<String, List<TextEntity>> tempEntitiesByValue = entityFindingUtility.findAllPossibleEntitiesAndGroupByValue(node, findableEntities);
|
||||
|
||||
for (PrecursorEntity precursorEntity : findableEntities) {
|
||||
Optional<TextEntity> optionalClosestEntity = entityFindingUtility.findClosestEntityAndReturnEmptyIfNotFound(precursorEntity, tempEntitiesByValue, MATCH_THRESHOLD);
|
||||
if (optionalClosestEntity.isEmpty()) {
|
||||
notFoundEntities.add(precursorEntity);
|
||||
continue;
|
||||
}
|
||||
createCorrectEntity(precursorEntity, optionalClosestEntity.get());
|
||||
}
|
||||
tempEntitiesByValue.values().stream().flatMap(Collection::stream).forEach(TextEntity::removeFromGraph);
|
||||
return notFoundEntities;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Deletes the temp Entity and creates a RedactionEntity with correct values, based on the given parameters.
|
||||
*
|
||||
* @param precursorEntity The entity identifier for the RedactionEntity.
|
||||
* @param closestEntity The closest Boundary to the RedactionEntity.
|
||||
*/
|
||||
private void createCorrectEntity(PrecursorEntity precursorEntity, TextEntity closestEntity) {
|
||||
|
||||
TextEntity correctEntity = TextEntity.initialEntityNode(closestEntity.getTextRange(), precursorEntity.type(), precursorEntity.getEntityType(), precursorEntity.getId());
|
||||
correctEntity.setDeepestFullyContainingNode(closestEntity.getDeepestFullyContainingNode());
|
||||
correctEntity.setIntersectingNodes(new ArrayList<>(closestEntity.getIntersectingNodes()));
|
||||
correctEntity.setDuplicateTextRanges(new ArrayList<>(closestEntity.getDuplicateTextRanges()));
|
||||
correctEntity.setPages(new HashSet<>(closestEntity.getPages()));
|
||||
|
||||
correctEntity.setValue(closestEntity.getValue());
|
||||
correctEntity.setTextAfter(closestEntity.getTextAfter());
|
||||
correctEntity.setTextBefore(closestEntity.getTextBefore());
|
||||
|
||||
correctEntity.getIntersectingNodes().forEach(n -> n.getEntities().add(correctEntity));
|
||||
correctEntity.getPages().forEach(page -> page.getEntities().add(correctEntity));
|
||||
|
||||
correctEntity.addMatchedRules(precursorEntity.getMatchedRuleList());
|
||||
correctEntity.setDictionaryEntry(precursorEntity.isDictionaryEntry());
|
||||
correctEntity.setDossierDictionaryEntry(precursorEntity.isDossierDictionaryEntry());
|
||||
correctEntity.getManualOverwrite().addChanges(precursorEntity.getManualOverwrite().getManualChangeLog());
|
||||
correctEntity.addEngines(precursorEntity.getEngines());
|
||||
}
|
||||
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package com.iqser.red.service.redaction.v1.server.service.document;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.AnalyzeRequest;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.imported.ImportedRedactions;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.ManualRedactions;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.BaseAnnotation;
|
||||
import com.iqser.red.service.redaction.v1.server.model.PrecursorEntity;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.Document;
|
||||
|
||||
import io.micrometer.observation.annotation.Observed;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ImportedRedactionEntryService {
|
||||
|
||||
private final EntityFromPrecursorCreationService entityFromPrecursorCreationService;
|
||||
|
||||
|
||||
@Observed(name = "ImportedRedactionEntryService", contextualName = "add-imported-redaction-entries")
|
||||
public List<PrecursorEntity> addImportedEntriesAndReturnNotFoundEntries(AnalyzeRequest analyzeRequest, ImportedRedactions importedRedactions, Document document) {
|
||||
|
||||
List<PrecursorEntity> notFoundImportedEntries = Collections.emptyList();
|
||||
if (!importedRedactions.getImportedRedactions().isEmpty()) {
|
||||
notFoundImportedEntries = entityFromPrecursorCreationService.createEntitiesIfFoundAndReturnNotFoundEntries(importedRedactions, document);
|
||||
log.info("Added Imported entries for file {} in dossier {}", analyzeRequest.getFileId(), analyzeRequest.getDossierId());
|
||||
}
|
||||
|
||||
if (notFoundImportedEntries.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<BaseAnnotation> manualChanges = allManualChangesExceptAdd(analyzeRequest.getManualRedactions());
|
||||
for (PrecursorEntity notFoundManualRedactionEntry : notFoundImportedEntries) {
|
||||
manualChanges.stream()
|
||||
.filter(change -> change.getAnnotationId().equals(notFoundManualRedactionEntry.getId()))
|
||||
.forEach(change -> notFoundManualRedactionEntry.getManualOverwrite().addChange(change));
|
||||
}
|
||||
|
||||
return notFoundImportedEntries;
|
||||
}
|
||||
|
||||
|
||||
private List<BaseAnnotation> allManualChangesExceptAdd(ManualRedactions manualRedactions) {
|
||||
|
||||
return Stream.of(manualRedactions.getForceRedactions(),
|
||||
manualRedactions.getResizeRedactions(),
|
||||
manualRedactions.getRecategorizations(),
|
||||
manualRedactions.getIdsToRemove(),
|
||||
manualRedactions.getLegalBasisChanges()).flatMap(Collection::stream).map(baseAnnotation -> (BaseAnnotation) baseAnnotation).toList();
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package com.iqser.red.service.redaction.v1.server.service.document;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.TextRange;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.SemanticNode;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
public class IntersectingNodeVisitor implements NodeVisitor {
|
||||
|
||||
@Getter
|
||||
private Set<SemanticNode> intersectingNodes;
|
||||
private final TextRange textRange;
|
||||
|
||||
public IntersectingNodeVisitor(TextRange textRange) {
|
||||
|
||||
this.textRange = textRange;
|
||||
this.intersectingNodes = new HashSet<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(SemanticNode node) {
|
||||
|
||||
if (node.getTextRange().intersects(textRange)) {
|
||||
intersectingNodes.add(node);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
-249
@@ -1,249 +0,0 @@
|
||||
package com.iqser.red.service.redaction.v1.server.service.document;
|
||||
|
||||
import static java.lang.String.format;
|
||||
import static java.util.stream.Collectors.groupingBy;
|
||||
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualRedactionEntry;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLog;
|
||||
import com.iqser.red.service.redaction.v1.server.model.ManualEntity;
|
||||
import com.iqser.red.service.redaction.v1.server.model.RectangleWithPage;
|
||||
import com.iqser.red.service.redaction.v1.server.model.dictionary.SearchImplementation;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.TextRange;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.entity.EntityType;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.entity.PositionOnPage;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.entity.TextEntity;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.Page;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.SemanticNode;
|
||||
import com.iqser.red.service.redaction.v1.server.service.DictionaryService;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
|
||||
public class ManualEntityCreationService {
|
||||
|
||||
static double MATCH_THRESHOLD = 5; // Is compared to the sum of distances in pdf coordinates for each corner of the bounding box of the entities
|
||||
EntityCreationService entityCreationService;
|
||||
DictionaryService dictionaryService;
|
||||
|
||||
|
||||
@Autowired
|
||||
public ManualEntityCreationService(EntityEnrichmentService entityEnrichmentService, DictionaryService dictionaryService) {
|
||||
|
||||
entityCreationService = new EntityCreationService(entityEnrichmentService);
|
||||
this.dictionaryService = dictionaryService;
|
||||
}
|
||||
|
||||
|
||||
public List<ManualEntity> toRedactionEntity(RedactionLog redactionLog, SemanticNode node) {
|
||||
|
||||
List<ManualEntity> manualEntities = redactionLog.getRedactionLogEntry().stream().map(ManualEntity::fromRedactionLogEntry).peek(manualEntity -> {
|
||||
if (manualEntity.isApplied()) {
|
||||
manualEntity.apply(manualEntity.getRuleIdentifier(), manualEntity.getReason(), manualEntity.getLegalBasis());
|
||||
} else {
|
||||
manualEntity.skip(manualEntity.getRuleIdentifier(), manualEntity.getReason());
|
||||
}
|
||||
}).toList();
|
||||
return toRedactionEntity(manualEntities, node);
|
||||
}
|
||||
|
||||
|
||||
public List<ManualEntity> createRedactionEntitiesIfFoundAndReturnNotFoundEntries(Set<ManualRedactionEntry> manualRedactionEntries,
|
||||
SemanticNode node,
|
||||
String dossierTemplateId) {
|
||||
|
||||
List<ManualEntity> manualEntities = manualRedactionEntries.stream()
|
||||
.filter(manualRedactionEntry -> !(manualRedactionEntry.isAddToDictionary() || manualRedactionEntry.isAddToDossierDictionary()))
|
||||
.map(manualRedactionEntry -> ManualEntity.fromManualRedactionEntry(manualRedactionEntry,
|
||||
dictionaryService.isHint(manualRedactionEntry.getType(), dossierTemplateId)))
|
||||
.peek(manualEntity -> manualEntity.apply("MAN.5.0", "manual entries are applied by default", manualEntity.getLegalBasis()))
|
||||
.toList();
|
||||
|
||||
return toRedactionEntity(manualEntities, node);
|
||||
}
|
||||
|
||||
|
||||
private List<ManualEntity> toRedactionEntity(List<ManualEntity> manualEntities, SemanticNode node) {
|
||||
|
||||
Set<Integer> pageNumbers = manualEntities.stream().flatMap(entry -> entry.getEntityPosition().stream().map(RectangleWithPage::pageNumber)).collect(Collectors.toSet());
|
||||
Set<String> entryValues = manualEntities.stream().map(ManualEntity::getValue).map(String::toLowerCase).collect(Collectors.toSet());
|
||||
|
||||
Map<String, List<TextEntity>> tempEntitiesByValue = findAllPossibleEntitiesAndGroupByValue(node, pageNumbers, entryValues);
|
||||
|
||||
List<ManualEntity> notFoundManualEntities = new LinkedList<>();
|
||||
for (ManualEntity manualEntity : manualEntities) {
|
||||
Optional<TextEntity> optionalRedactionEntity = findClosestEntityAndReturnEmptyIfNotFound(manualEntity, tempEntitiesByValue);
|
||||
if (optionalRedactionEntity.isEmpty()) {
|
||||
notFoundManualEntities.add(manualEntity);
|
||||
continue;
|
||||
}
|
||||
createCorrectEntity(manualEntity, node, optionalRedactionEntity.get().getTextRange());
|
||||
}
|
||||
tempEntitiesByValue.values().stream().flatMap(Collection::stream).forEach(TextEntity::removeFromGraph);
|
||||
return notFoundManualEntities;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Deletes the temp Entity and creates a RedactionEntity with correct values, based on the given parameters.
|
||||
*
|
||||
* @param manualEntity The entity identifier for the RedactionEntity.
|
||||
* @param node The SemanticNode associated with the RedactionEntity.
|
||||
* @param closestTextRange The closest Boundary to the RedactionEntity.
|
||||
*/
|
||||
private void createCorrectEntity(ManualEntity manualEntity, SemanticNode node, TextRange closestTextRange) {
|
||||
|
||||
TextEntity correctEntity = entityCreationService.forceByTextRange(closestTextRange, manualEntity.getType(), manualEntity.getEntityType(), node);
|
||||
|
||||
correctEntity.addMatchedRules(manualEntity.getMatchedRuleList());
|
||||
correctEntity.setDictionaryEntry(manualEntity.isDictionaryEntry());
|
||||
correctEntity.setDossierDictionaryEntry(manualEntity.isDossierDictionaryEntry());
|
||||
correctEntity.getManualOverwrite().addChanges(manualEntity.getManualOverwrite().getManualChangeLog());
|
||||
|
||||
|
||||
// AnnotationIds must match the IDs in the add requests, or comments break. Maybe think about migrating IDs on the fly!
|
||||
List<PositionOnPage> redactionPositionsWithIdOfManualOnPage = new ArrayList<>(correctEntity.getPositionsOnPagePerPage().size());
|
||||
for (PositionOnPage positionOnPage : correctEntity.getPositionsOnPagePerPage()) {
|
||||
redactionPositionsWithIdOfManualOnPage.add(new PositionOnPage(manualEntity.getId(), positionOnPage.getPage(), positionOnPage.getRectanglePerLine()));
|
||||
}
|
||||
correctEntity.setPositionsOnPagePerPage(redactionPositionsWithIdOfManualOnPage);
|
||||
}
|
||||
|
||||
|
||||
private Optional<TextEntity> findClosestEntityAndReturnEmptyIfNotFound(ManualEntity identifier, Map<String, List<TextEntity>> entitiesWithSameValue) {
|
||||
|
||||
List<TextEntity> possibleEntities = entitiesWithSameValue.get(identifier.getValue().toLowerCase(Locale.ENGLISH));
|
||||
|
||||
if (entityIdentifierValueNotFound(possibleEntities)) {
|
||||
log.warn("Entity could not be created with identifier: {}, due to the value {} not being found anywhere.", identifier, identifier.getValue());
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
Optional<TextEntity> optionalClosestEntity = possibleEntities.stream()
|
||||
.filter(entity -> pagesMatch(entity, identifier.getEntityPosition()))
|
||||
.min(Comparator.comparingDouble(entity -> calculateMinDistance(identifier.getEntityPosition(), entity)));
|
||||
|
||||
if (optionalClosestEntity.isEmpty()) {
|
||||
log.warn("No Entity with value {} found on page {}", identifier.getValue(), identifier.getEntityPosition());
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
TextEntity closestEntity = optionalClosestEntity.get();
|
||||
double distance = calculateMinDistance(identifier.getEntityPosition(), closestEntity);
|
||||
if (distance > MATCH_THRESHOLD) {
|
||||
log.warn(format("Distance to closest found entity is %.2f and therefore higher than the threshold of %.2f for \n%s \n%s",
|
||||
distance,
|
||||
MATCH_THRESHOLD,
|
||||
identifier.getEntityPosition(),
|
||||
closestEntity.getPositionsOnPagePerPage()));
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(closestEntity);
|
||||
}
|
||||
|
||||
|
||||
private static boolean entityIdentifierValueNotFound(List<TextEntity> possibleEntities) {
|
||||
|
||||
return possibleEntities == null || possibleEntities.isEmpty();
|
||||
}
|
||||
|
||||
|
||||
private Map<String, List<TextEntity>> findAllPossibleEntitiesAndGroupByValue(SemanticNode node, Set<Integer> pageNumbers, Set<String> entryValues) {
|
||||
|
||||
if (!pageNumbers.stream().allMatch(node::onPage)) {
|
||||
throw new IllegalArgumentException(format("SemanticNode \"%s\" does not contain these pages %s, it has pages: %s",
|
||||
node,
|
||||
pageNumbers.stream().filter(pageNumber -> !node.onPage(pageNumber)).toList(),
|
||||
node.getPages()));
|
||||
}
|
||||
SearchImplementation searchImplementation = new SearchImplementation(entryValues, true);
|
||||
|
||||
return searchImplementation.getBoundaries(node.getTextBlock(), node.getTextRange())
|
||||
.stream().map(boundary -> entityCreationService.forceByTextRange(boundary, "temp", EntityType.ENTITY, node))
|
||||
.collect(groupingBy(entity -> entity.getValue().toLowerCase(Locale.ROOT)));
|
||||
}
|
||||
|
||||
|
||||
private static boolean allValuesFound(Map<String, List<TextEntity>> entitiesByValue, Set<String> entryValues) {
|
||||
|
||||
return entitiesByValue.keySet().equals(entryValues);
|
||||
}
|
||||
|
||||
|
||||
private static boolean pagesMatch(TextEntity entity, List<RectangleWithPage> originalPositions) {
|
||||
|
||||
Set<Integer> entityPageNumbers = entity.getPositionsOnPagePerPage().stream().map(PositionOnPage::getPage).map(Page::getNumber).collect(Collectors.toSet());
|
||||
Set<Integer> originalPageNumbers = originalPositions.stream().map(RectangleWithPage::pageNumber).collect(Collectors.toSet());
|
||||
return entityPageNumbers.containsAll(originalPageNumbers);
|
||||
}
|
||||
|
||||
|
||||
private double calculateMinDistance(List<RectangleWithPage> originalPositions, TextEntity entity) {
|
||||
|
||||
if (originalPositions.size() != countRectangles(entity)) {
|
||||
return Double.MAX_VALUE;
|
||||
}
|
||||
return originalPositions.stream()
|
||||
.mapToDouble(rectangleWithPage -> calculateMinDistancePerRectangle(entity, rectangleWithPage.pageNumber(), rectangleWithPage.rectangle2D()))
|
||||
.sum();
|
||||
}
|
||||
|
||||
|
||||
private static long countRectangles(TextEntity entity) {
|
||||
|
||||
return entity.getPositionsOnPagePerPage().stream().mapToLong(redactionPosition -> redactionPosition.getRectanglePerLine().size()).sum();
|
||||
}
|
||||
|
||||
|
||||
private double calculateMinDistancePerRectangle(TextEntity entity, int pageNumber, Rectangle2D originalRectangle) {
|
||||
|
||||
return entity.getPositionsOnPagePerPage()
|
||||
.stream()
|
||||
.filter(redactionPosition -> redactionPosition.getPage().getNumber() == pageNumber)
|
||||
.map(PositionOnPage::getRectanglePerLine)
|
||||
.flatMap(Collection::stream)
|
||||
.mapToDouble(rectangle -> calculateDistance(rectangle, originalRectangle))
|
||||
.min()
|
||||
.orElse(Double.MAX_VALUE);
|
||||
}
|
||||
|
||||
|
||||
private double calculateDistance(Rectangle2D rectangle, Rectangle2D rectangle2D) {
|
||||
|
||||
// mirrored coordinates safe comparison
|
||||
double minX1 = Math.min(rectangle.getMinX(), rectangle.getMaxX());
|
||||
double minY1 = Math.min(rectangle.getMinY(), rectangle.getMaxY());
|
||||
double maxX1 = Math.max(rectangle.getMinX(), rectangle.getMaxX());
|
||||
double maxY1 = Math.max(rectangle.getMinY(), rectangle.getMaxY());
|
||||
|
||||
double minX2 = Math.min(rectangle2D.getMinX(), rectangle2D.getMaxX());
|
||||
double minY2 = Math.min(rectangle2D.getMinY(), rectangle2D.getMaxY());
|
||||
double maxX2 = Math.max(rectangle2D.getMinX(), rectangle2D.getMaxX());
|
||||
double maxY2 = Math.max(rectangle2D.getMinY(), rectangle2D.getMaxY());
|
||||
|
||||
return Math.abs(minX1 - minX2) //
|
||||
+ Math.abs(minY1 - minY2) //
|
||||
+ Math.abs(maxX1 - maxX2) //
|
||||
+ Math.abs(maxY1 - maxY2);
|
||||
}
|
||||
|
||||
}
|
||||
+10
-7
@@ -11,8 +11,9 @@ import com.iqser.red.service.persistence.service.v1.api.shared.model.AnalyzeRequ
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.ManualRedactions;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.BaseAnnotation;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.Document;
|
||||
import com.iqser.red.service.redaction.v1.server.model.ManualEntity;
|
||||
import com.iqser.red.service.redaction.v1.server.model.PrecursorEntity;
|
||||
|
||||
import io.micrometer.observation.annotation.Observed;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@@ -21,22 +22,24 @@ import lombok.extern.slf4j.Slf4j;
|
||||
@RequiredArgsConstructor
|
||||
public class ManualRedactionEntryService {
|
||||
|
||||
private final ManualEntityCreationService manualEntityCreationService;
|
||||
private final EntityFromPrecursorCreationService entityFromPrecursorCreationService;
|
||||
|
||||
|
||||
public List<ManualEntity> addManualRedactionEntriesAndReturnNotFoundEntries(AnalyzeRequest analyzeRequest, Document document, String dossierTemplateId) {
|
||||
@Observed(name = "ManualRedactionEntryService", contextualName = "add-manual-redaction-entries")
|
||||
public List<PrecursorEntity> addManualRedactionEntriesAndReturnNotFoundEntries(AnalyzeRequest analyzeRequest, Document document, String dossierTemplateId) {
|
||||
|
||||
List<ManualEntity> notFoundManualRedactionEntries = Collections.emptyList();
|
||||
List<PrecursorEntity> notFoundManualRedactionEntries = Collections.emptyList();
|
||||
if (analyzeRequest.getManualRedactions() != null) {
|
||||
notFoundManualRedactionEntries = manualEntityCreationService.createRedactionEntitiesIfFoundAndReturnNotFoundEntries(analyzeRequest.getManualRedactions()
|
||||
.getEntriesToAdd(), document, dossierTemplateId);
|
||||
notFoundManualRedactionEntries = entityFromPrecursorCreationService.createEntitiesIfFoundAndReturnNotFoundEntries(analyzeRequest.getManualRedactions(),
|
||||
document, dossierTemplateId);
|
||||
log.info("Added Manual redaction entries for file {} in dossier {}", analyzeRequest.getFileId(), analyzeRequest.getDossierId());
|
||||
}
|
||||
|
||||
if (notFoundManualRedactionEntries.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<BaseAnnotation> manualChanges = allManualChangesExceptAdd(analyzeRequest.getManualRedactions());
|
||||
for (ManualEntity notFoundManualRedactionEntry : notFoundManualRedactionEntries) {
|
||||
for (PrecursorEntity notFoundManualRedactionEntry : notFoundManualRedactionEntries) {
|
||||
manualChanges.stream()
|
||||
.filter(change -> change.getAnnotationId().equals(notFoundManualRedactionEntry.getId()))
|
||||
.forEach(change -> notFoundManualRedactionEntry.getManualOverwrite().addChange(change));
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package com.iqser.red.service.redaction.v1.server.service.document;
|
||||
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.SemanticNode;
|
||||
|
||||
public interface NodeVisitor {
|
||||
|
||||
void visit(SemanticNode node);
|
||||
}
|
||||
+60
-18
@@ -13,6 +13,9 @@ import org.springframework.stereotype.Service;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.AnalyzeRequest;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntityLog;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntityLogEntry;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.Position;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.imported.ImportedRedaction;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.imported.ImportedRedactions;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.ManualRedactions;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.Rectangle;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.IdRemoval;
|
||||
@@ -40,7 +43,11 @@ import lombok.extern.slf4j.Slf4j;
|
||||
public class SectionFinderService {
|
||||
|
||||
@Timed("redactmanager_findSectionsToReanalyse")
|
||||
public Set<Integer> findSectionsToReanalyse(DictionaryIncrement dictionaryIncrement, EntityLog entityLog, Document document, AnalyzeRequest analyzeRequest) {
|
||||
public Set<Integer> findSectionsToReanalyse(DictionaryIncrement dictionaryIncrement,
|
||||
EntityLog entityLog,
|
||||
Document document,
|
||||
AnalyzeRequest analyzeRequest,
|
||||
ImportedRedactions importedRedactions) {
|
||||
|
||||
long start = System.currentTimeMillis();
|
||||
Set<String> relevantManuallyModifiedAnnotationIds = getRelevantManuallyModifiedAnnotationIds(analyzeRequest.getManualRedactions());
|
||||
@@ -50,20 +57,26 @@ public class SectionFinderService {
|
||||
if (entry.getContainingNodeId().isEmpty()) {
|
||||
continue; // Empty list means either Entity has not been found or it is between main sections. Thus, this might lead to wrong reanalysis.
|
||||
}
|
||||
sectionsToReanalyse.add(entry.getContainingNodeId().get(0));
|
||||
sectionsToReanalyse.add(entry.getContainingNodeId()
|
||||
.get(0));
|
||||
}
|
||||
}
|
||||
|
||||
var dictionaryIncrementsSearch = new SearchImplementation(dictionaryIncrement.getValues().stream().map(DictionaryIncrementValue::getValue).collect(Collectors.toList()),
|
||||
true);
|
||||
var dictionaryIncrementsSearch = new SearchImplementation(dictionaryIncrement.getValues()
|
||||
.stream()
|
||||
.map(DictionaryIncrementValue::getValue)
|
||||
.toList(), true);
|
||||
|
||||
document.streamChildren().forEach(mainNode -> {
|
||||
if (dictionaryIncrementsSearch.atLeastOneMatches(mainNode.getTextBlock().getSearchText())) {
|
||||
sectionsToReanalyse.add(mainNode.getTreeId().get(0));
|
||||
}
|
||||
});
|
||||
document.streamChildren()
|
||||
.forEach(mainNode -> {
|
||||
if (dictionaryIncrementsSearch.atLeastOneMatches(mainNode.getTextBlock().getSearchText())) {
|
||||
sectionsToReanalyse.add(mainNode.getTreeId()
|
||||
.get(0));
|
||||
}
|
||||
});
|
||||
|
||||
Set<Integer> relevantPagesForReanalysis = getRelevantPageNumbersForAddRedactions(analyzeRequest);
|
||||
relevantPagesForReanalysis.addAll(getRelevantPageNumbersForImportedRedactions(importedRedactions, relevantManuallyModifiedAnnotationIds));
|
||||
if (!relevantPagesForReanalysis.isEmpty()) {
|
||||
sectionsToReanalyse.addAll(getSectionNumbersOnPages(document, relevantPagesForReanalysis));
|
||||
}
|
||||
@@ -79,8 +92,11 @@ public class SectionFinderService {
|
||||
return document.getPages()
|
||||
.stream()
|
||||
.filter(page -> relevantPagesForReanalysis.contains(page.getNumber()))
|
||||
.flatMap(page -> Stream.concat(page.getMainBody().stream().filter(node -> node.getType().equals(NodeType.SECTION)), Stream.of(page.getHeader(), page.getFooter())))
|
||||
.map(node -> node.getTreeId().get(0))
|
||||
.flatMap(page -> Stream.concat(page.getMainBody()
|
||||
.stream()
|
||||
.filter(node -> node.getType().equals(NodeType.SECTION)), Stream.of(page.getHeader(), page.getFooter())))
|
||||
.map(node -> node.getTreeId()
|
||||
.get(0))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@@ -91,8 +107,7 @@ public class SectionFinderService {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
return analyzeRequest.getManualRedactions()
|
||||
.getEntriesToAdd()
|
||||
return analyzeRequest.getManualRedactions().getEntriesToAdd()
|
||||
.stream()
|
||||
.filter(addRedaction -> !(addRedaction.isAddToDictionary() || addRedaction.isAddToDossierDictionary()))
|
||||
.map(ManualRedactionEntry::getPositions)
|
||||
@@ -102,17 +117,44 @@ public class SectionFinderService {
|
||||
}
|
||||
|
||||
|
||||
private static Set<Integer> getRelevantPageNumbersForImportedRedactions(ImportedRedactions importedRedactions, Set<String> idsWithUnprocessedManualChanges) {
|
||||
|
||||
if (importedRedactions == null || importedRedactions.getImportedRedactions() == null) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
return importedRedactions.getImportedRedactions()
|
||||
.stream()
|
||||
.filter(importedRedaction -> idsWithUnprocessedManualChanges.contains(importedRedaction.getId()))
|
||||
.map(ImportedRedaction::getPositions)
|
||||
.flatMap(Collection::stream)
|
||||
.map(Position::getPageNumber)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
|
||||
private static Set<String> getRelevantManuallyModifiedAnnotationIds(ManualRedactions manualRedactions) {
|
||||
|
||||
if (manualRedactions == null) {
|
||||
return new HashSet<>();
|
||||
}
|
||||
|
||||
return Stream.concat(manualRedactions.getResizeRedactions().stream().map(ManualResizeRedaction::getAnnotationId),
|
||||
Stream.concat(manualRedactions.getLegalBasisChanges().stream().map(ManualLegalBasisChange::getAnnotationId),
|
||||
Stream.concat(manualRedactions.getRecategorizations().stream().map(ManualRecategorization::getAnnotationId),
|
||||
Stream.concat(manualRedactions.getIdsToRemove().stream().map(IdRemoval::getAnnotationId),
|
||||
manualRedactions.getForceRedactions().stream().map(ManualForceRedaction::getAnnotationId))))).collect(Collectors.toSet());
|
||||
return Stream.concat(manualRedactions.getResizeRedactions()
|
||||
.stream()
|
||||
.map(ManualResizeRedaction::getAnnotationId),
|
||||
Stream.concat(manualRedactions.getLegalBasisChanges()
|
||||
.stream()
|
||||
.map(ManualLegalBasisChange::getAnnotationId),
|
||||
Stream.concat(manualRedactions.getRecategorizations()
|
||||
.stream()
|
||||
.map(ManualRecategorization::getAnnotationId),
|
||||
Stream.concat(manualRedactions.getIdsToRemove()
|
||||
.stream()
|
||||
.map(IdRemoval::getAnnotationId),
|
||||
manualRedactions.getForceRedactions()
|
||||
.stream()
|
||||
.map(ManualForceRedaction::getAnnotationId)))))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+141
-35
@@ -1,98 +1,198 @@
|
||||
package com.iqser.red.service.redaction.v1.server.service.drools;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.drools.drl.parser.DroolsParserException;
|
||||
import org.kie.api.builder.KieBuilder;
|
||||
import org.kie.api.builder.Message;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.google.common.collect.Sets;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.RuleFileType;
|
||||
import com.iqser.red.service.redaction.v1.model.DroolsSyntaxDeprecatedWarnings;
|
||||
import com.iqser.red.service.redaction.v1.model.DroolsSyntaxErrorMessage;
|
||||
import com.iqser.red.service.redaction.v1.model.DroolsSyntaxValidation;
|
||||
import com.iqser.red.service.redaction.v1.model.RuleValidationModel;
|
||||
import com.iqser.red.service.redaction.v1.server.DeprecatedElementsFinder;
|
||||
import com.iqser.red.service.redaction.v1.server.model.dictionary.SearchImplementation;
|
||||
import com.iqser.red.service.redaction.v1.server.model.drools.BasicQuery;
|
||||
import com.iqser.red.service.redaction.v1.server.model.drools.BasicRule;
|
||||
import com.iqser.red.service.redaction.v1.server.model.drools.RuleClass;
|
||||
import com.iqser.red.service.redaction.v1.server.model.drools.RuleFileBluePrint;
|
||||
import com.iqser.red.service.redaction.v1.server.model.drools.RuleUnit;
|
||||
import com.iqser.red.service.redaction.v1.server.storage.RuleManagementResources;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class DroolsSyntaxValidationService {
|
||||
|
||||
private static final Pattern allowedImportsPattern = Pattern.compile("^(?:import\\s+static\\s+)?(?:import\\s+)?(?:com\\.knecon\\.fforesight|com\\.iqser\\.red)\\..*;$");
|
||||
|
||||
private final KieContainerCreationService kieContainerCreationService;
|
||||
|
||||
private final DeprecatedElementsFinder deprecatedElementsFinder;
|
||||
|
||||
|
||||
@SneakyThrows
|
||||
public DroolsSyntaxValidation testRules(RuleValidationModel rules) {
|
||||
|
||||
DroolsSyntaxValidation customDroolsSyntaxValidation = buildCustomDroolsSyntaxValidation(rules.getRulesString(), RuleFileType.valueOf(rules.getRuleFileType()));
|
||||
DroolsSyntaxValidation customDroolsSyntaxValidation;
|
||||
try {
|
||||
customDroolsSyntaxValidation = buildCustomDroolsSyntaxValidation(rules.getRulesString(), RuleFileType.valueOf(rules.getRuleFileType()));
|
||||
} catch (DroolsParserException e) {
|
||||
// this means the parser could not parse the file at all. In this case use drools compiler only as it will return useful error messages.
|
||||
customDroolsSyntaxValidation = new DroolsSyntaxValidation();
|
||||
}
|
||||
DroolsSyntaxValidation droolsCompilerSyntaxValidation = buildDroolsCompilerSyntaxValidation(rules);
|
||||
droolsCompilerSyntaxValidation.getDroolsSyntaxErrorMessages().addAll(customDroolsSyntaxValidation.getDroolsSyntaxErrorMessages());
|
||||
droolsCompilerSyntaxValidation.getDroolsSyntaxDeprecatedWarnings().addAll(customDroolsSyntaxValidation.getDroolsSyntaxDeprecatedWarnings());
|
||||
return droolsCompilerSyntaxValidation;
|
||||
}
|
||||
|
||||
private DroolsSyntaxDeprecatedWarnings getWarningsForDeprecatedImports(RuleFileBluePrint ruleFileBluePrint) {
|
||||
|
||||
private DroolsSyntaxValidation buildCustomDroolsSyntaxValidation(String ruleString, RuleFileType ruleFileType) {
|
||||
if (!deprecatedElementsFinder.getDeprecatedClasses().isEmpty()) {
|
||||
String imports = ruleFileBluePrint.getImports();
|
||||
SearchImplementation classesSearchImplementation = deprecatedElementsFinder.getClassesSearchImplementation();
|
||||
List<SearchImplementation.MatchPosition> matches = classesSearchImplementation.getMatches(imports);
|
||||
if (!matches.isEmpty()) {
|
||||
String sb = "Following imports are deprecated: \n" + matches.stream()
|
||||
.map(m -> imports.substring(m.startIndex(), m.endIndex()))
|
||||
.collect(Collectors.joining("\n"));
|
||||
return DroolsSyntaxDeprecatedWarnings.builder().line(ruleFileBluePrint.getImportLine()).column(0).message(sb)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<DroolsSyntaxDeprecatedWarnings> getWarningsForDeprecatedRules(RuleFileBluePrint ruleFileBluePrint) {
|
||||
|
||||
List<DroolsSyntaxDeprecatedWarnings> warningMessages = new ArrayList<>();
|
||||
|
||||
// checks the rules for deprecateMethods
|
||||
if (!deprecatedElementsFinder.getDeprecatedMethods().isEmpty()) {
|
||||
SearchImplementation methodsSearchImplementation = deprecatedElementsFinder.getMethodsSearchImplementation();
|
||||
Map<String, String> deprecatedMethodsSignatureMap = deprecatedElementsFinder.getDeprecatedMethodsSignaturesMap();
|
||||
|
||||
for (RuleClass ruleClass : ruleFileBluePrint.getRuleClasses()) {
|
||||
for (RuleUnit ruleUnit : ruleClass.ruleUnits()) {
|
||||
for (BasicRule basicRule : ruleUnit.rules()) {
|
||||
List<SearchImplementation.MatchPosition> matches = methodsSearchImplementation.getMatches(basicRule.getCode());
|
||||
if (!matches.isEmpty()) {
|
||||
String warningMessage = matches.stream()
|
||||
.map(m -> basicRule.getCode().substring(m.startIndex(), m.endIndex()))
|
||||
.distinct()
|
||||
.map(dm -> String.format("Method %s might be deprecated because of \n %s \n", dm, deprecatedMethodsSignatureMap.get(dm)))
|
||||
.collect(Collectors.joining("\n"));
|
||||
warningMessages.add(DroolsSyntaxDeprecatedWarnings.builder().line(basicRule.getLine()).column(0).message(warningMessage)
|
||||
.build());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return warningMessages.stream()
|
||||
.sorted(Comparator.comparingInt(DroolsSyntaxDeprecatedWarnings::getLine))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
private DroolsSyntaxValidation buildCustomDroolsSyntaxValidation(String ruleString, RuleFileType ruleFileType) throws DroolsParserException {
|
||||
|
||||
RuleFileBluePrint ruleFileBluePrint = RuleFileParser.buildBluePrintFromRulesString(ruleString);
|
||||
|
||||
DroolsSyntaxValidation customSyntaxValidation = ruleFileBluePrint.getDroolsSyntaxValidation();
|
||||
|
||||
// find deprecated elements in the ruleFileBluePrint
|
||||
DroolsSyntaxDeprecatedWarnings warningMessageForImports = getWarningsForDeprecatedImports(ruleFileBluePrint);
|
||||
if (warningMessageForImports != null) {
|
||||
customSyntaxValidation.getDroolsSyntaxDeprecatedWarnings().add(warningMessageForImports);
|
||||
}
|
||||
customSyntaxValidation.getDroolsSyntaxDeprecatedWarnings().addAll(getWarningsForDeprecatedRules(ruleFileBluePrint));
|
||||
|
||||
RuleFileBluePrint baseRuleFileBluePrint = switch (ruleFileType) {
|
||||
case ENTITY -> RuleFileParser.buildBluePrintFromRulesString(RuleManagementResources.getBaseRuleFileString());
|
||||
case COMPONENT -> RuleFileParser.buildBluePrintFromRulesString(RuleManagementResources.getBaseComponentRuleFileString());
|
||||
};
|
||||
|
||||
if (!ruleFileBluePrint.getImports().equals(baseRuleFileBluePrint.getImports())) {
|
||||
if (!importsAreValid(baseRuleFileBluePrint, ruleFileBluePrint)) {
|
||||
customSyntaxValidation.getDroolsSyntaxErrorMessages()
|
||||
.add(DroolsSyntaxErrorMessage.builder()
|
||||
.line(ruleFileBluePrint.getImportLine())
|
||||
.column(0)
|
||||
.message(String.format("Changing the imports is not allowed! Must be: %n%s", baseRuleFileBluePrint.getImports()))
|
||||
.build());
|
||||
.line(ruleFileBluePrint.getImportLine())
|
||||
.column(0)
|
||||
.message(String.format("Changing the imports is not allowed! Must be: %n%s", baseRuleFileBluePrint.getImports()))
|
||||
.build());
|
||||
}
|
||||
if (!ruleFileBluePrint.getGlobals().equals(baseRuleFileBluePrint.getGlobals())) {
|
||||
customSyntaxValidation.getDroolsSyntaxErrorMessages()
|
||||
.add(DroolsSyntaxErrorMessage.builder().line(ruleFileBluePrint.getGlobalsLine())
|
||||
.column(0).message(String.format("Changing the globals is not allowed! Must be: %n%s", baseRuleFileBluePrint.getGlobals()))
|
||||
.build());
|
||||
.add(DroolsSyntaxErrorMessage.builder()
|
||||
.line(ruleFileBluePrint.getGlobalsLine())
|
||||
.column(0)
|
||||
.message(String.format("Changing the globals is not allowed! Must be: %n%s", baseRuleFileBluePrint.getGlobals()))
|
||||
.build());
|
||||
}
|
||||
baseRuleFileBluePrint.getQueries().forEach(basicQuery -> {
|
||||
if (!validateQueryIsPresent(basicQuery, ruleFileBluePrint)) {
|
||||
baseRuleFileBluePrint.getQueries()
|
||||
.forEach(basicQuery -> {
|
||||
if (!validateQueryIsPresent(basicQuery, ruleFileBluePrint)) {
|
||||
customSyntaxValidation.getDroolsSyntaxErrorMessages()
|
||||
.add(DroolsSyntaxErrorMessage.builder()
|
||||
.line(basicQuery.getLine())
|
||||
.column(0)
|
||||
.message(String.format("Changing or removing the query %s is not allowed! Must be: %n%s", basicQuery.getName(), basicQuery.getCode()))
|
||||
.build());
|
||||
}
|
||||
});
|
||||
if (ruleFileType.equals(RuleFileType.ENTITY)) {
|
||||
String requiredAgendaGroup = "LOCAL_DICTIONARY_ADDS";
|
||||
if (!validateAgendaGroupIsPresent(ruleFileBluePrint, requiredAgendaGroup)) {
|
||||
customSyntaxValidation.getDroolsSyntaxErrorMessages()
|
||||
.add(DroolsSyntaxErrorMessage.builder()
|
||||
.line(basicQuery.getLine())
|
||||
.column(0)
|
||||
.message(String.format("Changing or removing the query %s is not allowed! Must be: %n%s", basicQuery.getName(), basicQuery.getCode()))
|
||||
.build());
|
||||
.add(DroolsSyntaxErrorMessage.builder().line(0).column(0).message(String.format("At least one rule with Agenda-Group '%s' required!", requiredAgendaGroup))
|
||||
.build());
|
||||
}
|
||||
});
|
||||
baseRuleFileBluePrint.streamAllRules().forEach(basicRule -> {
|
||||
if (!validateRuleIsPresent(basicRule, ruleFileBluePrint)) {
|
||||
customSyntaxValidation.getDroolsSyntaxErrorMessages()
|
||||
.add(DroolsSyntaxErrorMessage.builder()
|
||||
.line(basicRule.getLine())
|
||||
.column(0)
|
||||
.message(String.format("Changing or removing the rule %s is not allowed! Must be: %n%s", basicRule.getName(), basicRule.getCode()))
|
||||
.build());
|
||||
}
|
||||
});
|
||||
}
|
||||
return customSyntaxValidation;
|
||||
}
|
||||
|
||||
|
||||
private static boolean validateRuleIsPresent(BasicRule basicRule, RuleFileBluePrint ruleFileBluePrint) {
|
||||
private boolean validateAgendaGroupIsPresent(RuleFileBluePrint ruleFileBluePrint, String agendaGroupName) {
|
||||
|
||||
return ruleFileBluePrint.findRulesByIdentifier(basicRule.getIdentifier()).stream().anyMatch(otherRule -> otherRule.getCode().equals(basicRule.getCode()));
|
||||
return ruleFileBluePrint.streamAllRules()
|
||||
.anyMatch(basicRule -> basicRule.getAgendaGroup().equals(agendaGroupName));
|
||||
}
|
||||
|
||||
|
||||
private boolean importsAreValid(RuleFileBluePrint baseRuleFileBluePrint, RuleFileBluePrint ruleFileBluePrint) {
|
||||
|
||||
// imports may shrink, but not add anything new!
|
||||
Set<String> baseImports = baseRuleFileBluePrint.getImportSplitByKeyword();
|
||||
Set<String> imports = ruleFileBluePrint.getImportSplitByKeyword();
|
||||
|
||||
Set<String> additionalImports = Sets.difference(imports, baseImports);
|
||||
|
||||
return additionalImports.stream()
|
||||
.allMatch(additionalImport -> allowedImportsPattern.matcher(additionalImport).matches());
|
||||
}
|
||||
|
||||
|
||||
private static boolean validateQueryIsPresent(BasicQuery queryToCheckFor, RuleFileBluePrint ruleFileBluePrint) {
|
||||
|
||||
return ruleFileBluePrint.getQueries().stream().anyMatch(query -> query.getName().equals(queryToCheckFor.getName()) && query.getCode().equals(queryToCheckFor.getCode()));
|
||||
return ruleFileBluePrint.getQueries()
|
||||
.stream()
|
||||
.anyMatch(query -> query.getName().equals(queryToCheckFor.getName()) && query.getCode().equals(queryToCheckFor.getCode()));
|
||||
}
|
||||
|
||||
|
||||
@@ -101,7 +201,9 @@ public class DroolsSyntaxValidationService {
|
||||
var versionId = System.currentTimeMillis();
|
||||
var testRules = "test-rules";
|
||||
KieBuilder kieBuilder = kieContainerCreationService.registerNewKieContainerVersion(testRules,
|
||||
versionId, rules.getRulesString(), RuleFileType.valueOf(rules.getRuleFileType()));
|
||||
versionId,
|
||||
rules.getRulesString(),
|
||||
RuleFileType.valueOf(rules.getRuleFileType()));
|
||||
return buildDroolsCompilerSyntaxValidation(kieBuilder);
|
||||
}
|
||||
|
||||
@@ -109,14 +211,18 @@ public class DroolsSyntaxValidationService {
|
||||
private DroolsSyntaxValidation buildDroolsCompilerSyntaxValidation(KieBuilder kieBuilder) {
|
||||
|
||||
List<Message> errorMessages = kieBuilder.getResults().getMessages(Message.Level.ERROR);
|
||||
List<DroolsSyntaxErrorMessage> droolsSyntaxErrorMessages = errorMessages.stream().map(this::buildDroolsSyntaxErrorMessage).collect(Collectors.toList());
|
||||
return DroolsSyntaxValidation.builder().droolsSyntaxErrorMessages(droolsSyntaxErrorMessages).build();
|
||||
List<DroolsSyntaxErrorMessage> droolsSyntaxErrorMessages = errorMessages.stream()
|
||||
.map(this::buildDroolsSyntaxErrorMessage)
|
||||
.collect(Collectors.toList());
|
||||
return DroolsSyntaxValidation.builder().droolsSyntaxErrorMessages(droolsSyntaxErrorMessages)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
private DroolsSyntaxErrorMessage buildDroolsSyntaxErrorMessage(Message message) {
|
||||
|
||||
return DroolsSyntaxErrorMessage.builder().line(message.getLine()).column(message.getColumn()).message(message.getText()).build();
|
||||
return DroolsSyntaxErrorMessage.builder().line(message.getLine()).column(message.getColumn()).message(message.getText())
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+20
-6
@@ -7,8 +7,6 @@ import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.RuleFileType;
|
||||
import com.iqser.red.service.redaction.v1.server.utils.exception.DroolsTimeoutException;
|
||||
import org.kie.api.runtime.KieContainer;
|
||||
import org.kie.api.runtime.KieSession;
|
||||
import org.kie.api.runtime.rule.QueryResults;
|
||||
@@ -16,6 +14,7 @@ import org.kie.api.runtime.rule.QueryResultsRow;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.FileAttribute;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.RuleFileType;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.ManualRedactions;
|
||||
import com.iqser.red.service.redaction.v1.server.RedactionServiceSettings;
|
||||
import com.iqser.red.service.redaction.v1.server.model.NerEntities;
|
||||
@@ -25,8 +24,11 @@ import com.iqser.red.service.redaction.v1.server.model.document.nodes.SemanticNo
|
||||
import com.iqser.red.service.redaction.v1.server.service.ManualChangesApplicationService;
|
||||
import com.iqser.red.service.redaction.v1.server.service.document.EntityCreationService;
|
||||
import com.iqser.red.service.redaction.v1.server.service.document.EntityEnrichmentService;
|
||||
import com.iqser.red.service.redaction.v1.server.utils.exception.DroolsTimeoutException;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
import io.micrometer.observation.annotation.Observed;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
@@ -39,11 +41,13 @@ import lombok.extern.slf4j.Slf4j;
|
||||
public class EntityDroolsExecutionService {
|
||||
|
||||
EntityEnrichmentService entityEnrichmentService;
|
||||
|
||||
ObservationRegistry observationRegistry;
|
||||
ManualChangesApplicationService manualChangesApplicationService;
|
||||
RedactionServiceSettings settings;
|
||||
|
||||
|
||||
@Timed("redactmanager_executeRules")
|
||||
@Observed(name = "EntityDroolsExecutionService", contextualName = "execute-entity-rules")
|
||||
public List<FileAttribute> executeRules(KieContainer kieContainer,
|
||||
Document document,
|
||||
Dictionary dictionary,
|
||||
@@ -56,6 +60,7 @@ public class EntityDroolsExecutionService {
|
||||
|
||||
|
||||
@Timed("redactmanager_executeRules")
|
||||
@Observed(name = "EntityDroolsExecutionService", contextualName = "execute-entity-rules")
|
||||
public List<FileAttribute> executeRules(KieContainer kieContainer,
|
||||
Document document,
|
||||
List<SemanticNode> sectionsToAnalyze,
|
||||
@@ -64,9 +69,10 @@ public class EntityDroolsExecutionService {
|
||||
ManualRedactions manualRedactions,
|
||||
NerEntities nerEntities) {
|
||||
|
||||
addNumberOfPagesAndSectionsToAnalyseToTrace(document.getNumberOfPages(), sectionsToAnalyze.size());
|
||||
|
||||
KieSession kieSession = kieContainer.newKieSession();
|
||||
EntityCreationService entityCreationService = new EntityCreationService(entityEnrichmentService, kieSession);
|
||||
ManualChangesApplicationService manualChangesApplicationService = new ManualChangesApplicationService(entityCreationService);
|
||||
|
||||
kieSession.setGlobal("document", document);
|
||||
kieSession.setGlobal("entityCreationService", entityCreationService);
|
||||
@@ -93,7 +99,6 @@ public class EntityDroolsExecutionService {
|
||||
|
||||
kieSession.getAgenda().getAgendaGroup("LOCAL_DICTIONARY_ADDS").setFocus();
|
||||
|
||||
|
||||
CompletableFuture<Void> completableFuture = CompletableFuture.supplyAsync(() -> {
|
||||
kieSession.fireAllRules();
|
||||
return null;
|
||||
@@ -103,7 +108,7 @@ public class EntityDroolsExecutionService {
|
||||
completableFuture.orTimeout(settings.getDroolsExecutionTimeoutSecs(), TimeUnit.SECONDS).get();
|
||||
} catch (ExecutionException e) {
|
||||
kieSession.dispose();
|
||||
if(e.getCause() instanceof TimeoutException){
|
||||
if (e.getCause() instanceof TimeoutException) {
|
||||
throw new DroolsTimeoutException(e, false, RuleFileType.ENTITY);
|
||||
}
|
||||
throw new RuntimeException(e);
|
||||
@@ -128,4 +133,13 @@ public class EntityDroolsExecutionService {
|
||||
return fileAttributes;
|
||||
}
|
||||
|
||||
|
||||
private void addNumberOfPagesAndSectionsToAnalyseToTrace(int numberOfPages, int sectionsToAnalyse) {
|
||||
|
||||
if (observationRegistry.getCurrentObservation() != null) {
|
||||
observationRegistry.getCurrentObservation().highCardinalityKeyValue("numberOfPages", String.valueOf(numberOfPages));
|
||||
observationRegistry.getCurrentObservation().highCardinalityKeyValue("sectionsToAnalyse", String.valueOf(sectionsToAnalyse));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+3
@@ -20,6 +20,7 @@ import com.iqser.red.service.redaction.v1.server.utils.exception.DroolsTimeoutEx
|
||||
import com.knecon.fforesight.tenantcommons.TenantContext;
|
||||
|
||||
import feign.FeignException;
|
||||
import io.micrometer.observation.annotation.Observed;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Service
|
||||
@@ -29,6 +30,8 @@ public class KieContainerCreationService {
|
||||
private final RulesClient rulesClient;
|
||||
|
||||
|
||||
@Observed(name = "KieContainerCreationService",
|
||||
contextualName = "get-kie-container")
|
||||
public KieWrapper getLatestKieContainer(String dossierTemplateId, RuleFileType ruleFileType) {
|
||||
|
||||
try {
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.iqser.red.service.redaction.v1.server.storage;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.DocumentData;
|
||||
|
||||
import io.micrometer.observation.annotation.Observed;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ObservedStorageService {
|
||||
|
||||
private final RedactionStorageService redactionStorageService;
|
||||
|
||||
|
||||
@Observed(name = "RedactionStorageService", contextualName = "get-document-data")
|
||||
public DocumentData getDocumentData(String dossierId, String fileId) {
|
||||
return redactionStorageService.getDocumentData(dossierId, fileId);
|
||||
}
|
||||
|
||||
}
|
||||
+64
-22
@@ -3,18 +3,21 @@ package com.iqser.red.service.redaction.v1.server.storage;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.componentlog.ComponentLog;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntityLog;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.imported.ImportedRedactions;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.imported.ImportedRedactionsPerPage;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.file.FileType;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLog;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.imported.ImportedRedactions;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.section.SectionGrid;
|
||||
import com.iqser.red.service.redaction.v1.server.client.model.NerEntitiesModel;
|
||||
import com.iqser.red.service.redaction.v1.server.utils.exception.NotFoundException;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.DocumentData;
|
||||
import com.iqser.red.service.redaction.v1.server.utils.exception.NotFoundException;
|
||||
import com.iqser.red.storage.commons.exception.StorageObjectDoesNotExist;
|
||||
import com.iqser.red.storage.commons.service.StorageService;
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.DocumentPage;
|
||||
@@ -75,10 +78,26 @@ public class RedactionStorageService {
|
||||
@Timed("redactmanager_getImportedRedactions")
|
||||
public ImportedRedactions getImportedRedactions(String dossierId, String fileId) {
|
||||
|
||||
try {
|
||||
ImportedRedactionsPerPage importedRedactionsPerPage = storageService.readJSONObject(TenantContext.getTenantId(),
|
||||
StorageIdUtils.getStorageId(dossierId, fileId, FileType.IMPORTED_REDACTIONS),
|
||||
ImportedRedactionsPerPage.class);
|
||||
return new ImportedRedactions(importedRedactionsPerPage.getImportedRedactions().values().stream().flatMap(List::stream).collect(Collectors.toList()));
|
||||
} catch (StorageObjectDoesNotExist e) {
|
||||
log.debug("Imported redactions not available.");
|
||||
return new ImportedRedactions();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Timed("redactmanager_getImportedRedactions")
|
||||
public ImportedRedactionsPerPage getImportedRedactionsPerPage(String dossierId, String fileId) {
|
||||
|
||||
try {
|
||||
return storageService.readJSONObject(TenantContext.getTenantId(),
|
||||
StorageIdUtils.getStorageId(dossierId, fileId, FileType.IMPORTED_REDACTIONS),
|
||||
ImportedRedactions.class);
|
||||
ImportedRedactionsPerPage.class);
|
||||
} catch (StorageObjectDoesNotExist e) {
|
||||
log.debug("Imported redactions not available.");
|
||||
return null;
|
||||
@@ -86,11 +105,19 @@ public class RedactionStorageService {
|
||||
}
|
||||
|
||||
|
||||
@Deprecated(forRemoval = true)
|
||||
@Timed("redactmanager_getRedactionLog")
|
||||
public RedactionLog getRedactionLog(String dossierId, String fileId) {
|
||||
|
||||
try {
|
||||
return storageService.readJSONObject(TenantContext.getTenantId(), StorageIdUtils.getStorageId(dossierId, fileId, FileType.REDACTION_LOG), RedactionLog.class);
|
||||
RedactionLog redactionLog = storageService.readJSONObject(TenantContext.getTenantId(),
|
||||
StorageIdUtils.getStorageId(dossierId, fileId, FileType.REDACTION_LOG),
|
||||
RedactionLog.class);
|
||||
redactionLog.setRedactionLogEntry(redactionLog.getRedactionLogEntry()
|
||||
.stream()
|
||||
.filter(entry -> !(entry.getPositions() == null || entry.getPositions().isEmpty()))
|
||||
.collect(Collectors.toList()));
|
||||
return redactionLog;
|
||||
} catch (StorageObjectDoesNotExist e) {
|
||||
log.debug("RedactionLog not available.");
|
||||
return null;
|
||||
@@ -98,28 +125,48 @@ public class RedactionStorageService {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Timed("redactmanager_getRedactionLog")
|
||||
public EntityLog getEntityLog(String dossierId, String fileId) {
|
||||
|
||||
try {
|
||||
return storageService.readJSONObject(TenantContext.getTenantId(), StorageIdUtils.getStorageId(dossierId, fileId, FileType.ENTITY_LOG), EntityLog.class);
|
||||
EntityLog entityLog = storageService.readJSONObject(TenantContext.getTenantId(), StorageIdUtils.getStorageId(dossierId, fileId, FileType.ENTITY_LOG), EntityLog.class);
|
||||
entityLog.setEntityLogEntry(entityLog.getEntityLogEntry()
|
||||
.stream()
|
||||
.filter(entry -> !(entry.getPositions() == null || entry.getPositions().isEmpty()))
|
||||
.collect(Collectors.toList()));
|
||||
return entityLog;
|
||||
} catch (StorageObjectDoesNotExist e) {
|
||||
log.debug("RedactionLog not available.");
|
||||
log.debug("EntityLog not available.");
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// !Warning! before activating redis cache you need to set
|
||||
// -Dio.netty.noPreferDirect=true -XX:MaxDirectMemorySize=1000M
|
||||
// Jvm args to the largest document data size we want to process. for 4443 pages file that was 500mb.
|
||||
// And the cache eviction logic when a file changes after e.g. ocr is not implemented yet.
|
||||
// See https://knecon.atlassian.net/jira/software/c/projects/RED/boards/37?selectedIssue=RED-8106.
|
||||
@Timed("redactmanager_getDocumentGraph")
|
||||
@Cacheable(value = "documentDataCache")
|
||||
public DocumentData getDocumentData(String dossierId, String fileId) {
|
||||
|
||||
try {
|
||||
return DocumentData.builder()
|
||||
.documentStructure(storageService.readJSONObject(TenantContext.getTenantId(), StorageIdUtils.getStorageId(dossierId, fileId, FileType.DOCUMENT_STRUCTURE), DocumentStructure.class))
|
||||
.documentTextData(storageService.readJSONObject(TenantContext.getTenantId(), StorageIdUtils.getStorageId(dossierId, fileId, FileType.DOCUMENT_TEXT), DocumentTextData[].class))
|
||||
.documentPositionData(storageService.readJSONObject(TenantContext.getTenantId(), StorageIdUtils.getStorageId(dossierId, fileId, FileType.DOCUMENT_POSITION), DocumentPositionData[].class))
|
||||
.documentPages(storageService.readJSONObject(TenantContext.getTenantId(), StorageIdUtils.getStorageId(dossierId, fileId, FileType.DOCUMENT_PAGES), DocumentPage[].class))
|
||||
.documentStructure(storageService.readJSONObject(TenantContext.getTenantId(),
|
||||
StorageIdUtils.getStorageId(dossierId, fileId, FileType.DOCUMENT_STRUCTURE),
|
||||
DocumentStructure.class))
|
||||
.documentTextData(storageService.readJSONObject(TenantContext.getTenantId(),
|
||||
StorageIdUtils.getStorageId(dossierId, fileId, FileType.DOCUMENT_TEXT),
|
||||
DocumentTextData[].class))
|
||||
.documentPositionData(storageService.readJSONObject(TenantContext.getTenantId(),
|
||||
StorageIdUtils.getStorageId(dossierId, fileId, FileType.DOCUMENT_POSITION),
|
||||
DocumentPositionData[].class))
|
||||
.documentPages(storageService.readJSONObject(TenantContext.getTenantId(),
|
||||
StorageIdUtils.getStorageId(dossierId, fileId, FileType.DOCUMENT_PAGES),
|
||||
DocumentPage[].class))
|
||||
.build();
|
||||
} catch (StorageObjectDoesNotExist e) {
|
||||
log.debug("DocumentData not available.");
|
||||
@@ -139,17 +186,6 @@ public class RedactionStorageService {
|
||||
}
|
||||
|
||||
|
||||
@Timed("redactmanager_getSectionGrid")
|
||||
public SectionGrid getSectionGrid(String dossierId, String fileId) {
|
||||
|
||||
try {
|
||||
return storageService.readJSONObject(TenantContext.getTenantId(), StorageIdUtils.getStorageId(dossierId, fileId, FileType.SECTION_GRID), SectionGrid.class);
|
||||
} catch (StorageObjectDoesNotExist e) {
|
||||
throw new NotFoundException("Section Grid is not available.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public ComponentLog getComponentLog(String dossierId, String fileId) {
|
||||
|
||||
try {
|
||||
@@ -160,6 +196,12 @@ public class RedactionStorageService {
|
||||
}
|
||||
|
||||
|
||||
public boolean entityLogExists(String dossierId, String fileId) {
|
||||
|
||||
return storageService.objectExists(TenantContext.getTenantId(), StorageIdUtils.getStorageId(dossierId, fileId, FileType.ENTITY_LOG));
|
||||
}
|
||||
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public enum StorageType {
|
||||
PARSED_DOCUMENT(".json");
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package com.iqser.red.service.redaction.v1.server.utils;
|
||||
|
||||
import java.util.LinkedList;
|
||||
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 com.google.common.base.Functions;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.migration.MigratedIds;
|
||||
import com.iqser.red.service.redaction.v1.server.model.MigrationEntity;
|
||||
|
||||
public class MigratedIdsCollector implements Collector<MigrationEntity, MigratedIds, MigratedIds> {
|
||||
|
||||
@Override
|
||||
public Supplier<MigratedIds> supplier() {
|
||||
|
||||
return () -> new MigratedIds(new LinkedList<>());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public BiConsumer<MigratedIds, MigrationEntity> accumulator() {
|
||||
|
||||
return (migratedIds, migrationEntity) -> migratedIds.addMapping(migrationEntity.getOldId(), migrationEntity.getNewId());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public BinaryOperator<MigratedIds> combiner() {
|
||||
|
||||
return (migratedIds, migratedIds2) -> {
|
||||
migratedIds.getMappings().addAll(migratedIds2.getMappings());
|
||||
return migratedIds;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Function<MigratedIds, MigratedIds> finisher() {
|
||||
|
||||
return Functions.identity();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Set<Characteristics> characteristics() {
|
||||
|
||||
return Set.of(Characteristics.IDENTITY_FINISH, Characteristics.CONCURRENT, Characteristics.UNORDERED);
|
||||
}
|
||||
|
||||
}
|
||||
+1
-2
@@ -11,8 +11,7 @@ public final class Patterns {
|
||||
|
||||
public static final Map<String, Pattern> patternCache = new HashMap<>();
|
||||
|
||||
public static final Pattern AUTHOR_TABLE_SPLITTER = Pattern.compile(
|
||||
"(((((di)|(van)) )|[A-Z]’)?[A-ZÄÖÜ][\\wäöüéèê]{2,500}( ?[A-ZÄÖÜ]{1,2}\\.){1,3})|(((((di)|(van)) )|[A-Z]’)?[A-ZÄÖÜ][\\wäöüéèê]{2,500}( ?[A-ZÄÖÜ]{1,2} ){1,3})");
|
||||
public static final Pattern AUTHOR_NAME_PATTERN = Pattern.compile("^(?!\\w*[.]$)\\p{Lu}\\p{L}*[.\\p{L}\\s]*$");
|
||||
|
||||
|
||||
public Pattern getCompiledPattern(String pattern, boolean caseInsensitive) {
|
||||
|
||||
+9
-12
@@ -12,7 +12,7 @@ import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collector;
|
||||
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Rectangle;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.Position;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.textblock.AtomicTextBlock;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
@@ -26,18 +26,9 @@ public class RectangleTransformations {
|
||||
}
|
||||
|
||||
|
||||
public static Rectangle2D rectangleBBox(List<Rectangle> rectangles) {
|
||||
public static Rectangle2D rectangleBBox(List<Position> positions) {
|
||||
|
||||
return rectangles.stream().map(RectangleTransformations::toRectangle2D).collect(new Rectangle2DBBoxCollector());
|
||||
}
|
||||
|
||||
|
||||
public static Rectangle2D toRectangle2D(Rectangle redactionLogRectangle) {
|
||||
|
||||
return new Rectangle2D.Double(redactionLogRectangle.getTopLeft().getX(),
|
||||
redactionLogRectangle.getTopLeft().getY() + redactionLogRectangle.getHeight(),
|
||||
redactionLogRectangle.getWidth(),
|
||||
-redactionLogRectangle.getHeight());
|
||||
return positions.stream().map(Position::toRectangle2D).collect(new Rectangle2DBBoxCollector());
|
||||
}
|
||||
|
||||
|
||||
@@ -79,6 +70,12 @@ public class RectangleTransformations {
|
||||
}
|
||||
|
||||
|
||||
public static Collector<Rectangle2D, Rectangle2DBBoxCollector.BBox, Rectangle2D> collectBBox() {
|
||||
|
||||
return new Rectangle2DBBoxCollector();
|
||||
}
|
||||
|
||||
|
||||
private static class Rectangle2DBBoxCollector implements Collector<Rectangle2D, Rectangle2DBBoxCollector.BBox, Rectangle2D> {
|
||||
|
||||
@Override
|
||||
|
||||
+6
@@ -132,6 +132,12 @@ public class RedactionSearchUtility {
|
||||
return getTextRangesByPatternWithLineBreaks(textBlock, group, pattern);
|
||||
}
|
||||
|
||||
public static List<TextRange> findTextRangesByRegexIgnoreCase(String regexPattern, TextBlock textBlock) {
|
||||
|
||||
Pattern pattern = Patterns.getCompiledPattern(regexPattern, true);
|
||||
return getTextRangesByPattern(textBlock, 0, pattern);
|
||||
}
|
||||
|
||||
|
||||
public static List<TextRange> findTextRangesByRegexIgnoreCase(String regexPattern, int group, TextBlock textBlock) {
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
server:
|
||||
port: 8083
|
||||
port: 8077
|
||||
|
||||
persistence-service.url: "http://localhost:8085"
|
||||
tenant-user-management-service.url: "http://localhost:8091/internal"
|
||||
|
||||
@@ -7,10 +7,18 @@ entity-recognition-service.url: "http://entity-recognition-service-v1:8080"
|
||||
tenant-user-management-service.url: "http://tenant-user-management-service:8080/internal"
|
||||
fforesight.tenants.remote: true
|
||||
|
||||
logging.pattern.level: "%5p [${spring.application.name},%X{traceId:-},%X{spanId:-}]"
|
||||
|
||||
logging.type: ${LOGGING_TYPE:CONSOLE}
|
||||
kubernetes.namespace: ${NAMESPACE:default}
|
||||
project.version: 1.0-SNAPSHOT
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: redaction-service
|
||||
main:
|
||||
allow-circular-references: true # FIXME
|
||||
profiles:
|
||||
@@ -29,6 +37,15 @@ spring:
|
||||
max-attempts: 3
|
||||
max-interval: 15000
|
||||
prefetch: 1
|
||||
cache:
|
||||
type: NONE
|
||||
data:
|
||||
redis:
|
||||
database: 0
|
||||
host: ${REDIS_HOST:localhost}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: ${REDIS_PASSWORD}
|
||||
timeout: 60000
|
||||
|
||||
management:
|
||||
endpoint:
|
||||
@@ -37,6 +54,13 @@ management:
|
||||
health.enabled: true
|
||||
endpoints.web.exposure.include: prometheus, health
|
||||
metrics.export.prometheus.enabled: ${monitoring.enabled:false}
|
||||
tracing:
|
||||
enabled: ${TRACING_ENABLED:false}
|
||||
sampling:
|
||||
probability: ${TRACING_PROBABILITY:1.0}
|
||||
otlp:
|
||||
tracing:
|
||||
endpoint: ${OTLP_ENDPOINT:http://otel-collector-opentelemetry-collector.otel-collector:4318/v1/traces}
|
||||
|
||||
|
||||
storage:
|
||||
|
||||
+2
-1
@@ -22,6 +22,7 @@ import com.iqser.red.service.redaction.v1.server.model.document.entity.MatchedRu
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.*;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.Section;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.Table;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.TableCell;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.SemanticNode;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.Document;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.Paragraph;
|
||||
@@ -73,7 +74,7 @@ query "getFileAttributes"
|
||||
//------------------------------------ Local dictionary search rules ------------------------------------
|
||||
|
||||
// Rule unit: LocalDictionarySearch.0
|
||||
rule "LDS.0.0: run local dictionary search"
|
||||
rule "LDS.0.0: Run local dictionary search"
|
||||
agenda-group "LOCAL_DICTIONARY_ADDS"
|
||||
salience -999
|
||||
when
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
<configuration>
|
||||
|
||||
<springProperty scope="configuration" name="logType" source="logging.type"/>
|
||||
<springProperty scope="context" name="application.name" source="spring.application.name"/>
|
||||
<springProperty scope="context" name="version" source="project.version"/>
|
||||
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
|
||||
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
|
||||
|
||||
<appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
|
||||
</appender>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="${logType}"/>
|
||||
</root>
|
||||
|
||||
</configuration>
|
||||
+33
-19
@@ -1,8 +1,32 @@
|
||||
package com.iqser.red.service.redaction.v1.server;
|
||||
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.mockito.stubbing.Answer;
|
||||
import org.springframework.amqp.rabbit.core.RabbitTemplate;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.iqser.red.service.dictionarymerge.commons.DictionaryEntry;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.AnalyzeRequest;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.ManualRedactions;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.configuration.Colors;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.file.FileType;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.type.Type;
|
||||
@@ -12,33 +36,20 @@ import com.iqser.red.service.redaction.v1.server.client.LegalBasisClient;
|
||||
import com.iqser.red.service.redaction.v1.server.client.RulesClient;
|
||||
import com.iqser.red.service.redaction.v1.server.controller.RedactionController;
|
||||
import com.iqser.red.service.redaction.v1.server.service.AnalyzeService;
|
||||
import com.iqser.red.service.redaction.v1.server.utils.ResourceLoader;
|
||||
import com.iqser.red.service.redaction.v1.server.utils.TextNormalizationUtilities;
|
||||
import com.iqser.red.service.redaction.v1.server.service.UnprocessedChangesService;
|
||||
import com.iqser.red.service.redaction.v1.server.storage.RedactionStorageService;
|
||||
import com.iqser.red.service.redaction.v1.server.utils.LayoutParsingRequestProvider;
|
||||
import com.iqser.red.service.redaction.v1.server.utils.ResourceLoader;
|
||||
import com.iqser.red.service.redaction.v1.server.utils.TextNormalizationUtilities;
|
||||
import com.iqser.red.storage.commons.service.StorageService;
|
||||
import com.iqser.red.storage.commons.utils.FileSystemBackedStorageService;
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.queue.LayoutParsingFinishedEvent;
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.queue.LayoutParsingType;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.LayoutParsingPipeline;
|
||||
import com.knecon.fforesight.tenantcommons.TenantContext;
|
||||
import com.knecon.fforesight.tenantcommons.TenantsClient;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.mockito.stubbing.Answer;
|
||||
import org.springframework.amqp.rabbit.core.RabbitTemplate;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public abstract class AbstractRedactionIntegrationTest {
|
||||
|
||||
@@ -106,6 +117,9 @@ public abstract class AbstractRedactionIntegrationTest {
|
||||
@Autowired
|
||||
private LayoutParsingPipeline layoutParsingPipeline;
|
||||
|
||||
@Autowired
|
||||
protected UnprocessedChangesService unprocessedChangesService;
|
||||
|
||||
@MockBean
|
||||
protected RabbitTemplate rabbitTemplate;
|
||||
|
||||
@@ -431,7 +445,6 @@ public abstract class AbstractRedactionIntegrationTest {
|
||||
@SneakyThrows
|
||||
protected LayoutParsingFinishedEvent analyzeDocumentStructure(LayoutParsingType layoutParsingType, AnalyzeRequest request) {
|
||||
|
||||
|
||||
return layoutParsingPipeline.parseLayoutAndSaveFilesToStorage(LayoutParsingRequestProvider.build(layoutParsingType, request));
|
||||
}
|
||||
|
||||
@@ -462,6 +475,7 @@ public abstract class AbstractRedactionIntegrationTest {
|
||||
.dossierId(TEST_DOSSIER_ID)
|
||||
.fileId(TEST_FILE_ID)
|
||||
.lastProcessed(OffsetDateTime.now())
|
||||
.manualRedactions(ManualRedactions.builder().build())
|
||||
.build();
|
||||
|
||||
storageService.storeObject(TenantContext.getTenantId(), RedactionStorageService.StorageIdUtils.getStorageId(TEST_DOSSIER_ID, TEST_FILE_ID, FileType.ORIGIN), fileStream);
|
||||
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
package com.iqser.red.service.redaction.v1.server;
|
||||
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.FilterType;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
|
||||
import com.iqser.red.commons.jackson.ObjectMapperFactory;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.AnalyzeRequest;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.AnalyzeResult;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.RuleFileType;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.common.JSONPrimitive;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.type.Type;
|
||||
import com.iqser.red.service.redaction.v1.server.annotate.AnnotateRequest;
|
||||
import com.iqser.red.service.redaction.v1.server.annotate.AnnotateResponse;
|
||||
import com.iqser.red.service.redaction.v1.server.redaction.utils.OsUtils;
|
||||
import com.iqser.red.storage.commons.StorageAutoConfiguration;
|
||||
import com.iqser.red.storage.commons.service.StorageService;
|
||||
import com.iqser.red.storage.commons.utils.FileSystemBackedStorageService;
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.queue.LayoutParsingType;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.LayoutParsingServiceProcessorConfiguration;
|
||||
import com.knecon.fforesight.tenantcommons.TenantContext;
|
||||
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = {"application.type=DocuMine"})
|
||||
@Import(AnalysisTest.RedactionIntegrationTestConfiguration.class)
|
||||
public class AnalysisTest extends AbstractRedactionIntegrationTest {
|
||||
|
||||
private static final String RULES = loadFromClassPath("drools/test_rules.drl");
|
||||
private static final String COMPONENT_RULES = loadFromClassPath("drools/test_components.drl");
|
||||
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
public void analyzeTableDemoFile() throws IOException {
|
||||
|
||||
AnalyzeRequest request = uploadFileToStorage("files/Metolachlor/S-Metolachlor_RAR_01_Volume_1_2018-09-06.pdf");
|
||||
|
||||
System.out.println("Start Full integration test");
|
||||
analyzeDocumentStructure(LayoutParsingType.DOCUMINE, request);
|
||||
System.out.println("Finished structure analysis");
|
||||
AnalyzeResult result = analyzeService.analyze(request);
|
||||
System.out.println("Finished analysis");
|
||||
var entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
var componentLog = redactionStorageService.getComponentLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
AnnotateResponse annotateResponse = annotationService.annotate(AnnotateRequest.builder().dossierId(TEST_DOSSIER_ID).fileId(TEST_FILE_ID).build());
|
||||
|
||||
String outputFileName = OsUtils.getTemporaryDirectory() + "/Documine.pdf";
|
||||
|
||||
try (FileOutputStream fileOutputStream = new FileOutputStream(outputFileName)) {
|
||||
fileOutputStream.write(annotateResponse.getDocument());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Configuration
|
||||
@EnableAutoConfiguration(exclude = {RabbitAutoConfiguration.class})
|
||||
@Import(LayoutParsingServiceProcessorConfiguration.class)
|
||||
@ComponentScan(excludeFilters = {@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = StorageAutoConfiguration.class)})
|
||||
static class RedactionIntegrationTestConfiguration {
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
public StorageService inmemoryStorage() {
|
||||
|
||||
return new FileSystemBackedStorageService(ObjectMapperFactory.create());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@BeforeEach
|
||||
public void stubClients() {
|
||||
|
||||
TenantContext.setTenantId("documine");
|
||||
|
||||
when(rulesClient.getVersion(TEST_DOSSIER_TEMPLATE_ID, RuleFileType.ENTITY)).thenReturn(System.currentTimeMillis());
|
||||
when(rulesClient.getRules(TEST_DOSSIER_TEMPLATE_ID, RuleFileType.ENTITY)).thenReturn(JSONPrimitive.of(RULES));
|
||||
when(rulesClient.getVersion(TEST_DOSSIER_TEMPLATE_ID, RuleFileType.COMPONENT)).thenReturn(System.currentTimeMillis());
|
||||
when(rulesClient.getRules(TEST_DOSSIER_TEMPLATE_ID, RuleFileType.COMPONENT)).thenReturn(JSONPrimitive.of(COMPONENT_RULES));
|
||||
|
||||
loadDictionaryForTest();
|
||||
loadTypeForTest();
|
||||
loadNerForTest();
|
||||
when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(0L);
|
||||
when(dictionaryClient.getAllTypesForDossierTemplate(TEST_DOSSIER_TEMPLATE_ID, false)).thenReturn(getTypeResponse());
|
||||
|
||||
when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(0L);
|
||||
when(dictionaryClient.getAllTypesForDossier(TEST_DOSSIER_ID, false)).thenReturn(List.of(Type.builder()
|
||||
.id(DOSSIER_REDACTIONS_INDICATOR + ":" + TEST_DOSSIER_TEMPLATE_ID)
|
||||
.type(DOSSIER_REDACTIONS_INDICATOR)
|
||||
.dossierTemplateId(TEST_DOSSIER_ID)
|
||||
.hexColor("#ffe187")
|
||||
.isHint(hintTypeMap.get(DOSSIER_REDACTIONS_INDICATOR))
|
||||
.isCaseInsensitive(caseInSensitiveMap.get(DOSSIER_REDACTIONS_INDICATOR))
|
||||
.isRecommendation(recommendationTypeMap.get(DOSSIER_REDACTIONS_INDICATOR))
|
||||
.rank(rankTypeMap.get(DOSSIER_REDACTIONS_INDICATOR))
|
||||
.build()));
|
||||
|
||||
mockDictionaryCalls(null);
|
||||
|
||||
when(dictionaryClient.getColors(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(colors);
|
||||
}
|
||||
|
||||
}
|
||||
+3
-1
@@ -28,6 +28,7 @@ import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
|
||||
import com.google.common.collect.Sets;
|
||||
import com.iqser.red.commons.jackson.ObjectMapperFactory;
|
||||
import com.iqser.red.service.dictionarymerge.commons.DictionaryEntry;
|
||||
import com.iqser.red.service.dictionarymerge.commons.DictionaryEntryModel;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.configuration.Colors;
|
||||
@@ -36,6 +37,7 @@ import com.iqser.red.service.redaction.v1.server.client.DictionaryClient;
|
||||
import com.iqser.red.service.redaction.v1.server.model.dictionary.DictionaryVersion;
|
||||
import com.iqser.red.service.redaction.v1.server.service.DictionaryService;
|
||||
import com.iqser.red.storage.commons.service.StorageService;
|
||||
import com.iqser.red.storage.commons.utils.FileSystemBackedStorageService;
|
||||
import com.knecon.fforesight.tenantcommons.TenantContext;
|
||||
import com.knecon.fforesight.tenantcommons.TenantsClient;
|
||||
|
||||
@@ -67,7 +69,7 @@ public class DictionaryServiceTest {
|
||||
@Primary
|
||||
public StorageService inmemoryStorage() {
|
||||
|
||||
return new FileSystemBackedStorageService();
|
||||
return new FileSystemBackedStorageService(ObjectMapperFactory.create());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+17
-8
@@ -6,6 +6,8 @@ import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.file.FileType;
|
||||
import com.iqser.red.service.redaction.v1.server.storage.RedactionStorageService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -19,8 +21,10 @@ import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.FilterType;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
|
||||
import com.iqser.red.commons.jackson.ObjectMapperFactory;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.AnalyzeRequest;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.AnalyzeResult;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.RuleFileType;
|
||||
@@ -31,6 +35,7 @@ import com.iqser.red.service.redaction.v1.server.annotate.AnnotateResponse;
|
||||
import com.iqser.red.service.redaction.v1.server.redaction.utils.OsUtils;
|
||||
import com.iqser.red.storage.commons.StorageAutoConfiguration;
|
||||
import com.iqser.red.storage.commons.service.StorageService;
|
||||
import com.iqser.red.storage.commons.utils.FileSystemBackedStorageService;
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.queue.LayoutParsingType;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.LayoutParsingServiceProcessorConfiguration;
|
||||
import com.knecon.fforesight.tenantcommons.TenantContext;
|
||||
@@ -48,7 +53,11 @@ public class DocumineFloraTest extends AbstractRedactionIntegrationTest {
|
||||
// @Disabled
|
||||
public void titleExtraction() throws IOException {
|
||||
|
||||
AnalyzeRequest request = uploadFileToStorage("files/Documine/Flora/A8591B/15-Curacron_ToxicidadeAgudaOral.pdf");
|
||||
AnalyzeRequest request = uploadFileToStorage("files/ImportedRedactions/18 Chlorothalonil RAR 08 Volume 3CA B 6a Oct 2017.pdf");
|
||||
|
||||
ClassPathResource importedRedactionClasspathResource = new ClassPathResource("files/ImportedRedactions/18 Chlorothalonil RAR 08 Volume 3CA B 6a Oct 2017.IMPORTED_REDACTIONS.json");
|
||||
storageService.storeObject(TenantContext.getTenantId(), RedactionStorageService.StorageIdUtils.getStorageId(TEST_DOSSIER_ID, TEST_FILE_ID, FileType.IMPORTED_REDACTIONS), importedRedactionClasspathResource.getInputStream());
|
||||
|
||||
|
||||
// AnalyzeRequest request = prepareStorage("files/Documine/Flora/ProblemDocs/SOLICITA_VICTRATO-GOLD-II_Item 21_Mutacao_Genica (1).pdf",
|
||||
// "files/Documine/Flora/ProblemDocs/SOLICITA_VICTRATO-GOLD-II_Item 21_Mutacao_Genica (1).TABLES.json");
|
||||
@@ -58,7 +67,7 @@ public class DocumineFloraTest extends AbstractRedactionIntegrationTest {
|
||||
System.out.println("Finished structure analysis");
|
||||
AnalyzeResult result = analyzeService.analyze(request);
|
||||
System.out.println("Finished analysis");
|
||||
var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
var entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
var componentLog = redactionStorageService.getComponentLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
AnnotateResponse annotateResponse = annotationService.annotate(AnnotateRequest.builder().dossierId(TEST_DOSSIER_ID).fileId(TEST_FILE_ID).build());
|
||||
|
||||
@@ -84,7 +93,7 @@ public class DocumineFloraTest extends AbstractRedactionIntegrationTest {
|
||||
System.out.println("Finished structure analysis");
|
||||
AnalyzeResult result = analyzeService.analyze(request);
|
||||
System.out.println("Finished analysis");
|
||||
var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
var entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
|
||||
AnnotateResponse annotateResponse = annotationService.annotate(AnnotateRequest.builder().dossierId(TEST_DOSSIER_ID).fileId(TEST_FILE_ID).build());
|
||||
|
||||
@@ -104,14 +113,14 @@ public class DocumineFloraTest extends AbstractRedactionIntegrationTest {
|
||||
// Fix In BodyTextFrameService destroys header detection in files/new/SYNGENTA_EFSA_sanitisation_GFL_v1_moreSections.pdf
|
||||
// TODO unify logic
|
||||
|
||||
AnalyzeRequest request = uploadFileToStorage("files/Documine/Flora/402Study.pdf");
|
||||
AnalyzeRequest request = uploadFileToStorage("files/Documine/Flora/402Study-ocred.pdf");
|
||||
|
||||
System.out.println("Start Full integration test");
|
||||
analyzeDocumentStructure(LayoutParsingType.DOCUMINE, request);
|
||||
System.out.println("Finished structure analysis");
|
||||
AnalyzeResult result = analyzeService.analyze(request);
|
||||
System.out.println("Finished analysis");
|
||||
var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
var entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
|
||||
AnnotateResponse annotateResponse = annotationService.annotate(AnnotateRequest.builder().dossierId(TEST_DOSSIER_ID).fileId(TEST_FILE_ID).build());
|
||||
|
||||
@@ -134,7 +143,7 @@ public class DocumineFloraTest extends AbstractRedactionIntegrationTest {
|
||||
@Primary
|
||||
public StorageService inmemoryStorage() {
|
||||
|
||||
return new FileSystemBackedStorageService();
|
||||
return new FileSystemBackedStorageService(ObjectMapperFactory.create());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -153,10 +162,10 @@ public class DocumineFloraTest extends AbstractRedactionIntegrationTest {
|
||||
loadTypeForTest();
|
||||
loadNerForTest();
|
||||
when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(0L);
|
||||
when(dictionaryClient.getAllTypesForDossierTemplate(TEST_DOSSIER_TEMPLATE_ID, false)).thenReturn(getTypeResponse());
|
||||
when(dictionaryClient.getAllTypesForDossierTemplate(TEST_DOSSIER_TEMPLATE_ID, true)).thenReturn(getTypeResponse());
|
||||
|
||||
when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(0L);
|
||||
when(dictionaryClient.getAllTypesForDossier(TEST_DOSSIER_ID, false)).thenReturn(List.of(Type.builder()
|
||||
when(dictionaryClient.getAllTypesForDossier(TEST_DOSSIER_ID, true)).thenReturn(List.of(Type.builder()
|
||||
.id(DOSSIER_REDACTIONS_INDICATOR + ":" + TEST_DOSSIER_TEMPLATE_ID)
|
||||
.type(DOSSIER_REDACTIONS_INDICATOR)
|
||||
.dossierTemplateId(TEST_DOSSIER_ID)
|
||||
|
||||
-128
@@ -1,128 +0,0 @@
|
||||
package com.iqser.red.service.redaction.v1.server;
|
||||
|
||||
import static java.io.File.createTempFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.springframework.core.io.InputStreamResource;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.iqser.red.commons.jackson.ObjectMapperFactory;
|
||||
import com.iqser.red.storage.commons.exception.StorageObjectDoesNotExist;
|
||||
import com.iqser.red.storage.commons.service.StorageService;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
public class FileSystemBackedStorageService implements StorageService {
|
||||
|
||||
private final Map<String, File> dataMap = new HashMap<>();
|
||||
|
||||
|
||||
public FileSystemBackedStorageService() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@SneakyThrows
|
||||
@Override
|
||||
public void downloadTo(String tenantId, String objectId, File destinationFile) {
|
||||
|
||||
var res = dataMap.get(objectId);
|
||||
if (res == null) {
|
||||
throw new StorageObjectDoesNotExist(new RuntimeException());
|
||||
}
|
||||
|
||||
IOUtils.copy(new FileInputStream(res), new FileOutputStream(destinationFile));
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public InputStreamResource getObject(String tenantId, String objectId) {
|
||||
return new InputStreamResource(new FileInputStream(dataMap.get(objectId)));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void deleteObject(String tenantId, String objectId) {
|
||||
|
||||
dataMap.remove(objectId);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean objectExists(String tenantId, String objectId) {
|
||||
|
||||
return dataMap.containsKey(objectId);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public <T> void storeJSONObject(String tenantId, String objectId, T any) {
|
||||
|
||||
File tempFile = createTempFile("test", ".tmp");
|
||||
getMapper().writeValue(new FileOutputStream(tempFile), any);
|
||||
dataMap.put(objectId, tempFile);
|
||||
}
|
||||
|
||||
|
||||
private ObjectMapper getMapper() {
|
||||
|
||||
return ObjectMapperFactory.create();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public <T> T readJSONObject(String tenantId, String objectId, Class<T> clazz) {
|
||||
|
||||
if (dataMap.get(objectId) == null || !dataMap.get(objectId).exists()) {
|
||||
throw new StorageObjectDoesNotExist("Stored object not found");
|
||||
}
|
||||
return getMapper().readValue(new FileInputStream(dataMap.get(objectId)), clazz);
|
||||
}
|
||||
|
||||
|
||||
public List<String> listPaths() {
|
||||
|
||||
return new ArrayList<>(dataMap.keySet());
|
||||
}
|
||||
|
||||
|
||||
public List<String> listFilePaths() {
|
||||
|
||||
return dataMap.values().stream().map(File::getAbsolutePath).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public void storeObject(String tenantId, String objectId, InputStream stream) {
|
||||
|
||||
File tempFile = createTempFile("test", ".tmp");
|
||||
|
||||
try (var fileOutputStream = new FileOutputStream(tempFile)) {
|
||||
IOUtils.copy(stream, fileOutputStream);
|
||||
}
|
||||
|
||||
dataMap.put(objectId, tempFile);
|
||||
}
|
||||
|
||||
|
||||
public void clearStorage() {
|
||||
|
||||
this.dataMap.forEach((k, v) -> {
|
||||
v.delete();
|
||||
});
|
||||
this.dataMap.clear();
|
||||
}
|
||||
|
||||
}
|
||||
+345
@@ -0,0 +1,345 @@
|
||||
package com.iqser.red.service.redaction.v1.server;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntityLog;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntityLogEntry;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.Position;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.migration.MigratedIds;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.ManualRedactions;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.file.FileType;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.type.Type;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Rectangle;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLog;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLogEntry;
|
||||
import com.iqser.red.service.redaction.v1.server.annotate.AnnotateRequest;
|
||||
import com.iqser.red.service.redaction.v1.server.annotate.AnnotateResponse;
|
||||
import com.iqser.red.service.redaction.v1.server.document.graph.BuildDocumentIntegrationTest;
|
||||
import com.iqser.red.service.redaction.v1.server.migration.LegacyRedactionLogMergeService;
|
||||
import com.iqser.red.service.redaction.v1.server.migration.RedactionLogToEntityLogMigrationService;
|
||||
import com.iqser.red.service.redaction.v1.server.model.MigratedEntityLog;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.Document;
|
||||
import com.iqser.red.service.redaction.v1.server.redaction.utils.OsUtils;
|
||||
import com.iqser.red.service.redaction.v1.server.service.DictionaryService;
|
||||
import com.iqser.red.service.redaction.v1.server.service.document.EntityFindingUtility;
|
||||
import com.iqser.red.service.redaction.v1.server.utils.RectangleTransformations;
|
||||
import com.knecon.fforesight.tenantcommons.TenantContext;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
//@Disabled
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
@Import(MigrationIntegrationTest.TestConfiguration.class)
|
||||
public class MigrationIntegrationTest extends BuildDocumentIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
RedactionLogToEntityLogMigrationService redactionLogToEntityLogMigrationService;
|
||||
|
||||
@Autowired
|
||||
LegacyRedactionLogMergeService legacyRedactionLogMergeService;
|
||||
|
||||
@Autowired
|
||||
DictionaryService dictionaryService;
|
||||
|
||||
@Autowired
|
||||
ObjectMapper mapper;
|
||||
|
||||
private final String TEST_DOSSIER_TEMPLATE_ID = "123";
|
||||
|
||||
|
||||
@BeforeEach
|
||||
public void stubClients() {
|
||||
|
||||
TenantContext.setTenantId("redaction");
|
||||
|
||||
loadDictionaryForTest();
|
||||
loadTypeForTest();
|
||||
when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(0L);
|
||||
when(dictionaryClient.getAllTypesForDossierTemplate(TEST_DOSSIER_TEMPLATE_ID, true)).thenReturn(getTypeResponse());
|
||||
|
||||
when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(0L);
|
||||
when(dictionaryClient.getAllTypesForDossier(TEST_DOSSIER_ID, true)).thenReturn(List.of(Type.builder()
|
||||
.id(DOSSIER_REDACTIONS_INDICATOR + ":" + TEST_DOSSIER_TEMPLATE_ID)
|
||||
.type(DOSSIER_REDACTIONS_INDICATOR)
|
||||
.dossierTemplateId(TEST_DOSSIER_ID)
|
||||
.hexColor("#ffe187")
|
||||
.isHint(hintTypeMap.get(DOSSIER_REDACTIONS_INDICATOR))
|
||||
.isCaseInsensitive(caseInSensitiveMap.get(DOSSIER_REDACTIONS_INDICATOR))
|
||||
.isRecommendation(recommendationTypeMap.get(DOSSIER_REDACTIONS_INDICATOR))
|
||||
.rank(rankTypeMap.get(DOSSIER_REDACTIONS_INDICATOR))
|
||||
.build()));
|
||||
|
||||
mockDictionaryCalls(null);
|
||||
|
||||
when(dictionaryClient.getColors(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(colors);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
@SneakyThrows
|
||||
public void testSave() {
|
||||
|
||||
MigratedIds ids = new MigratedIds(new LinkedList<>());
|
||||
ids.addMapping("123", "321");
|
||||
ids.addMapping("123", "321");
|
||||
ids.addMapping("123", "321");
|
||||
ids.addMapping("123", "321");
|
||||
ids.addMapping("123", "321");
|
||||
|
||||
mapper.writeValue(new FileOutputStream("/tmp/testIds.json"), ids);
|
||||
var ids2 = mapper.readValue(new FileInputStream("/tmp/testIds.json"), MigratedIds.class);
|
||||
assert ids2.getMappings().size() == 5;
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
@SneakyThrows
|
||||
public void testMigration() {
|
||||
|
||||
String filesPrefix = "files/migration/test";
|
||||
|
||||
String fileName = filesPrefix + ".ORIGIN.pdf";
|
||||
String imageFileName = filesPrefix + ".IMAGE_INFO.json";
|
||||
String tableFileName = filesPrefix + ".TABLES.json";
|
||||
String manualChangesFileName = filesPrefix + ".MANUAL_CHANGES.json";
|
||||
String migratedIdsFileName = filesPrefix + ".MIGRATED_IDS.json";
|
||||
|
||||
Document document = buildGraph(fileName, imageFileName, tableFileName);
|
||||
RedactionLog redactionLog;
|
||||
RedactionLog mergedRedactionLog;
|
||||
|
||||
dictionaryService.updateDictionary(TEST_DOSSIER_TEMPLATE_ID, TEST_DOSSIER_ID);
|
||||
|
||||
try (var in = new ClassPathResource(filesPrefix + ".REDACTION_LOG.json").getInputStream()) {
|
||||
redactionLog = mapper.readValue(in, RedactionLog.class);
|
||||
}
|
||||
var manualChangesResource = new ClassPathResource(manualChangesFileName);
|
||||
ManualRedactions manualRedactions;
|
||||
if (manualChangesResource.exists()) {
|
||||
try (var in = manualChangesResource.getInputStream()) {
|
||||
manualRedactions = mapper.readValue(in, ManualRedactions.class);
|
||||
if (manualRedactions.getEntriesToAdd() == null) {
|
||||
manualRedactions.setEntriesToAdd(Collections.emptySet());
|
||||
}
|
||||
if (manualRedactions.getForceRedactions() == null) {
|
||||
manualRedactions.setForceRedactions(Collections.emptySet());
|
||||
}
|
||||
if (manualRedactions.getIdsToRemove() == null) {
|
||||
manualRedactions.setIdsToRemove(Collections.emptySet());
|
||||
}
|
||||
if (manualRedactions.getLegalBasisChanges() == null) {
|
||||
manualRedactions.setLegalBasisChanges(Collections.emptySet());
|
||||
}
|
||||
if (manualRedactions.getRecategorizations() == null) {
|
||||
manualRedactions.setRecategorizations(Collections.emptySet());
|
||||
}
|
||||
if (manualRedactions.getResizeRedactions() == null) {
|
||||
manualRedactions.setResizeRedactions(Collections.emptySet());
|
||||
}
|
||||
}
|
||||
MigratedIds migratedIds = getMigratedIds(migratedIdsFileName);
|
||||
revertMigration(manualRedactions, migratedIds);
|
||||
mergedRedactionLog = legacyRedactionLogMergeService.addManualAddEntriesAndRemoveSkippedImported(redactionLog, manualRedactions, TEST_DOSSIER_TEMPLATE_ID);
|
||||
} else {
|
||||
manualRedactions = new ManualRedactions();
|
||||
mergedRedactionLog = redactionLog;
|
||||
}
|
||||
|
||||
MigratedEntityLog migratedEntityLog = redactionLogToEntityLogMigrationService.migrate(mergedRedactionLog, document, TEST_DOSSIER_TEMPLATE_ID, manualRedactions);
|
||||
|
||||
redactionStorageService.storeObject(TEST_DOSSIER_ID, TEST_FILE_ID, FileType.ENTITY_LOG, migratedEntityLog.getEntityLog());
|
||||
assertEquals(mergedRedactionLog.getRedactionLogEntry().size(), migratedEntityLog.getEntityLog().getEntityLogEntry().size());
|
||||
EntityLog entityLog = migratedEntityLog.getEntityLog();
|
||||
assertEquals(mergedRedactionLog.getAnalysisNumber(), entityLog.getAnalysisNumber());
|
||||
assertEquals(mergedRedactionLog.getAnalysisVersion(), entityLog.getAnalysisVersion());
|
||||
assertEquals(mergedRedactionLog.getDictionaryVersion(), entityLog.getDictionaryVersion());
|
||||
assertEquals(mergedRedactionLog.getDossierDictionaryVersion(), entityLog.getDossierDictionaryVersion());
|
||||
assertEquals(mergedRedactionLog.getLegalBasisVersion(), entityLog.getLegalBasisVersion());
|
||||
assertEquals(mergedRedactionLog.getRulesVersion(), entityLog.getRulesVersion());
|
||||
assertEquals(mergedRedactionLog.getLegalBasis().size(), entityLog.getLegalBasis().size());
|
||||
|
||||
Map<String, String> migratedIds = migratedEntityLog.getMigratedIds().buildOldToNewMapping();
|
||||
migratedIds.forEach((oldId, newId) -> assertEntryIsEqual(oldId, newId, mergedRedactionLog, entityLog, migratedIds));
|
||||
|
||||
AnnotateResponse annotateResponse = annotationService.annotate(AnnotateRequest.builder().dossierId(TEST_DOSSIER_ID).fileId(TEST_FILE_ID)
|
||||
.build());
|
||||
File outputFile = Path.of(OsUtils.getTemporaryDirectory()).resolve(Path.of(fileName.replaceAll(".pdf", "_MIGRATED.pdf")).getFileName()).toFile();
|
||||
|
||||
try (FileOutputStream fileOutputStream = new FileOutputStream(outputFile)) {
|
||||
fileOutputStream.write(annotateResponse.getDocument());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void revertMigration(ManualRedactions manualRedactions, MigratedIds migratedIds) {
|
||||
|
||||
var mapping = migratedIds.buildNewToOldMapping();
|
||||
manualRedactions.getEntriesToAdd()
|
||||
.stream()
|
||||
.filter(e -> mapping.containsKey(e.getAnnotationId()))
|
||||
.forEach(e -> e.setAnnotationId(mapping.get(e.getAnnotationId())));
|
||||
|
||||
manualRedactions.getRecategorizations()
|
||||
.stream()
|
||||
.filter(e -> mapping.containsKey(e.getAnnotationId()))
|
||||
.forEach(e -> e.setAnnotationId(mapping.get(e.getAnnotationId())));
|
||||
|
||||
manualRedactions.getResizeRedactions()
|
||||
.stream()
|
||||
.filter(e -> mapping.containsKey(e.getAnnotationId()))
|
||||
.forEach(e -> e.setAnnotationId(mapping.get(e.getAnnotationId())));
|
||||
|
||||
manualRedactions.getIdsToRemove()
|
||||
.stream()
|
||||
.filter(e -> mapping.containsKey(e.getAnnotationId()))
|
||||
.forEach(e -> e.setAnnotationId(mapping.get(e.getAnnotationId())));
|
||||
|
||||
manualRedactions.getLegalBasisChanges()
|
||||
.stream()
|
||||
.filter(e -> mapping.containsKey(e.getAnnotationId()))
|
||||
.forEach(e -> e.setAnnotationId(mapping.get(e.getAnnotationId())));
|
||||
|
||||
manualRedactions.getForceRedactions()
|
||||
.stream()
|
||||
.filter(e -> mapping.containsKey(e.getAnnotationId()))
|
||||
.forEach(e -> e.setAnnotationId(mapping.get(e.getAnnotationId())));
|
||||
}
|
||||
|
||||
|
||||
private MigratedIds getMigratedIds(String migratedIdsFileName) throws IOException {
|
||||
|
||||
var migratedIdsResource = new ClassPathResource(migratedIdsFileName);
|
||||
if (!migratedIdsResource.exists()) {
|
||||
return new MigratedIds();
|
||||
}
|
||||
try (var in = migratedIdsResource.getInputStream()) {
|
||||
return mapper.readValue(in, MigratedIds.class);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static boolean hasManualChanges(RedactionLogEntry entry) {
|
||||
|
||||
return !entry.getManualChanges().isEmpty() || (entry.getComments() != null && !entry.getComments().isEmpty());
|
||||
}
|
||||
|
||||
|
||||
private void assertEntryIsEqual(String oldId, String newId, RedactionLog redactionLog, EntityLog entityLog, Map<String, String> oldToNewMapping) {
|
||||
|
||||
RedactionLogEntry redactionLogEntry = redactionLog.getRedactionLogEntry()
|
||||
.stream()
|
||||
.filter(entry -> entry.getId().equals(oldId))
|
||||
.findAny()
|
||||
.orElseThrow();
|
||||
EntityLogEntry entityLogEntry = entityLog.getEntityLogEntry()
|
||||
.stream()
|
||||
.filter(entry -> entry.getId().equals(newId))
|
||||
.findAny()
|
||||
.orElseThrow();
|
||||
|
||||
if (!redactionLogEntry.isImage()) {
|
||||
assertEquals(redactionLogEntry.getValue().toLowerCase(Locale.ENGLISH), entityLogEntry.getValue().toLowerCase(Locale.ENGLISH));
|
||||
}
|
||||
assertEquals(redactionLogEntry.getChanges().size(), entityLogEntry.getChanges().size());
|
||||
assertTrue(redactionLogEntry.getManualChanges().size() <= entityLogEntry.getManualChanges().size());
|
||||
assertEquals(redactionLogEntry.getPositions().size(), entityLogEntry.getPositions().size());
|
||||
assertTrue(positionsAlmostEqual(redactionLogEntry.getPositions(), entityLogEntry.getPositions()));
|
||||
// assertEquals(redactionLogEntry.getColor(), entityLogEntry.getColor());
|
||||
assertEqualsNullSafe(redactionLogEntry.getLegalBasis(), entityLogEntry.getLegalBasis());
|
||||
// assertEqualsNullSafe(redactionLogEntry.getReason(), entityLogEntry.getReason());
|
||||
assertReferencesEqual(redactionLogEntry.getReference(), entityLogEntry.getReference(), oldToNewMapping);
|
||||
assertEquals(redactionLogEntry.isDictionaryEntry(), entityLogEntry.isDictionaryEntry());
|
||||
assertEquals(redactionLogEntry.isDossierDictionaryEntry(), entityLogEntry.isDossierDictionaryEntry());
|
||||
if (redactionLogEntry.getEngines() == null) {
|
||||
assertTrue(entityLogEntry.getEngines().isEmpty());
|
||||
} else {
|
||||
assertEquals(redactionLogEntry.getEngines()
|
||||
.stream()
|
||||
.map(Enum::name)
|
||||
.collect(Collectors.toSet()),
|
||||
entityLogEntry.getEngines()
|
||||
.stream()
|
||||
.map(Enum::name)
|
||||
.collect(Collectors.toSet()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private boolean positionsAlmostEqual(List<Rectangle> positions1, List<Position> positions2) {
|
||||
|
||||
double tolerance = 10;
|
||||
double averageDistance = 0;
|
||||
for (int i = 0; i < positions1.size(); i++) {
|
||||
Rectangle r1 = positions1.get(i);
|
||||
Rectangle2D p1 = new Rectangle2D.Double(r1.getTopLeft().getX(), r1.getTopLeft().getY(), r1.getWidth(), r1.getHeight());
|
||||
Position p2 = positions2.get(i);
|
||||
double distance = EntityFindingUtility.calculateDistance(p1, p2.toRectangle2D());
|
||||
if (r1.getPage() != p2.getPageNumber()) {
|
||||
return false;
|
||||
}
|
||||
averageDistance += distance;
|
||||
}
|
||||
|
||||
averageDistance /= positions1.size();
|
||||
|
||||
return averageDistance <= tolerance;
|
||||
}
|
||||
|
||||
|
||||
private void assertEqualsNullSafe(String string1, String string2) {
|
||||
|
||||
if (Objects.isNull(string1) && Objects.isNull(string2)) {
|
||||
assertEquals(string1, string2);
|
||||
} else if (Objects.isNull(string1)) {
|
||||
assertEquals("", string2);
|
||||
} else if (Objects.isNull(string2)) {
|
||||
assertEquals("", string1);
|
||||
} else {
|
||||
assertEquals(string1, string2);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private void assertReferencesEqual(Set<String> reference, Set<String> reference1, Map<String, String> oldToNewMapping) {
|
||||
|
||||
if (reference == null) {
|
||||
assertTrue(reference1.isEmpty());
|
||||
return;
|
||||
}
|
||||
|
||||
assertEquals(reference.stream()
|
||||
.map(oldToNewMapping::get)
|
||||
.collect(Collectors.toSet()), reference1);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+28
-6
@@ -1,5 +1,6 @@
|
||||
package com.iqser.red.service.redaction.v1.server;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@@ -24,6 +25,7 @@ import org.springframework.context.annotation.Import;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
|
||||
import com.iqser.red.commons.jackson.ObjectMapperFactory;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.AnalyzeRequest;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.AnalyzeResult;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.RuleFileType;
|
||||
@@ -31,7 +33,6 @@ import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntityLogEntry;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntryState;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntryType;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.AnnotationStatus;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.ManualRedactions;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.IdRemoval;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.common.JSONPrimitive;
|
||||
@@ -41,6 +42,7 @@ import com.iqser.red.service.redaction.v1.server.annotate.AnnotateResponse;
|
||||
import com.iqser.red.service.redaction.v1.server.redaction.utils.OsUtils;
|
||||
import com.iqser.red.storage.commons.StorageAutoConfiguration;
|
||||
import com.iqser.red.storage.commons.service.StorageService;
|
||||
import com.iqser.red.storage.commons.utils.FileSystemBackedStorageService;
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.queue.LayoutParsingType;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.LayoutParsingServiceProcessorConfiguration;
|
||||
import com.knecon.fforesight.tenantcommons.TenantContext;
|
||||
@@ -62,7 +64,7 @@ public class RedactionAcceptanceTest extends AbstractRedactionIntegrationTest {
|
||||
@Primary
|
||||
public StorageService inmemoryStorage() {
|
||||
|
||||
return new FileSystemBackedStorageService();
|
||||
return new FileSystemBackedStorageService(ObjectMapperFactory.create());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -81,10 +83,10 @@ public class RedactionAcceptanceTest extends AbstractRedactionIntegrationTest {
|
||||
loadTypeForTest();
|
||||
loadNerForTest();
|
||||
when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(0L);
|
||||
when(dictionaryClient.getAllTypesForDossierTemplate(TEST_DOSSIER_TEMPLATE_ID, false)).thenReturn(getTypeResponse());
|
||||
when(dictionaryClient.getAllTypesForDossierTemplate(TEST_DOSSIER_TEMPLATE_ID, true)).thenReturn(getTypeResponse());
|
||||
|
||||
when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(0L);
|
||||
when(dictionaryClient.getAllTypesForDossier(TEST_DOSSIER_ID, false)).thenReturn(List.of(Type.builder()
|
||||
when(dictionaryClient.getAllTypesForDossier(TEST_DOSSIER_ID, true)).thenReturn(List.of(Type.builder()
|
||||
.id(DOSSIER_REDACTIONS_INDICATOR + ":" + TEST_DOSSIER_TEMPLATE_ID)
|
||||
.type(DOSSIER_REDACTIONS_INDICATOR)
|
||||
.dossierTemplateId(TEST_DOSSIER_ID)
|
||||
@@ -101,6 +103,25 @@ public class RedactionAcceptanceTest extends AbstractRedactionIntegrationTest {
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void authorNameSplittingTest() {
|
||||
|
||||
AnalyzeRequest request = uploadFileToStorage("files/Minimal Examples/tableWithAuthors.pdf");
|
||||
|
||||
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||
AnalyzeResult result = analyzeService.analyze(request);
|
||||
|
||||
var entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
|
||||
var recommendations = entityLog.getEntityLogEntry()
|
||||
.stream()
|
||||
.filter(entityLogEntry -> entityLogEntry.getEntryType().equals(EntryType.RECOMMENDATION))
|
||||
.map(EntityLogEntry::getValue)
|
||||
.toList();
|
||||
|
||||
assertThat(recommendations).containsExactlyInAnyOrder("Michael N.", "Funnarie B.", "Feuer A.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptanceTests() throws IOException {
|
||||
|
||||
@@ -146,7 +167,8 @@ public class RedactionAcceptanceTest extends AbstractRedactionIntegrationTest {
|
||||
return redactionLog.getEntityLogEntry()
|
||||
.stream()
|
||||
.filter(entry -> entry.getType().equals(type))
|
||||
.filter(entry -> entry.getValue().equals(value)).filter(entry -> entry.getContainingNodeId().get(0).equals(sectionNumber.get(0)));
|
||||
.filter(entry -> entry.getValue().equals(value))
|
||||
.filter(entry -> entry.getContainingNodeId().get(0).equals(sectionNumber.get(0)));
|
||||
}
|
||||
|
||||
|
||||
@@ -185,7 +207,7 @@ public class RedactionAcceptanceTest extends AbstractRedactionIntegrationTest {
|
||||
|
||||
private static IdRemoval buildIdRemoval(String id) {
|
||||
|
||||
return IdRemoval.builder().annotationId(id).requestDate(OffsetDateTime.now()).status(AnnotationStatus.APPROVED).fileId(TEST_FILE_ID).build();
|
||||
return IdRemoval.builder().annotationId(id).requestDate(OffsetDateTime.now()).fileId(TEST_FILE_ID).build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+364
-81
@@ -2,6 +2,7 @@ package com.iqser.red.service.redaction.v1.server;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -19,6 +20,7 @@ import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -40,12 +42,14 @@ import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.iqser.red.commons.jackson.ObjectMapperFactory;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.AnalyzeRequest;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.AnalyzeResult;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.FileAttribute;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.RuleFileType;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.AnnotationStatus;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.Comment;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntityLogEntry;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntryState;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntryType;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.ManualRedactions;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.Rectangle;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.IdRemoval;
|
||||
@@ -59,7 +63,6 @@ import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemp
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.file.FileType;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.type.Type;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Point;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLogEntry;
|
||||
import com.iqser.red.service.redaction.v1.server.annotate.AnnotateRequest;
|
||||
import com.iqser.red.service.redaction.v1.server.annotate.AnnotateResponse;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.TextRange;
|
||||
@@ -69,6 +72,7 @@ import com.iqser.red.service.redaction.v1.server.service.document.DocumentGraphM
|
||||
import com.iqser.red.service.redaction.v1.server.storage.RedactionStorageService;
|
||||
import com.iqser.red.storage.commons.StorageAutoConfiguration;
|
||||
import com.iqser.red.storage.commons.service.StorageService;
|
||||
import com.iqser.red.storage.commons.utils.FileSystemBackedStorageService;
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.queue.LayoutParsingType;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.LayoutParsingServiceProcessorConfiguration;
|
||||
import com.knecon.fforesight.tenantcommons.TenantContext;
|
||||
@@ -92,7 +96,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
||||
@Primary
|
||||
public StorageService inmemoryStorage() {
|
||||
|
||||
return new FileSystemBackedStorageService();
|
||||
return new FileSystemBackedStorageService(ObjectMapperFactory.create());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -111,10 +115,10 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
||||
loadTypeForTest();
|
||||
loadNerForTest();
|
||||
when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(0L);
|
||||
when(dictionaryClient.getAllTypesForDossierTemplate(TEST_DOSSIER_TEMPLATE_ID, false)).thenReturn(getTypeResponse());
|
||||
when(dictionaryClient.getAllTypesForDossierTemplate(TEST_DOSSIER_TEMPLATE_ID, true)).thenReturn(getTypeResponse());
|
||||
|
||||
when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(0L);
|
||||
when(dictionaryClient.getAllTypesForDossier(TEST_DOSSIER_ID, false)).thenReturn(List.of(Type.builder()
|
||||
when(dictionaryClient.getAllTypesForDossier(TEST_DOSSIER_ID, true)).thenReturn(List.of(Type.builder()
|
||||
.id(DOSSIER_REDACTIONS_INDICATOR + ":" + TEST_DOSSIER_TEMPLATE_ID)
|
||||
.type(DOSSIER_REDACTIONS_INDICATOR)
|
||||
.dossierTemplateId(TEST_DOSSIER_ID)
|
||||
@@ -161,11 +165,11 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
||||
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||
AnalyzeResult result = analyzeService.analyze(request);
|
||||
|
||||
Map<String, List<RedactionLogEntry>> duplicates = new HashMap<>();
|
||||
Map<String, List<EntityLogEntry>> duplicates = new HashMap<>();
|
||||
|
||||
var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
var entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
|
||||
redactionLog.getRedactionLogEntry().forEach(entry -> {
|
||||
entityLog.getEntityLogEntry().forEach(entry -> {
|
||||
duplicates.computeIfAbsent(entry.getId(), v -> new ArrayList<>()).add(entry);
|
||||
});
|
||||
|
||||
@@ -192,6 +196,54 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void manualResizeTest() throws IOException {
|
||||
|
||||
// c6be5277f5ee60dc3d83527798b7fe02
|
||||
// Rectangle(topLeft=Point(x=236.8, y=182.90005), width=70.89604, height=12.642, page=7)
|
||||
|
||||
// {
|
||||
// "page": 7,
|
||||
// "topLeft": {
|
||||
// "x": 200.8,
|
||||
// "y": 332.364
|
||||
// },
|
||||
// "height": 15.408000000000015,
|
||||
// "width": 40.584
|
||||
// }
|
||||
|
||||
AnalyzeRequest request = uploadFileToStorage("files/new/crafted document.pdf");
|
||||
|
||||
ManualRedactions manualRedactions = ManualRedactions.builder()
|
||||
.resizeRedactions(Set.of(ManualResizeRedaction.builder()
|
||||
.annotationId("c6be5277f5ee60dc3d83527798b7fe02")
|
||||
.value("Dr. Alan")
|
||||
.positions(List.of(new Rectangle(236.8f, 182.90005f, 40.584f, 12.642f, 7)))
|
||||
.requestDate(OffsetDateTime.now())
|
||||
.updateDictionary(false)
|
||||
.build()))
|
||||
.build();
|
||||
|
||||
request.setManualRedactions(manualRedactions);
|
||||
|
||||
System.out.println("Start Full integration test");
|
||||
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||
System.out.println("Finished structure analysis");
|
||||
AnalyzeResult result = analyzeService.analyze(request);
|
||||
System.out.println("Finished analysis");
|
||||
var redactionLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
|
||||
AnnotateResponse annotateResponse = annotationService.annotate(AnnotateRequest.builder().dossierId(TEST_DOSSIER_ID).fileId(TEST_FILE_ID).build());
|
||||
|
||||
String outputFileName = OsUtils.getTemporaryDirectory() + "/Annotated.pdf";
|
||||
|
||||
try (FileOutputStream fileOutputStream = new FileOutputStream(outputFileName)) {
|
||||
fileOutputStream.write(annotateResponse.getDocument());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void redactionExpansionOverlap() {
|
||||
|
||||
@@ -202,24 +254,52 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
||||
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||
AnalyzeResult result = analyzeService.analyze(request);
|
||||
|
||||
var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
var redactionLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
|
||||
var values = redactionLog.getRedactionLogEntry().stream().map(RedactionLogEntry::getValue).collect(Collectors.toList());
|
||||
var values = redactionLog.getEntityLogEntry().stream().map(EntityLogEntry::getValue).collect(Collectors.toList());
|
||||
|
||||
assertThat(values).containsExactlyInAnyOrder("Lastname M.", "Doe", "Doe J.", "M. Mustermann", "Mustermann M.", "F. Lastname");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void titleExtraction() throws IOException {
|
||||
public void importedRedactionsTest() throws IOException {
|
||||
|
||||
AnalyzeRequest request = uploadFileToStorage("files/new/crafted document.pdf");
|
||||
ClassPathResource importedRedactionClasspathResource = new ClassPathResource(
|
||||
"files/ImportedRedactions/18 Chlorothalonil RAR 08 Volume 3CA B 6a Oct 2017.IMPORTED_REDACTIONS.json");
|
||||
storageService.storeObject(TenantContext.getTenantId(),
|
||||
RedactionStorageService.StorageIdUtils.getStorageId(TEST_DOSSIER_ID, TEST_FILE_ID, FileType.IMPORTED_REDACTIONS),
|
||||
importedRedactionClasspathResource.getInputStream());
|
||||
|
||||
AnalyzeRequest request = uploadFileToStorage("files/ImportedRedactions/18 Chlorothalonil RAR 08 Volume 3CA B 6a Oct 2017.pdf");
|
||||
System.out.println("Start Full integration test");
|
||||
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||
System.out.println("Finished structure analysis");
|
||||
AnalyzeResult result = analyzeService.analyze(request);
|
||||
System.out.println("Finished analysis");
|
||||
var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
var entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
|
||||
AnnotateResponse annotateResponse = annotationService.annotate(AnnotateRequest.builder().dossierId(TEST_DOSSIER_ID).fileId(TEST_FILE_ID).build());
|
||||
|
||||
String outputFileName = OsUtils.getTemporaryDirectory() + "/Annotated.pdf";
|
||||
|
||||
try (FileOutputStream fileOutputStream = new FileOutputStream(outputFileName)) {
|
||||
fileOutputStream.write(annotateResponse.getDocument());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void titleExtraction() throws IOException {
|
||||
|
||||
AnalyzeRequest request = uploadFileToStorage("files/Metolachlor/S-Metolachlor_RAR_01_Volume_1_2018-09-06.pdf");
|
||||
System.out.println("Start Full integration test");
|
||||
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||
System.out.println("Finished structure analysis");
|
||||
AnalyzeResult result = analyzeService.analyze(request);
|
||||
System.out.println("Finished analysis");
|
||||
var entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
|
||||
AnnotateResponse annotateResponse = annotationService.annotate(AnnotateRequest.builder().dossierId(TEST_DOSSIER_ID).fileId(TEST_FILE_ID).build());
|
||||
|
||||
@@ -257,11 +337,10 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
||||
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||
analyzeService.analyze(request);
|
||||
|
||||
var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
var entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
|
||||
var toRemove = IdRemoval.builder()
|
||||
.annotationId("c630599611e6e3db314518374bcf70f7")
|
||||
.status(AnnotationStatus.APPROVED)
|
||||
.user("test")
|
||||
.removeFromDictionary(false)
|
||||
.processedDate(OffsetDateTime.now())
|
||||
@@ -272,13 +351,13 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
||||
request.setManualRedactions(manualRedactions);
|
||||
analyzeService.reanalyze(request);
|
||||
|
||||
var mergedRedactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
var mergedEntityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
|
||||
var cbiAddressBeforeHintRemoval = redactionLog.getRedactionLogEntry().stream().filter(re -> re.getType().equalsIgnoreCase("CBI_Address")).findAny().get();
|
||||
assertThat(cbiAddressBeforeHintRemoval.isRedacted()).isFalse();
|
||||
var cbiAddressBeforeHintRemoval = entityLog.getEntityLogEntry().stream().filter(re -> re.getType().equalsIgnoreCase("CBI_Address")).findAny().get();
|
||||
assertThat(cbiAddressBeforeHintRemoval.getState().equals(EntryState.APPLIED)).isFalse();
|
||||
|
||||
var cbiAddressAfterHintRemoval = mergedRedactionLog.getRedactionLogEntry().stream().filter(re -> re.getType().equalsIgnoreCase("CBI_Address")).findAny().get();
|
||||
assertThat(cbiAddressAfterHintRemoval.isRedacted()).isTrue();
|
||||
var cbiAddressAfterHintRemoval = mergedEntityLog.getEntityLogEntry().stream().filter(re -> re.getType().equalsIgnoreCase("CBI_Address")).findAny().get();
|
||||
assertThat(cbiAddressAfterHintRemoval.getState().equals(EntryState.APPLIED)).isTrue();
|
||||
}
|
||||
|
||||
|
||||
@@ -303,11 +382,11 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
||||
AnalyzeResult result = analyzeService.analyze(request);
|
||||
System.out.println("analysis analysis duration: " + (System.currentTimeMillis() - fstart));
|
||||
|
||||
Map<String, List<RedactionLogEntry>> duplicates = new HashMap<>();
|
||||
Map<String, List<EntityLogEntry>> duplicates = new HashMap<>();
|
||||
|
||||
var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
var entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
|
||||
redactionLog.getRedactionLogEntry().forEach(entry -> {
|
||||
entityLog.getEntityLogEntry().forEach(entry -> {
|
||||
duplicates.computeIfAbsent(entry.getId(), v -> new ArrayList<>()).add(entry);
|
||||
});
|
||||
|
||||
@@ -340,7 +419,6 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
||||
|
||||
long start = System.currentTimeMillis();
|
||||
AnalyzeRequest request = uploadFileToStorage(fileName);
|
||||
request.setExcludedPages(Set.of(1));
|
||||
|
||||
request.setFileAttributes(List.of(FileAttribute.builder()
|
||||
.id("fileAttributeId")
|
||||
@@ -352,7 +430,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
||||
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||
AnalyzeResult result = analyzeService.analyze(request);
|
||||
|
||||
var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
var entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
var documentGraph = DocumentGraphMapper.toDocumentGraph(redactionStorageService.getDocumentData(TEST_DOSSIER_ID, TEST_FILE_ID));
|
||||
|
||||
long end = System.currentTimeMillis();
|
||||
@@ -365,15 +443,15 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
||||
|
||||
int correctFound = 0;
|
||||
loop:
|
||||
for (RedactionLogEntry redactionLogEntry : redactionLog.getRedactionLogEntry()) {
|
||||
for (EntityLogEntry entityLogEntry : entityLog.getEntityLogEntry()) {
|
||||
for (Section section : documentGraph.getMainSections()) {
|
||||
if (redactionLogEntry.isImage()) {
|
||||
if (entityLogEntry.getEntryType().equals(EntryType.IMAGE)) {
|
||||
correctFound++;
|
||||
continue loop;
|
||||
}
|
||||
if (redactionLogEntry.getSectionNumber() == section.getTreeId().get(0)) {
|
||||
String value = section.getTextBlock().subSequence(new TextRange(redactionLogEntry.getStartOffset(), redactionLogEntry.getEndOffset())).toString();
|
||||
if (redactionLogEntry.getValue().equalsIgnoreCase(value)) {
|
||||
if (Objects.equals(entityLogEntry.getContainingNodeId().get(0), section.getTreeId().get(0))) {
|
||||
String value = section.getTextBlock().subSequence(new TextRange(entityLogEntry.getStartOffset(), entityLogEntry.getEndOffset())).toString();
|
||||
if (entityLogEntry.getValue().equalsIgnoreCase(value)) {
|
||||
correctFound++;
|
||||
} else {
|
||||
throw new RuntimeException("WTF");
|
||||
@@ -414,7 +492,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
||||
|
||||
AnalyzeResult reanalyzeResult = analyzeService.reanalyze(request);
|
||||
|
||||
redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
|
||||
end = System.currentTimeMillis();
|
||||
System.out.println("reanalysis analysis duration: " + (end - start));
|
||||
@@ -434,7 +512,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
||||
|
||||
analyzeService.reanalyze(request);
|
||||
|
||||
redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
|
||||
System.out.println("Output file:" + outputFileName);
|
||||
}
|
||||
@@ -468,9 +546,9 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
||||
mockDictionaryCalls(3L);
|
||||
analyzeService.reanalyze(request);
|
||||
|
||||
var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
var entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
|
||||
var changes = redactionLog.getRedactionLogEntry().stream().filter(entry -> entry.getValue() != null && entry.getValue().equals("report")).findFirst().get().getChanges();
|
||||
var changes = entityLog.getEntityLogEntry().stream().filter(entry -> entry.getValue() != null && entry.getValue().equals("report")).findFirst().get().getChanges();
|
||||
|
||||
assertThat(changes.size()).isEqualTo(2);
|
||||
|
||||
@@ -495,7 +573,6 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
||||
|
||||
long start = System.currentTimeMillis();
|
||||
AnalyzeRequest request = uploadFileToStorage(fileName);
|
||||
request.setExcludedPages(Set.of(1));
|
||||
|
||||
request.setFileAttributes(List.of(FileAttribute.builder()
|
||||
.id("fileAttributeId")
|
||||
@@ -507,7 +584,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
||||
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||
AnalyzeResult result = analyzeService.analyze(request);
|
||||
|
||||
var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
var entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
var documentGraph = DocumentGraphMapper.toDocumentGraph(redactionStorageService.getDocumentData(TEST_DOSSIER_ID, TEST_FILE_ID));
|
||||
|
||||
long end = System.currentTimeMillis();
|
||||
@@ -518,13 +595,13 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
||||
fileOutputStream.write(objectMapper.writeValueAsBytes(redactionStorageService.getDocumentData(TEST_DOSSIER_ID, TEST_FILE_ID)));
|
||||
}
|
||||
|
||||
List<String> valuesInDocument = redactionLog.getRedactionLogEntry()
|
||||
List<String> valuesInDocument = entityLog.getEntityLogEntry()
|
||||
.stream()
|
||||
.filter(e -> !e.isImage())
|
||||
.filter(e -> !e.getEntryType().equals(EntryType.IMAGE))
|
||||
.map(redactionLogEntry -> new TextRange(redactionLogEntry.getStartOffset(), redactionLogEntry.getEndOffset()))
|
||||
.map(boundary -> documentGraph.getTextBlock().subSequence(boundary).toString())
|
||||
.toList();
|
||||
List<String> valuesInRedactionLog = redactionLog.getRedactionLogEntry().stream().filter(e -> !e.isImage()).map(RedactionLogEntry::getValue).toList();
|
||||
List<String> valuesInRedactionLog = entityLog.getEntityLogEntry().stream().filter(e -> !e.getEntryType().equals(EntryType.IMAGE)).map(EntityLogEntry::getValue).toList();
|
||||
|
||||
assertEquals(valuesInRedactionLog, valuesInDocument);
|
||||
|
||||
@@ -553,7 +630,6 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
||||
manualRedactions.setRecategorizations(Set.of(ManualRecategorization.builder()
|
||||
.annotationId("37eee3e9d589a5cc529bfec38c3ba479")
|
||||
.fileId("fileId")
|
||||
.status(AnnotationStatus.APPROVED)
|
||||
.type("signature")
|
||||
.requestDate(OffsetDateTime.now())
|
||||
.build()));
|
||||
@@ -562,7 +638,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
||||
|
||||
AnalyzeResult reanalyzeResult = analyzeService.reanalyze(request);
|
||||
|
||||
redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
|
||||
end = System.currentTimeMillis();
|
||||
System.out.println("reanalysis analysis duration: " + (end - start));
|
||||
@@ -583,7 +659,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
||||
|
||||
analyzeService.reanalyze(request);
|
||||
|
||||
redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
|
||||
System.out.println("Output file:" + outputFileName);
|
||||
}
|
||||
@@ -609,7 +685,6 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
||||
.add(IdRemoval.builder()
|
||||
.annotationId("308dab9015bfafd911568cffe0a7f7de")
|
||||
.fileId(TEST_FILE_ID)
|
||||
.status(AnnotationStatus.APPROVED)
|
||||
.requestDate(OffsetDateTime.of(2022, 05, 23, 8, 30, 07, 475479, ZoneOffset.UTC))
|
||||
.processedDate(OffsetDateTime.of(2022, 05, 23, 8, 30, 07, 483651, ZoneOffset.UTC))
|
||||
.build());
|
||||
@@ -618,7 +693,6 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
||||
.add(ManualForceRedaction.builder()
|
||||
.annotationId("0b56ea1a87c83f351df177315af94f0d")
|
||||
.fileId(TEST_FILE_ID)
|
||||
.status(AnnotationStatus.APPROVED)
|
||||
.legalBasis("Something")
|
||||
.requestDate(OffsetDateTime.of(2022, 05, 23, 9, 30, 15, 4653, ZoneOffset.UTC))
|
||||
.processedDate(OffsetDateTime.of(2022, 05, 23, 9, 30, 15, 794, ZoneOffset.UTC))
|
||||
@@ -628,7 +702,6 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
||||
.add(IdRemoval.builder()
|
||||
.annotationId("0b56ea1a87c83f351df177315af94f0d")
|
||||
.fileId(TEST_FILE_ID)
|
||||
.status(AnnotationStatus.APPROVED)
|
||||
.requestDate(OffsetDateTime.of(2022, 05, 23, 8, 30, 23, 961721, ZoneOffset.UTC))
|
||||
.processedDate(OffsetDateTime.of(2022, 05, 23, 8, 30, 23, 96528, ZoneOffset.UTC))
|
||||
.build());
|
||||
@@ -810,8 +883,8 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
||||
fileOutputStream.write(annotateResponse.getDocument());
|
||||
}
|
||||
|
||||
var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
assertThat(redactionLog.getRedactionLogEntry().size()).isEqualTo(5);
|
||||
var entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
assertThat(entityLog.getEntityLogEntry().size()).isEqualTo(5);
|
||||
|
||||
long end = System.currentTimeMillis();
|
||||
|
||||
@@ -838,26 +911,25 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
||||
AnalyzeRequest request = uploadFileToStorage("files/new/S157.pdf");
|
||||
|
||||
ManualRedactions manualRedactions = new ManualRedactions();
|
||||
|
||||
manualRedactions.getResizeRedactions()
|
||||
.add(ManualResizeRedaction.builder()
|
||||
.annotationId("ca2b437e2480a4b5966cb8386020d454")
|
||||
.fileId(TEST_FILE_ID)
|
||||
.status(AnnotationStatus.APPROVED)
|
||||
.requestDate(OffsetDateTime.of(2022, 05, 23, 8, 30, 07, 475479, ZoneOffset.UTC))
|
||||
.processedDate(OffsetDateTime.of(2022, 05, 23, 8, 30, 07, 483651, ZoneOffset.UTC))
|
||||
.value("Bera P")
|
||||
.positions(List.of(new Rectangle(382.12485f, 235.94246f, 8.768621f, -21.632504f, 1)))
|
||||
.textBefore("")
|
||||
.textAfter("")
|
||||
.build());
|
||||
ManualResizeRedaction manualResizeRedaction = ManualResizeRedaction.builder()
|
||||
.annotationId("ca2b437e2480a4b5966cb8386020d454")
|
||||
.fileId(TEST_FILE_ID)
|
||||
.requestDate(OffsetDateTime.of(2022, 05, 23, 8, 30, 07, 475479, ZoneOffset.UTC))
|
||||
.processedDate(OffsetDateTime.of(2022, 05, 23, 8, 30, 07, 483651, ZoneOffset.UTC))
|
||||
.value("Bera P")
|
||||
.positions(List.of(new Rectangle(382.12485f, 235.94246f, 8.768621f, -21.632504f, 1)))
|
||||
.textBefore("")
|
||||
.updateDictionary(false)
|
||||
.textAfter("")
|
||||
.build();
|
||||
manualRedactions.getResizeRedactions().add(manualResizeRedaction);
|
||||
|
||||
request.setManualRedactions(manualRedactions);
|
||||
|
||||
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||
AnalyzeResult result = analyzeService.analyze(request);
|
||||
|
||||
var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
var entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
|
||||
AnnotateResponse annotateResponse = annotationService.annotate(AnnotateRequest.builder()
|
||||
.manualRedactions(manualRedactions)
|
||||
@@ -878,6 +950,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
||||
|
||||
|
||||
@Test
|
||||
@Disabled // TODO Figure out why this changed
|
||||
public void phantomCellsDocumentTest() {
|
||||
|
||||
AnalyzeRequest request = uploadFileToStorage("files/Minimal Examples/Phantom Cells.pdf");
|
||||
@@ -885,11 +958,15 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
||||
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||
AnalyzeResult result = analyzeService.analyze(request);
|
||||
|
||||
var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
var redactionLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
|
||||
redactionLog.getRedactionLogEntry().forEach(entry -> {
|
||||
if (!entry.isHint()) {
|
||||
assertThat(entry.getReason()).isEqualTo("Not redacted because it's row does not belong to a vertebrate study");
|
||||
redactionLog.getEntityLogEntry().forEach(entry -> {
|
||||
if (!entry.getEntryType().equals(EntryType.HINT)) {
|
||||
if (entry.getType().equals("CBI_author")) {
|
||||
assertThat(entry.getReason()).isEqualTo("Not redacted because it's row does not belong to a vertebrate study");
|
||||
} else if (entry.getType().equals("CBI_address")) {
|
||||
assertThat(entry.getReason()).isEqualTo("No vertebrate found");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -927,11 +1004,9 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
||||
|
||||
String manualAddId = UUID.randomUUID().toString();
|
||||
|
||||
Comment comment = Comment.builder().date(OffsetDateTime.now()).user("TEST_USER").text("This is a comment test").build();
|
||||
manualRedactions.setIdsToRemove(Set.of(IdRemoval.builder()
|
||||
.annotationId("5b940b2cb401ed9f5be6fc24f6e77bcf")
|
||||
.fileId("fileId")
|
||||
.status(AnnotationStatus.DECLINED)
|
||||
.processedDate(OffsetDateTime.now())
|
||||
.requestDate(OffsetDateTime.now())
|
||||
.build()));
|
||||
@@ -939,19 +1014,13 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
||||
.annotationId("675eba69b0c2917de55462c817adaa05")
|
||||
.fileId("fileId")
|
||||
.legalBasis("Something")
|
||||
.status(AnnotationStatus.APPROVED)
|
||||
.requestDate(OffsetDateTime.now())
|
||||
.processedDate(OffsetDateTime.now())
|
||||
.build()));
|
||||
|
||||
manualRedactions.getComments().put("e5be0f1d941bbb92a068e198648d06c4", List.of(comment));
|
||||
manualRedactions.getComments().put("0836727c3508a0b2ea271da69c04cc2f", List.of(comment));
|
||||
manualRedactions.getComments().put(manualAddId, List.of(comment));
|
||||
|
||||
ManualRedactionEntry manualRedactionEntry = new ManualRedactionEntry();
|
||||
manualRedactionEntry.setAnnotationId(manualAddId);
|
||||
manualRedactionEntry.setFileId("fileId");
|
||||
manualRedactionEntry.setStatus(AnnotationStatus.APPROVED);
|
||||
manualRedactionEntry.setType("CBI_author");
|
||||
manualRedactionEntry.setValue("O'Loughlin C.K.");
|
||||
manualRedactionEntry.setReason("Manual Redaction");
|
||||
@@ -971,7 +1040,6 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
||||
manualRedactions.setIdsToRemove(Set.of(IdRemoval.builder()
|
||||
.annotationId("5b940b2cb401ed9f5be6fc24f6e77bcf")
|
||||
.fileId("fileId")
|
||||
.status(AnnotationStatus.APPROVED)
|
||||
.requestDate(OffsetDateTime.now())
|
||||
.processedDate(OffsetDateTime.now())
|
||||
.build()));
|
||||
@@ -979,7 +1047,6 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
||||
.annotationId("675eba69b0c2917de55462c817adaa05")
|
||||
.fileId("fileId")
|
||||
.legalBasis("Manual Legal Basis Change")
|
||||
.status(AnnotationStatus.APPROVED)
|
||||
.processedDate(OffsetDateTime.now())
|
||||
.requestDate(OffsetDateTime.now())
|
||||
.build())));
|
||||
@@ -993,7 +1060,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
||||
Rectangle.builder().topLeftX(298.67056f).topLeftY(327.567f).width(7.648041f).height(75.32377f).page(1).build(),
|
||||
Rectangle.builder().topLeftX(307.89517f).topLeftY(327.567f).width(7.648041f).height(61.670967f).page(1).build(),
|
||||
Rectangle.builder().topLeftX(316.99985f).topLeftY(327.567f).width(7.648041f).height(38.104286f).page(1).build()))
|
||||
.status(AnnotationStatus.APPROVED)
|
||||
.updateDictionary(false)
|
||||
.build()));
|
||||
|
||||
analyzeService.reanalyze(request);
|
||||
@@ -1035,6 +1102,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
||||
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
public void testImportedRedactions() throws IOException {
|
||||
|
||||
String outputFileName = OsUtils.getTemporaryDirectory() + "/ImportedRedactions.pdf";
|
||||
@@ -1048,7 +1116,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
||||
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||
AnalyzeResult result = analyzeService.analyze(request);
|
||||
|
||||
var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
var entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
|
||||
AnnotateResponse annotateResponse = annotationService.annotate(AnnotateRequest.builder().dossierId(TEST_DOSSIER_ID).fileId(TEST_FILE_ID).build());
|
||||
|
||||
@@ -1056,7 +1124,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
||||
fileOutputStream.write(annotateResponse.getDocument());
|
||||
}
|
||||
|
||||
redactionLog.getRedactionLogEntry().forEach(entry -> {
|
||||
entityLog.getEntityLogEntry().forEach(entry -> {
|
||||
if (entry.getValue() == null) {
|
||||
return;
|
||||
}
|
||||
@@ -1094,12 +1162,227 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
||||
fileOutputStream.write(annotateResponse.getDocument());
|
||||
}
|
||||
|
||||
var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
var values = redactionLog.getRedactionLogEntry().stream().map(RedactionLogEntry::getValue).collect(Collectors.toList());
|
||||
var entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
var values = entityLog.getEntityLogEntry().stream().map(EntityLogEntry::getValue).collect(Collectors.toList());
|
||||
|
||||
assertThat(values).contains("Mrs. Robinson");
|
||||
assertThat(values).contains("Mr. Bojangles");
|
||||
assertThat(values).contains("Mr. Tambourine Man");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void signaturesAreRedactionAfterReanalyse() throws IOException {
|
||||
|
||||
AnalyzeRequest request = uploadFileToStorage("files/new/SYNGENTA_EFSA_sanitisation_GFL_v1 (1).pdf");
|
||||
ClassPathResource imageServiceResponseFileResource = new ClassPathResource("files/new/SYNGENTA_EFSA_sanitisation_GFL_v1 (1).IMAGE_INFO.json");
|
||||
|
||||
storageService.storeObject(TenantContext.getTenantId(),
|
||||
RedactionStorageService.StorageIdUtils.getStorageId(TEST_DOSSIER_ID, TEST_FILE_ID, FileType.IMAGE_INFO),
|
||||
imageServiceResponseFileResource.getInputStream());
|
||||
|
||||
System.out.println("Start Full integration test");
|
||||
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||
System.out.println("Finished structure analysis");
|
||||
analyzeService.analyze(request);
|
||||
System.out.println("Finished analysis");
|
||||
|
||||
request.setManualRedactions(ManualRedactions.builder()
|
||||
.legalBasisChanges(Set.of(ManualLegalBasisChange.builder()
|
||||
.annotationId("3029651d0842a625f2d23f8375c23600")
|
||||
.section("[19, 2]: Paragraph: Contact point: LexCo Contact:")
|
||||
.value("0049 331 441 551 14")
|
||||
.requestDate(OffsetDateTime.now())
|
||||
.fileId(TEST_FILE_ID)
|
||||
.legalBasis("Article 39(e)(2) of Regulation (EC) No 178/2002")
|
||||
.build()))
|
||||
.build());
|
||||
|
||||
analyzeService.reanalyze(request);
|
||||
System.out.println("Finished reanalysis");
|
||||
|
||||
var entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
entityLog.getEntityLogEntry().stream().filter(entityLogEntry -> entityLogEntry.getType().equals("signature")).forEach(entityLogEntry -> {
|
||||
assertThat(entityLogEntry.getState() == EntryState.APPLIED).isTrue();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void entityIsAppliedAfterRecateorize() throws IOException {
|
||||
|
||||
AnalyzeRequest request = uploadFileToStorage("files/new/SYNGENTA_EFSA_sanitisation_GFL_v1 (1).pdf");
|
||||
ClassPathResource imageServiceResponseFileResource = new ClassPathResource("files/new/SYNGENTA_EFSA_sanitisation_GFL_v1 (1).IMAGE_INFO.json");
|
||||
|
||||
storageService.storeObject(TenantContext.getTenantId(),
|
||||
RedactionStorageService.StorageIdUtils.getStorageId(TEST_DOSSIER_ID, TEST_FILE_ID, FileType.IMAGE_INFO),
|
||||
imageServiceResponseFileResource.getInputStream());
|
||||
|
||||
System.out.println("Start Full integration test");
|
||||
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||
System.out.println("Finished structure analysis");
|
||||
analyzeService.analyze(request);
|
||||
System.out.println("Finished analysis");
|
||||
|
||||
request.setManualRedactions(ManualRedactions.builder()
|
||||
.legalBasisChanges(Set.of(ManualLegalBasisChange.builder()
|
||||
.annotationId("3029651d0842a625f2d23f8375c23600")
|
||||
.section("[19, 2]: Paragraph: Contact point: LexCo Contact:")
|
||||
.value("0049 331 441 551 14")
|
||||
.requestDate(OffsetDateTime.now())
|
||||
.fileId(TEST_FILE_ID)
|
||||
.legalBasis("Article 39(e)(2) of Regulation (EC) No 178/2002")
|
||||
.build()))
|
||||
.recategorizations(Set.of(ManualRecategorization.builder()
|
||||
.annotationId("3029651d0842a625f2d23f8375c23600")
|
||||
.type("CBI_author")
|
||||
.legalBasis("")
|
||||
.requestDate(OffsetDateTime.now())
|
||||
.fileId(TEST_FILE_ID)
|
||||
.build()))
|
||||
.build());
|
||||
|
||||
analyzeService.reanalyze(request);
|
||||
System.out.println("Finished reanalysis");
|
||||
|
||||
var entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
var changedAnnotation = entityLog.getEntityLogEntry()
|
||||
.stream()
|
||||
.filter(entityLogEntry -> entityLogEntry.getId().equals("3029651d0842a625f2d23f8375c23600"))
|
||||
.findFirst()
|
||||
.get();
|
||||
|
||||
assertThat(changedAnnotation.getState() == EntryState.APPLIED).isTrue();
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
@SneakyThrows
|
||||
public void testRemovingAndAddingRedactionOnTheSameValue() {
|
||||
|
||||
String pdfFile = "files/new/test1S1T1.pdf";
|
||||
|
||||
String manualAddId = UUID.randomUUID().toString();
|
||||
String manualAddId2 = UUID.randomUUID().toString();
|
||||
List<Rectangle> positions = List.of(Rectangle.builder().topLeftX(305.35f).topLeftY(332.5033f).width(71.40744f).height(13.645125f).page(1).build());
|
||||
ManualRedactionEntry manualRedactionEntry = getManualRedactionEntry(manualAddId,
|
||||
positions,
|
||||
"the manufacturing or production process, including the method and innovative aspects thereof, as well as other technical and industrial specifications inherent to that process or method, except for information which is relevant to the assessment of safety");
|
||||
ManualRedactionEntry manualRedactionEntry2 = getManualRedactionEntry(manualAddId2,
|
||||
positions,
|
||||
"commercial information revealing sourcing, market shares or business strategy of the applicant");
|
||||
|
||||
IdRemoval idRemoval = getIdRemoval(manualAddId);
|
||||
IdRemoval idRemoval2 = getIdRemoval(manualAddId2);
|
||||
|
||||
AnalyzeRequest request = uploadFileToStorage(pdfFile);
|
||||
request.setManualRedactions(ManualRedactions.builder().entriesToAdd(Set.of(manualRedactionEntry)).build());
|
||||
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||
analyzeService.analyze(request);
|
||||
|
||||
var entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
|
||||
assertTrue(entityLog.getEntityLogEntry().stream().anyMatch(entityLogEntry -> entityLogEntry.getId().equals(manualAddId)));
|
||||
assertEquals(entityLog.getEntityLogEntry().stream().filter(entityLogEntry -> entityLogEntry.getId().equals(manualAddId)).findFirst().get().getState(), EntryState.APPLIED);
|
||||
|
||||
request.setManualRedactions(ManualRedactions.builder().entriesToAdd(Set.of(manualRedactionEntry)).idsToRemove(Set.of(idRemoval)).build());
|
||||
analyzeService.reanalyze(request);
|
||||
|
||||
entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
|
||||
assertTrue(entityLog.getEntityLogEntry().stream().anyMatch(entityLogEntry -> entityLogEntry.getId().equals(manualAddId)));
|
||||
assertEquals(entityLog.getEntityLogEntry().stream().filter(entityLogEntry -> entityLogEntry.getId().equals(manualAddId)).findFirst().get().getState(), EntryState.REMOVED);
|
||||
|
||||
request.setManualRedactions(ManualRedactions.builder().entriesToAdd(Set.of(manualRedactionEntry, manualRedactionEntry2)).idsToRemove(Set.of(idRemoval)).build());
|
||||
analyzeService.reanalyze(request);
|
||||
|
||||
entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
|
||||
assertTrue(entityLog.getEntityLogEntry().stream().anyMatch(entityLogEntry -> entityLogEntry.getId().equals(manualAddId)));
|
||||
assertEquals(entityLog.getEntityLogEntry().stream().filter(entityLogEntry -> entityLogEntry.getId().equals(manualAddId)).findFirst().get().getState(), EntryState.REMOVED);
|
||||
assertTrue(entityLog.getEntityLogEntry().stream().anyMatch(entityLogEntry -> entityLogEntry.getId().equals(manualAddId2)));
|
||||
assertEquals(entityLog.getEntityLogEntry().stream().filter(entityLogEntry -> entityLogEntry.getId().equals(manualAddId2)).findFirst().get().getState(), EntryState.APPLIED);
|
||||
|
||||
request.setManualRedactions(ManualRedactions.builder()
|
||||
.entriesToAdd(Set.of(manualRedactionEntry, manualRedactionEntry2))
|
||||
.idsToRemove(Set.of(idRemoval, idRemoval2))
|
||||
.build());
|
||||
analyzeService.reanalyze(request);
|
||||
|
||||
entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
|
||||
assertTrue(entityLog.getEntityLogEntry().stream().anyMatch(entityLogEntry -> entityLogEntry.getId().equals(manualAddId)));
|
||||
assertEquals(entityLog.getEntityLogEntry().stream().filter(entityLogEntry -> entityLogEntry.getId().equals(manualAddId)).findFirst().get().getState(), EntryState.REMOVED);
|
||||
assertTrue(entityLog.getEntityLogEntry().stream().anyMatch(entityLogEntry -> entityLogEntry.getId().equals(manualAddId2)));
|
||||
assertEquals(entityLog.getEntityLogEntry().stream().filter(entityLogEntry -> entityLogEntry.getId().equals(manualAddId2)).findFirst().get().getState(), EntryState.REMOVED);
|
||||
|
||||
manualRedactionEntry.setRequestDate(OffsetDateTime.now());
|
||||
request.setManualRedactions(ManualRedactions.builder()
|
||||
.entriesToAdd(Set.of(manualRedactionEntry, manualRedactionEntry2))
|
||||
.idsToRemove(Set.of(idRemoval, idRemoval2))
|
||||
.build());
|
||||
analyzeService.reanalyze(request);
|
||||
|
||||
entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
|
||||
assertTrue(entityLog.getEntityLogEntry().stream().anyMatch(entityLogEntry -> entityLogEntry.getId().equals(manualAddId)));
|
||||
assertEquals(entityLog.getEntityLogEntry().stream().filter(entityLogEntry -> entityLogEntry.getId().equals(manualAddId)).findFirst().get().getState(), EntryState.APPLIED);
|
||||
assertTrue(entityLog.getEntityLogEntry().stream().anyMatch(entityLogEntry -> entityLogEntry.getId().equals(manualAddId2)));
|
||||
assertEquals(entityLog.getEntityLogEntry().stream().filter(entityLogEntry -> entityLogEntry.getId().equals(manualAddId2)).findFirst().get().getState(), EntryState.REMOVED);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
@SneakyThrows
|
||||
public void testResizeWithUpdateDictionaryTrue() {
|
||||
|
||||
String pdfFile = "files/new/crafted document.pdf";
|
||||
|
||||
AnalyzeRequest request = uploadFileToStorage(pdfFile);
|
||||
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||
analyzeService.analyze(request);
|
||||
|
||||
var entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
var david = entityLog.getEntityLogEntry().stream().filter(e -> e.getValue().equals("David")).findFirst().get();
|
||||
|
||||
request.setManualRedactions(ManualRedactions.builder()
|
||||
.resizeRedactions(Set.of(ManualResizeRedaction.builder()
|
||||
.updateDictionary(true)
|
||||
.annotationId(david.getId())
|
||||
.requestDate(OffsetDateTime.now())
|
||||
.value("David Ksenia")
|
||||
.positions(List.of(Rectangle.builder().topLeftX(56.8f).topLeftY(293.564f).width(65.592f).height(15.408f).page(1).build()))
|
||||
.addToAllDossiers(false)
|
||||
.build()))
|
||||
.build());
|
||||
analyzeService.reanalyze(request);
|
||||
entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
var resizedEntity = entityLog.getEntityLogEntry().stream().filter(e -> e.getId().equals(david.getId())).findFirst().get();
|
||||
assertEquals(resizedEntity.getState(), EntryState.APPLIED);
|
||||
assertEquals(resizedEntity.getValue(), "David Ksenia");
|
||||
}
|
||||
|
||||
|
||||
private IdRemoval getIdRemoval(String id) {
|
||||
|
||||
return IdRemoval.builder().annotationId(id).removeFromAllDossiers(false).removeFromDictionary(false).requestDate(OffsetDateTime.now()).build();
|
||||
}
|
||||
|
||||
|
||||
private ManualRedactionEntry getManualRedactionEntry(String id, List<Rectangle> positions, String reason) {
|
||||
|
||||
ManualRedactionEntry manualRedactionEntry2 = new ManualRedactionEntry();
|
||||
manualRedactionEntry2.setAnnotationId(id);
|
||||
manualRedactionEntry2.setFileId("fileId");
|
||||
manualRedactionEntry2.setType("manual");
|
||||
manualRedactionEntry2.setRectangle(false);
|
||||
manualRedactionEntry2.setRequestDate(OffsetDateTime.now());
|
||||
manualRedactionEntry2.setValue("assessment");
|
||||
manualRedactionEntry2.setLegalBasis("Article 63(2)(a) of Regulation (EC) No 1107/2009 (making reference to Article 39 of Regulation EC No 178/2002)");
|
||||
manualRedactionEntry2.setReason(reason);
|
||||
manualRedactionEntry2.setPositions(positions);
|
||||
return manualRedactionEntry2;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+16
-16
@@ -20,13 +20,17 @@ import org.springframework.context.annotation.Import;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
|
||||
import com.iqser.red.commons.jackson.ObjectMapperFactory;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.AnalyzeRequest;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.RuleFileType;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.Engine;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntityLogEntry;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntryState;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.common.JSONPrimitive;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.type.Type;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLogEntry;
|
||||
import com.iqser.red.storage.commons.StorageAutoConfiguration;
|
||||
import com.iqser.red.storage.commons.service.StorageService;
|
||||
import com.iqser.red.storage.commons.utils.FileSystemBackedStorageService;
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.queue.LayoutParsingType;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.LayoutParsingServiceProcessorConfiguration;
|
||||
import com.knecon.fforesight.tenantcommons.TenantContext;
|
||||
@@ -51,7 +55,7 @@ public class RedactionIntegrationV2Test extends AbstractRedactionIntegrationTest
|
||||
@Primary
|
||||
public StorageService inmemoryStorage() {
|
||||
|
||||
return new FileSystemBackedStorageService();
|
||||
return new FileSystemBackedStorageService(ObjectMapperFactory.create());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -120,22 +124,20 @@ public class RedactionIntegrationV2Test extends AbstractRedactionIntegrationTest
|
||||
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||
analyzeService.analyze(request);
|
||||
|
||||
var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
var entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
|
||||
assertThat(redactionLog.getRedactionLogEntry().size()).isEqualTo(1);
|
||||
assertThat(entityLog.getEntityLogEntry().size()).isEqualTo(1);
|
||||
|
||||
RedactionLogEntry redactionLogEntry = redactionLog.getRedactionLogEntry().get(0);
|
||||
EntityLogEntry redactionLogEntry = entityLog.getEntityLogEntry().get(0);
|
||||
|
||||
assertThat(redactionLogEntry.getType()).isEqualTo(DICTIONARY_AUTHOR);
|
||||
assertThat(redactionLogEntry.getValue()).isEqualTo(entryAuthorAndPIIDictionary);
|
||||
assertThat(redactionLogEntry.isRedacted()).isEqualTo(true);
|
||||
assertThat(redactionLogEntry.isRecommendation()).isEqualTo(false);
|
||||
assertThat(redactionLogEntry.isFalsePositive()).isEqualTo(false);
|
||||
assertThat(redactionLogEntry.getState()).isEqualTo(EntryState.APPLIED);
|
||||
assertThat(redactionLogEntry.isExcluded()).isEqualTo(false);
|
||||
assertThat(redactionLogEntry.isDictionaryEntry()).isEqualTo(true);
|
||||
|
||||
assertThat(redactionLogEntry.getEngines().size()).isEqualTo(1);
|
||||
assertThat(redactionLogEntry.getEngines().contains(com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Engine.DICTIONARY)).isEqualTo(true);
|
||||
assertThat(redactionLogEntry.getEngines().contains(Engine.DICTIONARY)).isEqualTo(true);
|
||||
}
|
||||
|
||||
|
||||
@@ -158,22 +160,20 @@ public class RedactionIntegrationV2Test extends AbstractRedactionIntegrationTest
|
||||
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||
analyzeService.analyze(request);
|
||||
|
||||
var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
var redactionLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||
|
||||
assertThat(redactionLog.getRedactionLogEntry().size()).isEqualTo(1);
|
||||
assertThat(redactionLog.getEntityLogEntry().size()).isEqualTo(1);
|
||||
|
||||
RedactionLogEntry redactionLogEntry = redactionLog.getRedactionLogEntry().get(0);
|
||||
EntityLogEntry redactionLogEntry = redactionLog.getEntityLogEntry().get(0);
|
||||
|
||||
assertThat(redactionLogEntry.getType()).isEqualTo(DICTIONARY_AUTHOR);
|
||||
assertThat(redactionLogEntry.getValue()).isEqualTo(entryAuthorDictionary);
|
||||
assertThat(redactionLogEntry.isRedacted()).isEqualTo(true);
|
||||
assertThat(redactionLogEntry.isRecommendation()).isEqualTo(false);
|
||||
assertThat(redactionLogEntry.isFalsePositive()).isEqualTo(false);
|
||||
assertThat(redactionLogEntry.getState()).isEqualTo(EntryState.APPLIED);
|
||||
assertThat(redactionLogEntry.isExcluded()).isEqualTo(false);
|
||||
assertThat(redactionLogEntry.isDictionaryEntry()).isEqualTo(true);
|
||||
|
||||
assertThat(redactionLogEntry.getEngines().size()).isEqualTo(1);
|
||||
assertThat(redactionLogEntry.getEngines().contains(com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Engine.DICTIONARY)).isEqualTo(true);
|
||||
assertThat(redactionLogEntry.getEngines().contains(Engine.DICTIONARY)).isEqualTo(true);
|
||||
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user