Compare commits

..
349 Commits
Author SHA1 Message Date
Kilian Schüttler 9354cbb6bc Merge branch 'hotfix_removechain' into 'master'
Hotfix removechain

See merge request redactmanager/redaction-service!349
2024-03-27 16:27:09 +01:00
Kilian Schüttler 406f5a48cf Hotfix removechain 2024-03-27 16:27:09 +01:00
Christoph Schabert a71ee61e1b Update .gitlab-ci.yml 2024-03-27 15:41:37 +01:00
Andrei Isvoran 096544d8e2 Merge branch 'RED-8840-rules' into 'master'
RED-8840 - Adjust rules

Closes RED-8840

See merge request redactmanager/redaction-service!351
2024-03-27 15:33:49 +01:00
Andrei Isvoran 646f2020a8 RED-8840 - Adjust rules 2024-03-27 16:14:47 +02:00
Ali Oezyetimoglu 72a7761dd8 Merge branch 'RED-8480-B' into 'master'
RED-8480: addded property "value" to places with recategorizations

Closes RED-8480

See merge request redactmanager/redaction-service!348
2024-03-27 11:24:32 +01:00
Ali Oezyetimoglu c869a624e6 RED-8480: addded property "value" to places with recategorizations 2024-03-27 11:10:59 +01:00
Dominique Eifländer 3ca37133c6 Merge branch 'RED-8834' into 'master'
RED-8834: Fixed text entities with empty text range

Closes RED-8834

See merge request redactmanager/redaction-service!345
2024-03-27 09:59:33 +01:00
Dominique Eifländer 93baf97ce3 RED-8834: Fixed text entities with empty text range 2024-03-27 09:47:56 +01:00
Andrei Isvoran a0773d16dc Merge branch 'hotfix-get-dictionary-npe' into 'master'
Hotfix NPE on getDeepCopyDictionary

See merge request redactmanager/redaction-service!341
2024-03-26 10:36:02 +01:00
Andrei Isvoran c4816579c9 Hotfix NPE on getDeepCopyDictionary 2024-03-26 11:23:14 +02:00
Ali Oezyetimoglu c9e80e3fa1 Merge branch 'RED-8840' into 'master'
RED-8840 - Add PII.4 rules for sanitisation with correct legal basis

Closes RED-8840

See merge request redactmanager/redaction-service!337
2024-03-25 16:33:07 +01:00
Andrei Isvoran 677d62deca RED-8840 - Add PII.4 rules for sanitisation with correct legal basis 2024-03-25 16:33:07 +01:00
Kilian Schüttler 5229cd87f6 Merge branch 'RED-8610' into 'master'
RED-8610: Dictionary remove on dossier level should be displayed as skipped

Closes RED-8610

See merge request redactmanager/redaction-service!338
2024-03-25 14:13:44 +01:00
Kilian Schüttler 50e137575d RED-8610: Dictionary remove on dossier level should be displayed as skipped 2024-03-25 14:13:44 +01:00
Kilian Schüttler 7303a7579b Merge branch 'RED-8610' into 'master'
RED-8610: Dictionary remove on dossier level should be displayed as skipped

Closes RED-8610

See merge request redactmanager/redaction-service!335
2024-03-22 10:19:59 +01:00
Kilian Schüttler 2b4f174bdf RED-8610: Dictionary remove on dossier level should be displayed as skipped 2024-03-22 10:19:59 +01:00
Ali Oezyetimoglu d36d2ea2fa Merge branch 'RED-8480-B' into 'master'
RED-8480: updated code according to changes from ManualRecategorization

Closes RED-8480

See merge request redactmanager/redaction-service!332
2024-03-21 11:43:44 +01:00
Ali Oezyetimoglu b1c618505e RED-8480: updated code according to changes from ManualRecategorization 2024-03-20 18:44:20 +01:00
Andrei Isvoran 983f728248 Merge branch 'RED-8784' into 'master'
RED-8784 - Change PII.9.1/PII.9.2 to CBI.23.0/CBI.23.1

Closes RED-8784

See merge request redactmanager/redaction-service!326
2024-03-19 12:32:28 +01:00
Andrei Isvoran fe07e500b9 RED-8784 - Change PII.9.1/PII.9.2 to CBI.23.0/CBI.23.1 2024-03-19 12:32:28 +01:00
Dominique Eifländer 25015f633e Merge branch 'RED-7141' into 'master'
RED-7141: Fixed overlaps in duplicated and non duplicated blocks

Closes RED-7141

See merge request redactmanager/redaction-service!327
2024-03-18 14:37:52 +01:00
Dominique Eifländer 9b29d6f8f1 RED-7141: Fixed overlaps in duplicated and non duplicated blocks 2024-03-18 14:06:08 +01:00
Dominique Eifländer 16963d64ee Merge branch 'RED-7384' into 'master'
RED-7384: add useful fields to ManualRedactionEntry

Closes RED-7384

See merge request redactmanager/redaction-service!314
2024-03-18 13:17:15 +01:00
Kilian Schüttler a7674f406d RED-7384: add useful fields to ManualRedactionEntry 2024-03-18 13:17:15 +01:00
Andrei Isvoran 415daee3b6 Merge branch 'RED-8680-seeds' into 'master'
RED-8680 - Add specific CBI rules for seeds

Closes RED-8680

See merge request redactmanager/redaction-service!324
2024-03-18 10:58:18 +01:00
Andrei Isvoran d0a0bbc627 RED-8680 - Add specific CBI rules for seeds 2024-03-15 16:06:42 +02:00
Andrei Isvoran 93299c4e5f Merge branch 'RED-8705' into 'master'
RED-8705 - Fix image type

Closes RED-8705

See merge request redactmanager/redaction-service!321
2024-03-15 09:06:55 +01:00
Andrei Isvoran 72f8885ff0 RED-8705 - Fix image type 2024-03-14 17:15:14 +02:00
Andrei Isvoran cd6724b952 Merge branch 'RED-8680-seeds' into 'master'
RED-8680 - Add rules for syngenta sanitisation seeds

Closes RED-8680

See merge request redactmanager/redaction-service!320
2024-03-14 12:36:35 +01:00
Andrei Isvoran 3405cc6c1b RED-8680 - Add rules for syngenta sanitisation seeds 2024-03-14 10:59:37 +02:00
Andrei Isvoran 53ac78f788 Merge branch 'RED-8645-more-fixes-4.1' into 'master'
RED-8645 - Fix some more rules

Closes RED-8645

See merge request redactmanager/redaction-service!318
2024-03-13 08:48:01 +01:00
Andrei Isvoran 41821ff5ca RED-8645 - Fix some more rules 2024-03-12 15:45:38 +02:00
Dominique Eifländer f59dd7ff65 Merge branch 'RED-7384-4.1-2' into 'master'
RED-7384: Fixed missing requestDate in new created manualRedactions for resizes

Closes RED-7384

See merge request redactmanager/redaction-service!316
2024-03-12 12:12:51 +01:00
Dominique Eifländer 4e5718ba23 RED-7384: Fixed missing requestDate in new created manualRedactions for resizes 2024-03-12 12:01:13 +01:00
Dominique Eifländer e79bcbbc10 Merge branch 'RED-7384-4.1' into 'master'
RED-7384: Fixed migration problem for a specific file

Closes RED-7384

See merge request redactmanager/redaction-service!315
2024-03-12 09:48:40 +01:00
Dominique Eifländer d531f1be7f RED-7384: Fixed migration problem for a specific file 2024-03-12 09:35:53 +01:00
Dominique Eifländer 812946c81d Merge branch 'RED-7141' into 'master'
RED-7141: Adapted to layout parser using docstrum

Closes RED-7141

See merge request redactmanager/redaction-service!312
2024-03-08 15:08:23 +01:00
Dominique Eifländer b93f9a2c20 RED-7141: Adapted to layout parser using docstrum 2024-03-08 14:54:48 +01:00
Andrei Isvoran fa55917a89 Merge branch 'RED-8645-rules' into 'master'
RED-8645 - Update RM rules

Closes RED-8645

See merge request redactmanager/redaction-service!309
2024-03-08 14:34:07 +01:00
Andrei Isvoran d00847e955 RED-8645 - Update RM rules 2024-03-08 14:34:07 +01:00
Yannik Hampe 80cd2f2eda Merge branch 'RED-8515' into 'master'
proof of concept: use git lfs to store customer files

Closes RED-8515

See merge request redactmanager/redaction-service!304
2024-03-08 12:43:29 +01:00
Yannik Hampe d7f3f351ec proof of concept: use git lfs to store customer files 2024-03-08 12:43:29 +01:00
Maverick Studer b5d993561a Merge branch 'RED-7700' into 'master'
RED-7700: Safe rule execution

Closes RED-7700

See merge request redactmanager/redaction-service!311
2024-03-07 14:38:14 +01:00
Maverick Studer aad890b893 RED-7700: Safe rule execution 2024-03-07 14:38:13 +01:00
Kilian Schüttler c39c98b955 Merge branch 'image-name' into 'master'
revert image name change

See merge request redactmanager/redaction-service!306
2024-03-05 18:32:19 +01:00
Kilian Schuettler 620c315867 revert image name change 2024-03-05 16:30:58 +01:00
Andrei Isvoran 58fb062a79 Merge branch 'RED-8632' into 'master'
RED-8632 - Generate javadoc automatically

Closes RED-8632

See merge request redactmanager/redaction-service!305
2024-03-05 13:49:08 +01:00
Andrei Isvoran 9dfb0f49c6 RED-8632 - Generate javadoc automatically 2024-03-05 13:49:08 +01:00
Kilian Schüttler 7e144e30bf Merge branch 'reformat' into 'master'
reformat

See merge request redactmanager/redaction-service!303
2024-03-01 15:49:37 +01:00
Kilian Schuettler 9f8b1134b9 reformat 2024-03-01 15:41:24 +01:00
Andrei Isvoran 313df1e118 Merge branch 'RED-8586-dossier-redactions' into 'master'
RED-8586 - Don't treat dossier redactions differently

Closes RED-8586

See merge request redactmanager/redaction-service!302
2024-03-01 12:41:55 +01:00
Andrei Isvoran 24c1be66fd RED-8586 - Don't treat dossier redactions differently 2024-03-01 12:41:55 +01:00
Andrei Isvoran 0825686741 Merge branch 'RED-8586-fix' into 'master'
RED-8586 - Add higher salience to rule ETC.5.1

Closes RED-8586

See merge request redactmanager/redaction-service!299
2024-02-29 15:42:39 +01:00
Andrei Isvoran fd10d6e325 RED-8586 - Add higher salience to rule ETC.5.1 2024-02-29 16:28:33 +02:00
Corina Olariu b6ad1b5e91 Merge branch 'RED-8590' into 'master'
RED-8590 - Missing reason for added CBI Address, recategorized LOGO and signature

Closes RED-8590

See merge request redactmanager/redaction-service!293
2024-02-29 13:17:56 +01:00
Corina Olariu 5f961b0f99 RED-8590 - Missing reason for added CBI Address, recategorized LOGO and signature 2024-02-29 13:17:56 +01:00
Andrei Isvoran 89dea4138a Merge branch 'RED-8586' into 'master'
RED-8586 - Fix confidentiality rules

Closes RED-8586

See merge request redactmanager/redaction-service!294
2024-02-29 12:58:23 +01:00
Andrei Isvoran 4fb8b10429 RED-8586 - Fix confidentiality rules 2024-02-29 12:58:23 +01:00
Yannik Hampe f09a4e4d42 Merge branch 'RED-8481-rules' into 'master'
RED-8481: use visual layout parser for signature extraction

Closes RED-8481

See merge request redactmanager/redaction-service!296
2024-02-29 09:19:41 +01:00
Yannik Hampe ab5dc2f46b RED-8481: use visual layout parser for signature extraction 2024-02-29 09:19:41 +01:00
Kilian Schüttler b50899e18c Merge branch 'RED-8615' into 'master'
RED-8615: Revert and change the current behaviour of manualChanges

Closes RED-8615

See merge request redactmanager/redaction-service!291
2024-02-28 13:12:40 +01:00
Kilian Schüttler 30d0fd7def RED-8615: Revert and change the current behaviour of manualChanges 2024-02-28 13:12:40 +01:00
Yannik Hampe 23b8d164d2 Merge branch 'RED-8481-rules' into 'master'
RED-8481: use visual layout parser for signature extraction

Closes RED-8481

See merge request redactmanager/redaction-service!290
2024-02-28 09:04:55 +01:00
Yannik Hampe 0bfd92846f RED-8481: use visual layout parser for signature extraction 2024-02-28 09:04:55 +01:00
Kilian Schüttler ca3bb600f3 Merge branch 'RED-7384' into 'master'
More migration fixes

Closes RED-7384

See merge request redactmanager/redaction-service!280
2024-02-27 10:04:27 +01:00
Kilian Schüttler 5ffef504e4 More migration fixes 2024-02-27 10:04:27 +01:00
Yannik Hampe 94b7b741f8 Merge branch 'RED-8481' into 'master'
RED-8481: use visual layout parser for signature extraction

Closes RED-8481

See merge request redactmanager/redaction-service!289
2024-02-27 08:38:03 +01:00
Yannik Hampe 79ac1105f6 RED-8481: use visual layout parser for signature extraction 2024-02-27 08:38:03 +01:00
Kilian Schüttler 87f74c31c6 Merge branch 'hotfix' into 'master'
hotfix: streamEntitiesWhereRowContainsEntityOfType is broken

See merge request redactmanager/redaction-service!285
2024-02-23 14:30:48 +01:00
Kilian Schüttler 5f67efc8c7 hotfix: streamEntitiesWhereRowContainsEntityOfType is broken 2024-02-23 14:30:48 +01:00
Corina Olariu fd3fe87d90 Merge branch 'RED-8589' into 'master'
RED-8589 - Add "MANUAL" engine to all annotations that has entries in manualChanges

Closes RED-8589

See merge request redactmanager/redaction-service!282
2024-02-23 10:50:05 +01:00
Corina Olariu a85197cda4 RED-8589 - Add "MANUAL" engine to all annotations that has entries in manualChanges 2024-02-23 10:50:05 +01:00
Maverick Studer 05884533d1 Merge branch 'RED-8607' into 'master'
RED-8607: Higher rank hint removed if overlaps lower rank redaction

Closes RED-8607

See merge request redactmanager/redaction-service!283
2024-02-22 15:53:31 +01:00
Maverick Studer 310daa8060 RED-8607: Higher rank hint removed if overlaps lower rank redaction 2024-02-22 15:53:31 +01:00
Andrei Isvoran 7b798ae543 Merge branch 'RED-8588' into 'master'
RED-8588 - Fix ADD_TO_DICT

Closes RED-8588

See merge request redactmanager/redaction-service!281
2024-02-20 14:33:32 +01:00
Andrei Isvoran 3143f87587 RED-8588 - Fix ADD_TO_DICT 2024-02-20 14:33:31 +01:00
Corina Olariu 5fb3626208 Merge branch 'RED-7886' into 'master'
RED-7886 - Endpoint for rule validation

Closes RED-7886

See merge request redactmanager/redaction-service!278
2024-02-20 07:57:48 +01:00
Corina Olariu fb2d1fb2f2 RED-7886 - Endpoint for rule validation 2024-02-20 07:57:48 +01:00
Ali Oezyetimoglu cadbf6f275 Merge branch 'RED-8480' into 'master'
RED-8480: updated persistence service version and added legalBasis in ManualChangeOverwrite

Closes RED-8480

See merge request redactmanager/redaction-service!279
2024-02-16 14:58:27 +01:00
Ali Oezyetimoglu ea5d315dc3 RED-8480: updated persistence service version and added legalBasis in ManualChangeOverwrite 2024-02-16 14:58:27 +01:00
Andrei Isvoran 46990eb632 Merge branch 'RED-8490' into 'master'
RED-8490 - Add ADD_TO_DICTIONARY in manual changes for entries

Closes RED-8490

See merge request redactmanager/redaction-service!277
2024-02-14 08:51:09 +01:00
Andrei Isvoran 71e799bb92 RED-8490 - Add ADD_TO_DICTIONARY in manual changes for entries 2024-02-14 08:51:09 +01:00
Maverick Studer 09069b0095 Merge branch 'RED-8534' into 'master'
RED-8534: RECTANGLE is ENTITY instead of AREA

Closes RED-8534

See merge request redactmanager/redaction-service!276
2024-02-13 14:25:11 +01:00
Maverick Studer 01c44f6877 RED-8534: RECTANGLE is ENTITY instead of AREA 2024-02-13 14:25:11 +01:00
Kilian Schüttler 3b94e68af7 Merge branch 'RED-7384' into 'master'
RED-7384: various fixes for migration

See merge request redactmanager/redaction-service!275
2024-02-13 09:41:04 +01:00
Kilian Schüttler 5f598ff70c RED-7384: various fixes for migration 2024-02-13 09:41:03 +01:00
Andrei Isvoran b8064ce6e3 Merge branch 'RED-8542' into 'master'
RED-8542 - Only SKIPPED redaction should be displayed if it overlaps an ACTIVE...

Closes RED-8542

See merge request redactmanager/redaction-service!274
2024-02-13 08:17:40 +01:00
Andrei Isvoran 3b8202d21b RED-8542 - Only SKIPPED redaction should be displayed if it overlaps an ACTIVE... 2024-02-13 08:17:40 +01:00
Maverick Studer 94a75aa100 Merge branch 'RED-8451' into 'master'
RED-8451: RECATEGORIZED hint is APPLIED instead of SKIPPED

Closes RED-8451

See merge request redactmanager/redaction-service!273
2024-02-06 14:28:11 +01:00
Maverick Studer cc62bbc3ec RED-8451: RECATEGORIZED hint is APPLIED instead of SKIPPED 2024-02-06 14:28:11 +01:00
Maverick Studer 78869a13f6 Merge branch 'RED-8372' into 'master'
RED-8372: Include additional info about imported redactions in redaction annotations

Closes RED-8372

See merge request redactmanager/redaction-service!272
2024-02-06 09:58:00 +01:00
Maverick Studer c1028e7431 RED-8372: Include additional info about imported redactions in redaction annotations 2024-02-06 09:58:00 +01:00
Andrei Isvoran aa977c5a32 Merge branch 'RED-8248-dm' into 'master'
RED-8428 - Keep RESIZE for backwards compatibility for documine. RESIZE will...

Closes RED-8248

See merge request redactmanager/redaction-service!270
2024-02-05 14:34:57 +01:00
Andrei Isvoran 06eb4929a7 RED-8428 - Keep RESIZE for backwards compatibility for documine. RESIZE will... 2024-02-05 14:34:57 +01:00
Andrei Isvoran befd6f9a08 Merge branch 'RED-8428' into 'master'
RED-8428 - Add RESIZE_LOCALLY and RESIZE_IN_DICTIONARY in ManualRedactionType...

Closes RED-8428

See merge request redactmanager/redaction-service!269
2024-02-05 11:29:24 +01:00
Andrei Isvoran 053526d2da RED-8428 - Add RESIZE_LOCALLY and RESIZE_IN_DICTIONARY in ManualRedactionType... 2024-02-05 11:29:24 +01:00
Maverick Studer 1758dc1333 Merge branch 'RED-8424' into 'master'
RED-8424: Case-Sensitive Redaction Not Functioning Properly for Case-Sensitive Entities

Closes RED-8424

See merge request redactmanager/redaction-service!268
2024-02-05 09:59:50 +01:00
Maverick Studer a0d2430319 RED-8424: Case-Sensitive Redaction Not Functioning Properly for Case-Sensitive Entities 2024-02-05 09:59:50 +01:00
Dominique Eifländer b44e49b4d9 Merge branch 'RED-8171' into 'master'
RED-8171: Traces do not stop at @Async

Closes RED-8171

See merge request redactmanager/redaction-service!267
2024-02-02 13:44:09 +01:00
Dominique Eifländer ec2e359dd4 RED-8171: Traces do not stop at @Async 2024-02-02 13:34:23 +01:00
Andrei Isvoran 4d191d7bbd Merge branch 'RED-8347' into 'master'
RED-8347 - Add manual changes in entity log for removed entities

Closes RED-8347

See merge request redactmanager/redaction-service!266
2024-02-02 11:23:08 +01:00
Andrei Isvoran 930ad5b25e RED-8347 - Add manual changes in entity log for removed entities 2024-02-02 11:23:08 +01:00
Dominique Eifländer ce9ed6de9b Merge branch 'RED-8372' into 'master'
RED-8372: Include additional info about redactions in redaction annotations

Closes RED-8372

See merge request redactmanager/redaction-service!263
2024-02-02 11:11:30 +01:00
Maverick Studer 7be530b445 RED-8372: Include additional info about redactions in redaction annotations 2024-02-02 11:11:30 +01:00
Kilian Schüttler 7505766a83 Merge branch 'RED-7384' into 'master'
RED-7384: don't attempt to migrate id's that don't change, since...

Closes RED-7384

See merge request redactmanager/redaction-service!265
2024-02-02 10:26:38 +01:00
Kilian Schüttler 328a82835e RED-7384: don't attempt to migrate id's that don't change, since... 2024-02-02 10:26:38 +01:00
Andrei Isvoran e43e9ace68 Merge branch 'RED-8049' into 'master'
RED-8049 - Change TextEntity id to be calculated based on position

Closes RED-8049

See merge request redactmanager/redaction-service!255
2024-02-01 11:30:47 +01:00
Andrei Isvoran a7111b0e24 RED-8049 - Change TextEntity id to be calculated based on position 2024-02-01 11:30:47 +01:00
Kilian Schüttler 6a7e11cd2c Merge branch 'RED-7384' into 'master'
RED-7384: fixed ignored hints, fixed false positives, enhanced test to include manual changes

Closes RED-7384

See merge request redactmanager/redaction-service!261
2024-01-29 12:25:36 +01:00
Kilian Schüttler 5444a82c7f RED-7384: fixed ignored hints, fixed false positives, enhanced test to include manual changes 2024-01-29 12:25:36 +01:00
Timo Bejan 8b1b9b427d Merge branch 'RED-8085' into 'master'
RED-8085  pmd checkup/cleanup

Closes RED-8085

See merge request redactmanager/redaction-service!262
2024-01-29 10:33:22 +01:00
Timo Bejan ba5eaec957 RED-8085 pmd checkup/cleanup 2024-01-29 10:33:22 +01:00
Dominique Eifländer 34557c7cff Merge branch 'RED-7901' into 'master'
RED-7901: Changed rules to remove all recommendations that intersect with entities

Closes RED-7901

See merge request redactmanager/redaction-service!260
2024-01-25 14:54:47 +01:00
Dominique Eifländer b8e4f64520 RED-7901: Changed rules to remove all recommendations that intersect with entities 2024-01-25 14:41:39 +01:00
Maverick Studer 925df6c7f1 Merge branch 'RED-5449' into 'master'
RED-5449: Wrong behaviour of imported redactions only from PREVIEW-File

Closes RED-5449

See merge request redactmanager/redaction-service!258
2024-01-24 11:55:26 +01:00
Maverick Studer f84f63cc64 RED-5449: Wrong behaviour of imported redactions only from PREVIEW-File 2024-01-24 11:55:26 +01:00
Kilian Schüttler 23a3d08e85 Merge branch 'hotfix' into 'master'
hotfix: analysisNumber not set in reanalysis

See merge request redactmanager/redaction-service!257
2024-01-24 09:53:54 +01:00
Kilian Schuettler 2d890d1189 hotfix: analysisNumber not set in reanalysis 2024-01-23 16:55:39 +01:00
Dominique Eifländer b55e9abd7e Merge branch 'RED-8305' into 'master'
RED-8305: Apply recategorizations by default, increase memory for unittests

Closes RED-8305

See merge request redactmanager/redaction-service!256
2024-01-22 14:22:45 +01:00
Dominique Eifländer c71241288c RED-8305: Apply recategorizations by default, increase memory for unittests 2024-01-22 13:38:47 +01:00
Ali Oezyetimoglu 158fc71813 Merge branch 'RED-8264' into 'master'
RED-8264: translated and added new rules from Syngenta dossier templates

Closes RED-8264

See merge request redactmanager/redaction-service!254
2024-01-19 13:21:43 +01:00
Ali Oezyetimoglu 44900bd5c4 RED-8264: translated and added new rules from Syngenta dossier templates 2024-01-19 12:22:09 +01:00
Kilian Schüttler 031bdb05dd Merge branch 'RED-8305' into 'master'
RED-8305: Added rule 'MAN.3.4: Apply recategorization with legalbasis change by default'

Closes RED-8305

See merge request redactmanager/redaction-service!253
2024-01-19 11:39:11 +01:00
Dominique Eifländer 5d0d218137 RED-8305: Added rule 'MAN.3.4: Apply recategorization with legalbasis change by default' 2024-01-19 11:28:49 +01:00
Corina Olariu 755b3b1adc Merge branch 'RED-7256-imported-redactions' into 'master'
RED-7256 - Overlapping annotation color from imported redaction and resized...

Closes RED-7256

See merge request redactmanager/redaction-service!249
2024-01-18 17:08:43 +01:00
Corina Olariu ac6bbf8c2f RED-7256 - Overlapping annotation color from imported redaction and resized... 2024-01-18 17:08:41 +01:00
Dominique Eifländer 896524a6c0 Merge branch 'RED-8278' into 'master'
RED-8278: Fixed skipped signatures after manual changes

Closes RED-8278

See merge request redactmanager/redaction-service!252
2024-01-18 14:05:59 +01:00
Dominique Eifländer 9dbd2ac260 RED-8278: Fixed skipped signatures after manual changes 2024-01-18 13:54:23 +01:00
Dominique Eifländer 91766f5b0f Merge branch 'RED-7384' into 'master'
RED-7384: fix minor migration issues

Closes RED-7384

See merge request redactmanager/redaction-service!251
2024-01-18 10:33:42 +01:00
Corina Olariu 192fb96552 Merge branch 'RED-8274' into 'master'
RED-8274 - Resize redaction in header leads to error state

Closes RED-8274

See merge request redactmanager/redaction-service!250
2024-01-18 10:11:48 +01:00
Corina Olariu 5a5b8625dd RED-8274 - Resize redaction in header leads to error state 2024-01-18 10:11:48 +01:00
Kilian Schuettler c5c416dd65 RED-7384: fix minor migration issues 2024-01-18 10:10:59 +01:00
Ali Oezyetimoglu 9c41888721 Merge branch 'hotfix-ali-parse-rule-identifiers' into 'master'
fix: while parsing rule identifiers the wrong length is calculated

See merge request redactmanager/redaction-service!248
2024-01-16 08:26:52 +01:00
Ali Oezyetimoglu 30323568c4 fix: while parsing rule identifiers the wrong length is calculated 2024-01-15 21:01:07 +01:00
Kilian Schüttler c9c5476cf2 Merge branch 'RED-7384' into 'master'
RED-7384: fix migration issues

Closes RED-7384

See merge request redactmanager/redaction-service!247
2024-01-12 10:27:43 +01:00
Andrei Isvoran 44aebe054e Merge branch 'RED-8173-fix' into 'master'
RED-8173 - Fix for removing and adding redaction in the same place

Closes RED-8173

See merge request redactmanager/redaction-service!243
2024-01-12 08:23:23 +01:00
Andrei Isvoran 01ae14522d RED-8173 - Fix for removing and adding redaction in the same place 2024-01-12 08:23:22 +01:00
Kilian Schuettler 3d81c7fc16 RED-7384: fix migration issues 2024-01-11 19:04:32 +01:00
Andrei Isvoran 8aad7bcdf6 Merge branch 'RED-8239' into 'master'
RED-8239 - Update dictionary for surrounding text analysis

Closes RED-8239

See merge request redactmanager/redaction-service!245
2024-01-11 17:53:04 +01:00
Andrei Isvoran f2d86d634e RED-8239 - Update dictionary for surrounding text analysis 2024-01-11 16:29:25 +02:00
Dominique Eifländer 8f19bed2b6 Merge branch 'MetaSpaceIncrease' into 'master'
Hotfix: Increased MaxMetaspaceSize to 512m to prevent OOM in performace tests

See merge request redactmanager/redaction-service!244
2024-01-11 15:20:53 +01:00
Dominique Eifländer e1da2802b0 Hotfix: Increased MaxMetaspaceSize to 512m to prevent OOM in performace tests 2024-01-11 15:16:48 +01:00
Andrei Isvoran 72ad7cc53d Merge branch 'RED-8173' into 'master'
RED-8173 - Fix for removing and adding redaction in the same place leading not working

Closes RED-8173

See merge request redactmanager/redaction-service!242
2024-01-11 08:59:16 +01:00
Andrei Isvoran 2e60e05fd1 RED-8173 - Fix for removing and adding redaction in the same place leading not working 2024-01-11 08:59:16 +01:00
Kilian Schüttler d6bff17544 Merge branch 'RED-7384' into 'master'
RED-7384: update dictionary before manual change merge

Closes RED-7384

See merge request redactmanager/redaction-service!241
2024-01-10 13:37:45 +01:00
Kilian Schuettler 82c9884654 RED-7384: update dictionary before manual change merge 2024-01-10 12:48:05 +01:00
Kilian Schüttler efc1b8385b Merge branch 'hotfix' into 'master'
hotfix: Hints should never be applied

See merge request redactmanager/redaction-service!239
2024-01-10 12:33:34 +01:00
Andrei Isvoran a2faa84f86 Merge branch 'RED-8043-more-fixes' into 'master'
RED-8043 - Correctly calculate surrounding text for manual unprocessed add redactions

Closes RED-8043

See merge request redactmanager/redaction-service!240
2024-01-10 09:21:30 +01:00
Andrei Isvoran 4f48b3f5be RED-8043 - Correctly calculate surrounding text for manual unprocessed add redactions 2024-01-10 09:21:29 +01:00
Kilian Schuettler 5168f1d8a7 hotfix: Hints should never be applied 2024-01-09 13:52:02 +01:00
Andrei Isvoran 4fea65f982 Merge branch 'RED-8043-resize-surrounding-text' into 'master'
RED-8043 - Calculate surrounding text for unprocessed manual resize

Closes RED-8043

See merge request redactmanager/redaction-service!238
2024-01-08 14:15:38 +01:00
Andrei Isvoran b42fae4054 RED-8043 - Calculate surrounding text for unprocessed manual resize 2024-01-08 14:15:38 +01:00
Dominique Eifländer 7b5c0026f9 Merge branch 'RED-8106' into 'master'
RED-8106: Prepare use redis for document data cache. Added more @Obeserved s

Closes RED-8106

See merge request redactmanager/redaction-service!237
2023-12-22 15:01:39 +01:00
Dominique Eifländer c315b52cb2 RED-8106: Prepare use redis for document data cache. Added more @Obeserved s 2023-12-22 14:29:14 +01:00
Dominique Eifländer b384cacbe3 Merge branch 'RED-1137' into 'master'
RED-1137: Do not observe actuator endpoints

Closes RED-1137

See merge request redactmanager/redaction-service!235
2023-12-20 14:19:14 +01:00
Dominique Eifländer d208769446 RED-1137: Do not observe actuator endpoints 2023-12-20 14:09:15 +01:00
Kilian Schüttler 2423989192 Merge branch 'RED-7384' into 'master'
RED-7834: additional fixes for migration

Closes RED-7384

See merge request redactmanager/redaction-service!234
2023-12-20 12:38:25 +01:00
Kilian Schüttler de9524d16b RED-7834: additional fixes for migration 2023-12-20 12:38:25 +01:00
Dominique Eifländer 8e6aa67412 Merge branch 'RED-8034' into 'master'
RED-8034: Excluded pages is indendent from analyse

Closes RED-8034

See merge request redactmanager/redaction-service!233
2023-12-19 15:12:48 +01:00
Dominique Eifländer b89b90f4ba RED-8034: Excluded pages is indendent from analyse 2023-12-19 14:15:56 +01:00
Andrei Isvoran 7f2d4b01e8 Merge branch 'RED-8043' into 'master'
RED-8043 - Don't send analysis successful call for surrounding text analysis

Closes RED-8043

See merge request redactmanager/redaction-service!232
2023-12-19 13:59:07 +01:00
Andrei Isvoran 714387ad7c RED-8043 - Don't send analysis successful call for surrounding text analysis 2023-12-19 13:59:06 +01:00
Dominique Eifländer e80686fe01 Merge branch 'RED-5223' into 'master'
RED-5223: Added @Observed annotations for interesting methods

Closes RED-5223

See merge request redactmanager/redaction-service!231
2023-12-15 10:59:36 +01:00
Dominique Eifländer 90b5d7d055 RED-5223: Added @Observed annotations for interesting methods 2023-12-15 10:47:10 +01:00
Yannik Hampe 74d37a710c Merge branch 'RED-8078' into 'master'
RED-8078: Removing an entity type should trigger complete reanalysis

Closes RED-8078

See merge request redactmanager/redaction-service!230
2023-12-15 10:14:22 +01:00
Yannik Hampe c4a9bb3178 RED-8078: Removing an entity type should trigger complete reanalysis 2023-12-15 10:14:22 +01:00
Dominique Eifländer bc787972cc Merge branch 'RED-5223' into 'master'
RED-5223: Use tracing-commons from fforesight

Closes RED-5223

See merge request redactmanager/redaction-service!228
2023-12-13 16:10:58 +01:00
Andrei Isvoran aa8ed82379 Merge branch 'RED-8043' into 'master'
RED-8043 - Add SurroundTextAnalysis case for unprocessed redactions to be...

Closes RED-8043

See merge request redactmanager/redaction-service!227
2023-12-13 16:06:09 +01:00
Andrei Isvoran b0c04ca0ec RED-8043 - Add SurroundTextAnalysis case for unprocessed redactions to be... 2023-12-13 16:06:09 +01:00
Dominique Eifländer 4b84885d19 RED-5223: Use tracing-commons from fforesight 2023-12-13 15:58:04 +01:00
Timo Bejan 382672bc5e Merge branch 'RED-5223' into 'master'
RED-5223: Enabled tracing, upgrade spring, use logstash-logback-encoder for json logs

Closes RED-5223

See merge request redactmanager/redaction-service!226
2023-12-11 15:56:00 +01:00
Dominique Eifländer 4a6f9449d0 RED-5223: Enabled tracing, upgrade spring, use logstash-logback-encoder for json logs 2023-12-11 15:45:17 +01:00
Timo Bejan dc03894ea4 Merge branch 'RED-7834' into 'master'
RED-7834: fixes for migration

Closes RED-7834

See merge request redactmanager/redaction-service!225
2023-12-11 15:44:41 +01:00
Kilian Schüttler c957ed8605 RED-7834: fixes for migration 2023-12-11 15:44:40 +01:00
Andrei Isvoran ac978048b0 Merge branch 'RED-8057' into 'master'
RED-8057 - Fix issue with adding dict based manual redactions with no position specified

Closes RED-8057

See merge request redactmanager/redaction-service!224
2023-12-08 16:21:07 +01:00
Andrei Isvoran f6cc0edded RED-8057 - Fix issue with adding dict based manual redactions with no position specified 2023-12-08 16:21:05 +01:00
Kilian Schüttler efc01f9d20 Merge branch 'RED-7834' into 'master'
RED-7834: fixes for migration code

Closes RED-7834

See merge request redactmanager/redaction-service!220
2023-12-08 11:05:49 +01:00
Kilian Schüttler 4047b93f89 RED-7834: fixes for migration code 2023-12-08 11:05:49 +01:00
Andrei Isvoran 03f43f4408 Merge branch 'RED-7715' into 'master'
RED-7715 - Add log4j config to enable switching between json/line logs

Closes RED-7715

See merge request redactmanager/redaction-service!223
2023-12-06 12:40:40 +01:00
Andrei Isvoran bfb3dea059 RED-7715 - Add log4j config to enable switching between json/line logs 2023-12-06 11:37:35 +02:00
Corina Olariu d7ea126d2e Merge branch 'RED-7979-forceHint' into 'master'
RED-7979 - Force Hint leads to 500 BE Error

Closes RED-7979

See merge request redactmanager/redaction-service!222
2023-12-06 08:34:37 +01:00
Corina Olariu 17bc14e9c1 RED-7979 - Force Hint leads to 500 BE Error 2023-12-06 08:34:37 +01:00
Kilian Schüttler a09645669c Merge branch 'RED-8006' into 'master'
Resolve RED-8006

Closes RED-8006

See merge request redactmanager/redaction-service!219
2023-12-01 14:44:18 +01:00
Kilian Schüttler 60aeb5bddc Resolve RED-8006 2023-12-01 14:44:18 +01:00
Ali Oezyetimoglu 26e50da938 Merge branch 'RED-8006-2' into 'master'
RED-8006: updated rules to add CBI_address in CBI.7.1

Closes RED-8006

See merge request redactmanager/redaction-service!218
2023-12-01 13:58:42 +01:00
Ali Oezyetimoglu 5d0b00204f RED-8006: updated rules to add CBI_address in CBI.7.1 2023-12-01 13:41:08 +01:00
Ali Oezyetimoglu 5ef410fb70 Merge branch 'RED-8007' into 'master'
RED-8007: made "Vertebrate == Yes" more safe

Closes RED-8007

See merge request redactmanager/redaction-service!217
2023-12-01 13:29:32 +01:00
Ali Oezyetimoglu e655ee3bb9 RED-8007: made "Vertebrate == Yes" more safe 2023-12-01 11:50:23 +01:00
Dominique Eifländer b75efedf7b Merge branch 'DM-606' into 'master'
Resolve DM-606

Closes DM-606

See merge request redactmanager/redaction-service!216
2023-11-30 13:47:15 +01:00
Kilian Schüttler 1a8fbb97e9 Resolve DM-606 2023-11-30 13:47:15 +01:00
Dominique Eifländer db6c027cac Merge branch 'hotfix' into 'master'
hotfix: filter entities with empty value in redactionLog and entityLog

See merge request redactmanager/redaction-service!215
2023-11-30 13:25:17 +01:00
Kilian Schüttler 66ac1429b0 hotfix: filter entities with empty value in redactionLog and entityLog 2023-11-30 13:25:17 +01:00
Corina Olariu 0acc731113 Merge branch 'RED-7991' into 'master'
RED-7991: Do not skip readded manual redactions

Closes RED-7991

See merge request redactmanager/redaction-service!212
2023-11-29 15:28:21 +01:00
Dominique Eifländer 07a9fbc2e1 RED-7991: Do not skip readded manual redactions 2023-11-29 15:17:14 +01:00
Dominique Eifländer 2358c01c97 Merge branch 'DM-602' into 'master'
DM-602: Do not skip annotation after chenge type for documine

Closes DM-602

See merge request redactmanager/redaction-service!211
2023-11-29 13:56:55 +01:00
Dominique Eifländer 10484cb0d6 DM-602: Do not skip annotation after chenge type for documine 2023-11-29 13:36:29 +01:00
Kilian Schüttler 5176bda496 Merge branch 'RED-7886' into 'master'
RED-7886: additional validations, now allowing for additional imports, as long...

Closes RED-7886

See merge request redactmanager/redaction-service!210
2023-11-29 09:14:54 +01:00
Kilian Schüttler 6f5e0d3491 RED-7886: additional validations, now allowing for additional imports, as long... 2023-11-29 09:14:54 +01:00
Ali Oezyetimoglu d0f1964dd9 Merge branch 'RED-7757' into 'master'
RED-7757: added check for case sensitivity when adding local dictionary entries

Closes RED-7757

See merge request redactmanager/redaction-service!209
2023-11-28 15:12:07 +01:00
Ali Oezyetimoglu 3c99e015aa RED-7757: added check for case sensitivity when adding local dictionary entries 2023-11-28 11:28:50 +01:00
Kilian Schüttler 775f43ce03 Merge branch 'enable-caching' into 'master'
enable caching

See merge request redactmanager/redaction-service!208
2023-11-24 10:43:06 +01:00
Kilian Schüttler b4cb80bc68 enable caching 2023-11-24 10:43:06 +01:00
Timo Bejan 31c74b12c2 Merge branch 'dev-profile-fix' into 'master'
Different dev port

See merge request redactmanager/redaction-service!207
2023-11-23 20:58:42 +01:00
Timo Bejan 51e959e972 Different dev port 2023-11-23 21:37:01 +02:00
Kilian Schüttler e871813a2b Merge branch 'RED-7902' into 'master'
RED-7902: fix author recommendation via rules

Closes RED-7902

See merge request redactmanager/redaction-service!206
2023-11-23 15:22:44 +01:00
Kilian Schüttler 74e7a6fb68 RED-7902: fix author recommendation via rules 2023-11-23 15:22:44 +01:00
Kilian Schüttler 5c652df904 Merge branch 'RED-7902' into 'master'
RED-7902: fix author name splitting

Closes RED-7902

See merge request redactmanager/redaction-service!204
2023-11-21 12:22:09 +01:00
Kilian Schüttler 9c31f04478 RED-7902: fix author name splitting 2023-11-21 12:22:09 +01:00
Dominique Eifländer b18498e880 Merge branch 'RED-7934' into 'master'
RED-7934: Fixed indexOutOfBounds for removed entries

Closes RED-7934

See merge request redactmanager/redaction-service!203
2023-11-21 12:18:34 +01:00
Dominique Eifländer 851f9eeb97 RED-7934: Fixed indexOutOfBounds for removed entries 2023-11-21 12:04:31 +01:00
Kilian Schüttler 79e76d688e Merge branch 'RED-7902' into 'master'
RED-7902: fix author name splitting

Closes RED-7902

See merge request redactmanager/redaction-service!202
2023-11-21 11:22:54 +01:00
Kilian Schüttler c364a6b9ef RED-7902: fix author name splitting 2023-11-21 11:22:54 +01:00
Andrei Isvoran eceefe363a Merge branch 'RED-7784' into 'master'
RED-7784 - Calculate positions and legalbasis for unprocessed redactions

Closes RED-7784

See merge request redactmanager/redaction-service!201
2023-11-20 13:45:49 +01:00
Andrei Isvoran 522c514b1a RED-7784 - Calculate positions and legalbasis for unprocessed redactions 2023-11-20 13:45:49 +01:00
Dominique Eifländer d42758b189 Merge branch 'RED-7934' into 'master'
RED-7934: Manual Redactions should be removed instead of ignored, when removed by id

Closes RED-7934

See merge request redactmanager/redaction-service!200
2023-11-17 15:36:34 +01:00
Kilian Schüttler 150cb01649 RED-7934: Manual Redactions should be removed instead of ignored, when removed by id 2023-11-17 15:36:33 +01:00
Christoph Schabert a9daac317e Merge branch 'buildFix' into 'master'
hotfix: Fixed wrong tag name

See merge request redactmanager/redaction-service!199
2023-11-17 12:36:26 +01:00
Dominique Eifländer cb103a3f83 hotfix: Fixed wrong tag name 2023-11-17 12:28:08 +01:00
Dominique Eifländer 3f9f3857f1 Merge branch 'RED-7934' into 'master'
RED-7934: Manual Redactions should be removed instead of ignored, when removed by id

Closes RED-7934

See merge request redactmanager/redaction-service!198
2023-11-17 10:36:15 +01:00
Kilian Schüttler 12a9b5029c RED-7934: Manual Redactions should be removed instead of ignored, when removed by id 2023-11-17 10:36:15 +01:00
Kilian Schüttler f79838687e Merge branch 'caching' into 'master'
enable build caching

See merge request redactmanager/redaction-service!197
2023-11-14 11:50:17 +01:00
Kilian Schüttler 95b880b92e enable build caching 2023-11-14 11:50:17 +01:00
Kilian Schüttler b499e10590 Merge branch 'RED-7631' into 'master'
RED-7631: fix recommandations being applied instead of skipped

Closes RED-7631

See merge request redactmanager/redaction-service!195
2023-11-14 11:37:30 +01:00
Kilian Schüttler b1e9f1d5a5 RED-7631: fix recommandations being applied instead of skipped 2023-11-14 11:37:30 +01:00
Ali Oezyetimoglu 20b8db3755 Merge branch 'RED-7679-BBB3' into 'master'
Resolve RED-7679 "Bbb3"

Closes RED-7679

See merge request redactmanager/redaction-service!191
2023-11-14 10:51:28 +01:00
Ali Oezyetimoglu 71789e12fa Resolve RED-7679 "Bbb3" 2023-11-14 10:51:28 +01:00
Dominique Eifländer afc689839a Merge branch 'RED-7891-2' into 'master'
RED-7891: Fixed indexOutOfBounds if imported redactions are available in reanalysis

Closes RED-7891

See merge request redactmanager/redaction-service!194
2023-11-13 14:24:58 +01:00
Dominique Eifländer 4e88bec0e4 RED-7891: Fixed indexOutOfBounds if imported redactions are available in reanalysis 2023-11-13 14:16:09 +01:00
Dominique Eifländer ad9bf6f846 Merge branch 'RED-7891' into 'master'
RED-7891: Fixed nullpointer when imported redactions are available

Closes RED-7891

See merge request redactmanager/redaction-service!193
2023-11-13 11:54:29 +01:00
Dominique Eifländer 269d72c078 RED-7891: Fixed nullpointer when imported redactions are available 2023-11-13 11:44:34 +01:00
Andrei Isvoran 3a685e5671 Merge branch 'RED-7898' into 'master'
RED-7898 - Publish rule is not applied line-wise

Closes RED-7898

See merge request redactmanager/redaction-service!190
2023-11-10 14:29:58 +01:00
Andrei Isvoran a68633091a RED-7898 - Publish rule is not applied line-wise 2023-11-10 14:29:58 +01:00
Dominique Eifländer d116a2bea3 Merge branch 'RED-7891' into 'master'
RED-7891: Fixed Nullpointer when adding importedRedactions

Closes RED-7891

See merge request redactmanager/redaction-service!192
2023-11-10 14:23:29 +01:00
Dominique Eifländer 07b14c3f77 RED-7891: Fixed Nullpointer when adding importedRedactions 2023-11-10 14:12:28 +01:00
Kilian Schüttler 7aec2801a1 Merge branch 'DM-557' into 'master'
DM-557: fix component sorting

Closes DM-557

See merge request redactmanager/redaction-service!189
2023-11-10 12:41:14 +01:00
Kilian Schüttler 6e6cea89f7 DM-557: fix component sorting 2023-11-10 12:41:14 +01:00
Andrei Isvoran 5a4947372f Merge branch 'RED-7712' into 'master'
RED-7712 - Published information does not always trigger the skip mechanism

Closes RED-7712

See merge request redactmanager/redaction-service!192
2023-11-03 08:42:48 +01:00
Andrei Isvoran 7eb0abf4da RED-7712 - Published information does not always trigger the skip mechanism 2023-11-03 08:42:48 +01:00
Kilian Schüttler 67cf5659d4 Merge branch 'hotfix' into 'master'
rule validation hotfix:

See merge request redactmanager/redaction-service!191
2023-11-02 11:13:26 +01:00
Kilian Schüttler 8d8d83bc25 rule validation hotfix: 2023-11-02 11:13:26 +01:00
Andrei Isvoran 2860089712 Merge branch 'RED-7712' into 'master'
RED-7712 - Published information does not always trigger the skip mechanism

Closes RED-7712

See merge request redactmanager/redaction-service!189
2023-11-01 08:59:11 +01:00
Andrei Isvoran 908b3ca4b2 RED-7712 - Published information does not always trigger the skip mechanism 2023-11-01 08:59:11 +01:00
Kilian Schüttler 54bb79b294 Merge branch 'RED-7631' into 'master'
RED-7631: skip manual entry hints instead of apply

Closes RED-7631

See merge request redactmanager/redaction-service!188
2023-10-31 14:09:10 +01:00
Kilian Schüttler 2e4ac644f8 RED-7631: skip manual entry hints instead of apply 2023-10-31 14:09:10 +01:00
Andrei Isvoran fbdf360fd9 Merge branch 'RED-7652' into 'master'
RED-7652 - Integrate rules-management into redaction-service & validate rules

Closes RED-7652

See merge request redactmanager/redaction-service!186
2023-10-31 10:16:14 +01:00
Andrei Isvoran a1245101de RED-7652 - Integrate rules-management into redaction-service & validate rules 2023-10-31 10:16:14 +01:00
Kilian Schüttler 698244e3f1 Merge branch 'RED-7384' into 'master'
RED-7384: first prototype for migrating redactionLogs to entityLogs

Closes RED-7384

See merge request redactmanager/redaction-service!184
2023-10-30 16:27:05 +01:00
Kilian Schüttler 8c062da5a1 RED-7384: first prototype for migrating redactionLogs to entityLogs 2023-10-30 16:27:05 +01:00
Ali Oezyetimoglu f38adabb8c Merge branch 'RED-6807-zyx' into 'master'
RED-6807: updated flora rules

Closes RED-6807

See merge request redactmanager/redaction-service!182
2023-10-26 16:47:33 +02:00
Ali Oezyetimoglu 3cb97f6e27 RED-6807: updated flora rules 2023-10-26 16:08:25 +02:00
Kilian Schüttler ddb94e4480 Merge branch 'RED-7631' into 'master'
RED-7631: add EntryType IMAGE_HINT

Closes RED-7631

See merge request redactmanager/redaction-service!181
2023-10-26 13:10:27 +02:00
Kilian Schüttler eea1857a37 RED-7631: add EntryType IMAGE_HINT 2023-10-26 13:10:27 +02:00
Andrei Isvoran a248460e70 Merge branch 'RED-7785-position' into 'master'
RED-7784 - Add position to UnprocessedManualEntity

Closes RED-7785

See merge request redactmanager/redaction-service!180
2023-10-26 12:00:57 +02:00
Ali Oezyetimoglu ead36404c0 Merge branch 'RED-7679-5' into 'master'
Resolve RED-7679 "5"

Closes RED-7679

See merge request redactmanager/redaction-service!179
2023-10-26 11:51:21 +02:00
Ali Oezyetimoglu 78add9ad9f Resolve RED-7679 "5" 2023-10-26 11:51:21 +02:00
Andrei Isvoran a2b130f75f RED-7784 - Add position to UnprocessedManualEntity 2023-10-26 11:56:16 +03:00
Andrei Isvoran 3d10dd97f9 Merge branch 'RED-7784-section' into 'master'
RED-7784 - Add section to UnprocessedManualEntity

Closes RED-7784

See merge request redactmanager/redaction-service!177
2023-10-25 14:58:57 +02:00
Andrei Isvoran 6da7c23fc2 RED-7784 - Add section to UnprocessedManualEntity 2023-10-25 15:51:04 +03:00
Andrei Isvoran 77f00914d3 Merge branch 'RED-7784-no-args-constructor' into 'master'
RED-7784 - Add no args constructor to UnprocessedManualEntity

Closes RED-7784

See merge request redactmanager/redaction-service!176
2023-10-25 13:20:36 +02:00
Andrei Isvoran ff9c0886ce RED-7784 - Add no args constructor to UnprocessedManualEntity 2023-10-25 14:12:44 +03:00
Andrei Isvoran dc07404b48 Merge branch 'RED-7784' into 'master'
RED-7784 - Apply unprocessed manual changes in reports and redactions

Closes RED-7784

See merge request redactmanager/redaction-service!170
2023-10-25 10:58:49 +02:00
Andrei Isvoran 7d0a159aed RED-7784 - Apply unprocessed manual changes in reports and redactions 2023-10-25 10:58:49 +02:00
Kilian Schüttler 0ace45ca61 Merge branch 'RED-7759' into 'master'
RED-7759: Cannot resize local manual redactions

Closes RED-7759

See merge request redactmanager/redaction-service!173
2023-10-24 15:12:42 +02:00
Kilian Schüttler b4356408b6 RED-7759: Cannot resize local manual redactions 2023-10-24 15:12:42 +02:00
Ali Oezyetimoglu 1f81167f9e Merge branch 'RED-6807-3' into 'master'
RED-6807: updated RM and DM rules

Closes RED-6807

See merge request redactmanager/redaction-service!168
2023-10-24 15:12:16 +02:00
Ali Oezyetimoglu 3c9cb68831 RED-6807: updated RM and DM rules 2023-10-24 15:12:16 +02:00
Kilian Schüttler db2b663547 Merge branch 'RED-7434' into 'master'
RED-7434 - Remove Section Grid entirely

Closes RED-7434

See merge request redactmanager/redaction-service!169
2023-10-20 15:28:07 +02:00
Corina Olariu 1d96a747e4 RED-7434 - Remove Section Grid entirely 2023-10-20 15:28:07 +02:00
Kilian Schüttler 025cc19b6c Merge branch 'hotfix' into 'master'
hotfix: write dummy AnnotationStatus to redactionLog ManualChanges

See merge request redactmanager/redaction-service!166
2023-10-19 10:01:22 +02:00
Kilian Schüttler a8180ab9d2 hotfix: write dummy AnnotationStatus to redactionLog ManualChanges 2023-10-19 10:01:22 +02:00
Yannik Hampe 44eff703e0 Merge branch 'RED-7759' into 'master'
RED-7759: Cannot resize local manual redactions

Closes RED-7759

See merge request redactmanager/redaction-service!165
2023-10-18 14:53:33 +02:00
Yannik Hampe 96d29b9e3a RED-7759: Cannot resize local manual redactions 2023-10-18 14:53:33 +02:00
Yannik Hampe e4b1824dd4 Merge branch 'RED-7040' into 'master'
RED-7040: Syngenta rule 29 does not work

Closes RED-7040

See merge request redactmanager/redaction-service!157
2023-10-16 12:36:18 +02:00
Yannik Hampe 9fcf02df73 RED-7040: Syngenta rule 29 does not work 2023-10-16 12:36:18 +02:00
Kilian Schüttler e395620e29 Merge branch 'DM-483-endless-loops' into 'master'
DM-483 - Prevent endless loops

Closes DM-483

See merge request redactmanager/redaction-service!162
2023-10-13 14:30:02 +02:00
Andrei Isvoran 157f608eaa DM-483 - Prevent endless loops 2023-10-13 14:30:02 +02:00
Kilian Schüttler 8d55619bb1 Merge branch 'RED-7631' into 'master'
RED-7631: entity log to replace redactionlog

Closes RED-7631

See merge request redactmanager/redaction-service!158
2023-10-13 10:00:21 +02:00
Kilian Schüttler 676a35da56 RED-7631: entity log to replace redactionlog 2023-10-13 10:00:21 +02:00
Kilian Schüttler e8fd945879 Merge branch 'renovate/master-plugins-(non-major)' into 'master'
Update Plugins (non-major) (master)

See merge request redactmanager/redaction-service!147
2023-10-10 15:53:49 +02:00
Renovate Bot 798919690e Update Plugins (non-major) (master) 2023-10-10 15:53:49 +02:00
Kilian Schüttler 1b96c43fa5 Merge branch 'renovate/master-com.iqser.red.service-persistence-service-internal-api-v1-2.x' into 'master'
Update dependency com.iqser.red.service:persistence-service-internal-api-v1 to v2.207.0 (master)

See merge request redactmanager/redaction-service!142
2023-10-10 15:53:34 +02:00
Renovate Bot 64762f064b Update dependency com.iqser.red.service:persistence-service-internal-api-v1 to v2.207.0 (master) 2023-10-10 15:53:34 +02:00
Kilian Schüttler b79dea809a Merge branch 'renovate/master-persistenceserviceversion' into 'master'
Update dependency com.iqser.red.service:persistence-service-internal-api-v1 to v2.207.0 (master)

See merge request redactmanager/redaction-service!143
2023-10-10 15:53:27 +02:00
Renovate Bot 8b2453b0bc Update dependency com.iqser.red.service:persistence-service-internal-api-v1 to v2.207.0 (master) 2023-10-10 15:53:26 +02:00
Kilian Schüttler 09c6a6e22d Merge branch 'renovate/master-major-droolsversion' into 'master'
Update droolsVersion to v9 (master) (major)

See merge request redactmanager/redaction-service!144
2023-10-10 15:53:09 +02:00
Renovate Bot 73c086a5b1 Update droolsVersion to v9 (master) (major) 2023-10-10 15:53:09 +02:00
Kilian Schüttler 8c98d99a8f Merge branch 'renovate/master-layoutparserversion' into 'master'
Update layoutParserVersion to v0.71.0 (master)

See merge request redactmanager/redaction-service!150
2023-10-10 15:52:51 +02:00
Renovate Bot 3e4c5a5a28 Update layoutParserVersion to v0.71.0 (master) 2023-10-10 15:52:51 +02:00
Kilian Schüttler a5a893a417 Merge branch 'renovate/master-com.iqser.red.commons-storage-commons-2.x' into 'master'
Update dependency com.iqser.red.commons:storage-commons to v2.45.0 (master)

See merge request redactmanager/redaction-service!151
2023-10-10 15:52:43 +02:00
Renovate Bot 1eff50d243 Update dependency com.iqser.red.commons:storage-commons to v2.45.0 (master) 2023-10-10 15:52:43 +02:00
Kilian Schüttler cb82f90590 Merge branch 'RED-7708-kilian' into 'master'
RED-7708: multiple entries with same id in redactionLog/entityLog

Closes RED-7708

See merge request redactmanager/redaction-service!154
2023-10-10 14:06:23 +02:00
Kilian Schüttler 492bb56513 RED-7708: multiple entries with same id in redactionLog/entityLog 2023-10-10 14:06:23 +02:00
Ali Oezyetimoglu d71f06d884 Merge branch 'RED-7631' into 'master'
RED-7631: create EntityLog to replace RedactionLog

Closes RED-7631

See merge request redactmanager/redaction-service!152
2023-10-09 17:16:59 +02:00
Kilian Schüttler d7a1c7c53b RED-7631: create EntityLog to replace RedactionLog 2023-10-09 17:16:59 +02:00
Kresnadi Budisantoso a282d035aa Merge branch 'DM-285' into 'master'
DM-285: sort dates in ComponentCreationService

Closes DM-285

See merge request redactmanager/redaction-service!148
2023-09-29 17:37:32 +02:00
Kilian Schüttler a1b4c22bdd DM-285: sort dates in ComponentCreationService 2023-09-29 17:37:32 +02:00
Kresnadi Budisantoso 44e9e2e759 Merge branch 'DM-285' into 'master'
DM-285: update component rules

Closes DM-285

See merge request redactmanager/redaction-service!146
2023-09-26 20:05:22 +02:00
Kilian Schüttler 8d07de8630 DM-285: update component rules 2023-09-26 20:05:22 +02:00
Kilian Schüttler 0053fc9fac Merge branch 'DM-285' into 'master'
DM-285: adjust code to new EntityLog format.

Closes DM-285

See merge request redactmanager/redaction-service!145
2023-09-26 13:17:17 +02:00
Kilian Schüttler 66c4a38aff DM-285: adjust code to new EntityLog format. 2023-09-26 13:17:17 +02:00
Kilian Schüttler ce580b6285 Merge branch 'DM-285' into 'master'
DM-285: adjust ComponentLog to new Format

Closes DM-285

See merge request redactmanager/redaction-service!141
2023-09-22 14:28:36 +02:00
Kilian Schüttler a15e6c2651 DM-285: adjust ComponentLog to new Format 2023-09-22 14:28:36 +02:00
Kilian Schüttler 1b22ef5af3 Merge branch 'RED-7307' into 'master'
Upgrade tenant commons to 0.13.0

Closes RED-7307

See merge request redactmanager/redaction-service!140
2023-09-22 10:57:49 +02:00
Kilian Schüttler 3aab6bfb68 Upgrade tenant commons to 0.13.0 2023-09-22 10:57:49 +02:00
Kilian Schüttler a6b82f6813 Merge branch 'RED-7307' into 'master'
RED-7307: make ManualEntities applied by default

Closes RED-7307

See merge request redactmanager/redaction-service!138
2023-09-22 10:08:25 +02:00
Kilian Schüttler a8ed18bde4 RED-7307: make ManualEntities applied by default 2023-09-22 10:08:25 +02:00
Kilian Schüttler 871b3cbc8f Merge branch 'renovate/master-spring-boot' into 'master'
Update spring boot to v3.1.4 (master)

See merge request redactmanager/redaction-service!108
2023-09-22 09:52:19 +02:00
Renovate Bot 27ec7b6d9b Update spring boot to v3.1.4 (master) 2023-09-22 09:52:18 +02:00
Kilian Schüttler 5f4b2290e4 Merge branch 'renovate/master-plugins-(non-major)' into 'master'
Update Plugins (non-major) (master)

See merge request redactmanager/redaction-service!110
2023-09-22 09:52:12 +02:00
Renovate Bot 6dceaf014e Update Plugins (non-major) (master) 2023-09-22 09:52:12 +02:00
Kilian Schüttler be0b6fe6dd Merge branch 'renovate/master-droolsversion' into 'master'
Update droolsVersion to v8.44.0.Final (master)

See merge request redactmanager/redaction-service!123
2023-09-22 09:52:03 +02:00
Renovate Bot 43c69dd544 Update droolsVersion to v8.44.0.Final (master) 2023-09-22 09:52:02 +02:00
Kilian Schüttler bc20ed4fa8 Merge branch 'renovate/master-com.iqser.red.service-persistence-service-internal-api-v1-2.x' into 'master'
Update dependency com.iqser.red.service:persistence-service-internal-api-v1 to v2.181.0 (master)

See merge request redactmanager/redaction-service!125
2023-09-22 09:51:54 +02:00
Renovate Bot ef4f399315 Update dependency com.iqser.red.service:persistence-service-internal-api-v1 to v2.181.0 (master) 2023-09-22 09:51:54 +02:00
Kilian Schüttler d524adc869 Merge branch 'renovate/master-com.iqser.red.commons-storage-commons-2.x' into 'master'
Update dependency com.iqser.red.commons:storage-commons to v2.44.0 (master)

See merge request redactmanager/redaction-service!129
2023-09-22 09:51:48 +02:00
Renovate Bot 251cebfaee Update dependency com.iqser.red.commons:storage-commons to v2.44.0 (master) 2023-09-22 09:51:48 +02:00
Kilian Schüttler 4d6b5c982e Merge branch 'renovate/master-persistenceserviceversion' into 'master'
Update dependency com.iqser.red.service:persistence-service-internal-api-v1 to v2.181.0 (master)

See merge request redactmanager/redaction-service!139
2023-09-22 09:51:44 +02:00
Renovate Bot fa81aaa5d7 Update dependency com.iqser.red.service:persistence-service-internal-api-v1 to v2.181.0 (master) 2023-09-22 09:51:44 +02:00
Kilian Schüttler 4464cd4c92 Merge branch 'renovate/master-com.iqser.red.commons-spring-commons-6.x' into 'master'
Update dependency com.iqser.red.commons:spring-commons to v6 (master)

See merge request redactmanager/redaction-service!75
2023-09-22 09:51:30 +02:00
Renovate Bot b7d312f4d6 Update dependency com.iqser.red.commons:spring-commons to v6 (master) 2023-09-22 09:51:30 +02:00
Renovate Bot 68cfde0331 Merge branch 'renovate/master-spring-core' into 'master'
Update dependency org.springframework:spring-web to v6.0.12 (master)

See merge request redactmanager/redaction-service!124
2023-09-21 19:09:26 +02:00
Renovate Bot 9b02c9c2b6 Update dependency org.springframework:spring-web to v6.0.12 (master) 2023-09-21 19:09:26 +02:00
Kilian Schüttler 4cf03123de Merge branch 'DM-285' into 'master'
DM-285: set correct versions in EntityLog

Closes DM-285

See merge request redactmanager/redaction-service!137
2023-09-20 13:04:15 +02:00
Kilian Schüttler bb4f6e1c88 DM-285: set correct versions in EntityLog 2023-09-20 13:04:15 +02:00
Ali Oezyetimoglu 72c75450e0 Merge branch 'RED-6807' into 'master'
RED-6807: updated EntityCreationService and RedactionSearchUtility to combine...

Closes RED-6807

See merge request redactmanager/redaction-service!136
2023-09-20 10:59:35 +02:00
Ali Oezyetimoglu b543952c4a RED-6807: updated EntityCreationService and RedactionSearchUtility to combine... 2023-09-20 10:59:35 +02:00
Ali Oezyetimoglu 6031d7c576 Merge branch 'DM-285' into 'master'
DM-285: fix betweenTextRanges

Closes DM-285

See merge request redactmanager/redaction-service!135
2023-09-19 15:18:12 +02:00
Kilian Schüttler 82f255a486 DM-285: fix betweenTextRanges 2023-09-19 15:18:12 +02:00
Kilian Schüttler a07f77574c Merge branch 'DM-285' into 'master'
DM-285: improve EntityLog change computation

Closes DM-285

See merge request redactmanager/redaction-service!134
2023-09-19 11:49:57 +02:00
Kilian Schüttler 5c8ff74107 DM-285: improve EntityLog change computation 2023-09-19 11:49:57 +02:00
Andrei Isvoran dfe01741e7 Merge branch 'DM-428' into 'master'
DM-428 - Fix bulk annotations not saving correctly

Closes DM-428

See merge request redactmanager/redaction-service!133
2023-09-18 14:17:21 +02:00
Andrei Isvoran ebb16c9110 DM-428 - Fix bulk annotations not saving correctly 2023-09-18 14:17:20 +02:00
Dominique Eifländer 04e618d2e9 Merge branch 'DM-285' into 'master'
DM-285: fix string NPE

Closes DM-285

See merge request redactmanager/redaction-service!132
2023-09-15 15:09:01 +02:00
Kilian Schüttler 39b754e5b1 DM-285: fix string NPE 2023-09-15 15:09:01 +02:00
Kilian Schüttler c91c136005 Merge branch 'DM-410' into 'master'
DM-410: Added timeout to Drools execution

Closes DM-410

See merge request redactmanager/redaction-service!131
2023-09-15 15:07:27 +02:00
deiflaender b3af87e932 DM-410: Added timeout to Drools execution 2023-09-15 14:27:06 +02:00
Dominique Eifländer 636066e8e7 Merge branch 'DM-285' into 'master'
DM-285: fix component drools execution

Closes DM-285

See merge request redactmanager/redaction-service!130
2023-09-15 13:11:11 +02:00
Kilian Schüttler 75d677942a DM-285: fix component drools execution 2023-09-15 13:11:11 +02:00
Kilian Schüttler ba212a525b Merge branch 'DM-285' into 'master'
DM-285: downgrade storage-service

Closes DM-285

See merge request redactmanager/redaction-service!128
2023-09-14 17:03:08 +02:00
Kilian Schüttler e410383fb6 DM-285: downgrade storage-service 2023-09-14 17:03:08 +02:00
Dominique Eifländer 9ca47631a9 Merge branch 'DM-285' into 'master'
DM-285: Entity to component mappings with new ruleset

Closes DM-285

See merge request redactmanager/redaction-service!127
2023-09-14 14:43:57 +02:00
Kilian Schüttler 27c058707d DM-285: Entity to component mappings with new ruleset 2023-09-14 14:43:57 +02:00
Kilian Schüttler 28c4fa5c87 Merge branch 'DM-285' into 'master'
DM-285: Deactivate timeout detection

Closes DM-285

See merge request redactmanager/redaction-service!126
2023-09-14 13:33:12 +02:00
Kilian Schüttler d5944cef3f DM-285: Deactivate timeout detection 2023-09-14 13:33:12 +02:00
Kilian Schüttler a3024cb838 Merge branch 'DM-285' into 'master'
DM-285: Entity to component mappings with new ruleset

Closes DM-285

See merge request redactmanager/redaction-service!119
2023-09-14 09:35:42 +02:00
Kilian Schüttler 40da9d9c5f DM-285: Entity to component mappings with new ruleset 2023-09-14 09:35:42 +02:00
Kilian Schüttler 2878cacad8 Merge branch 'DM-285-prep' into 'master'
DM-285-prep: prepare rule endpoint to avoid cyclic dependency resolution

Closes DM-285

See merge request redactmanager/redaction-service!122
2023-09-11 14:54:40 +02:00
Kilian Schüttler fcc04ed585 DM-285-prep: prepare rule endpoint to avoid cyclic dependency resolution 2023-09-11 14:54:40 +02:00
319 changed files with 64774 additions and 38211 deletions
+18 -6
View File
@@ -1,22 +1,34 @@
variables:
SONAR_PROJECT_KEY: 'RED_redaction-service'
GIT_SUBMODULE_STRATEGY: recursive
GIT_SUBMODULE_FORCE_HTTPS: "true"
include:
- project: 'gitlab/gitlab'
ref: 'main'
file: 'ci-templates/gradle_java.yml'
deploy:
deploy JavaDoc:
stage: deploy
tags:
- dind
script:
- echo "Building with gradle version ${BUILDVERSION}"
- echo "Building JavaDoc with gradle version ${BUILDVERSION}"
- gradle -Pversion=${BUILDVERSION} publish
- gradle bootBuildImage --cleanCache --publishImage -PbuildbootDockerHostNetwork=true -Pversion=${BUILDVERSION}
- echo "BUILDVERSION=$BUILDVERSION" >> version.env
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_COMMIT_BRANCH =~ /^release/
- if: $CI_COMMIT_TAG
generateJavaDoc:
stage: build
tags:
- dind
script:
- echo "Generating Javadoc..."
- gradle generateJavaDoc -PjavadocDestinationDir="javadoc"
artifacts:
reports:
dotenv: version.env
paths:
- redaction-service-v1/redaction-service-server-v1/javadoc/*
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_COMMIT_BRANCH =~ /^release/
+8
View File
@@ -0,0 +1,8 @@
[submodule "redaction-service-v1/redaction-service-server-v1/src/test/resources/files/syngenta"]
path = redaction-service-v1/redaction-service-server-v1/src/test/resources/files/syngenta
url = ssh://git@git.knecon.com:22222/fforesight/documents/syngenta.git
update = merge
[submodule "redaction-service-v1/redaction-service-server-v1/src/test/resources/files/basf"]
path = redaction-service-v1/redaction-service-server-v1/src/test/resources/files/basf
url = ssh://git@git.knecon.com:22222/fforesight/documents/basf.git
update = merge
@@ -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
View File
@@ -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>
+3 -5
View File
@@ -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 -1
View File
@@ -9,7 +9,7 @@ branch=$(git rev-parse --abbrev-ref HEAD)
commit_hash=$(git rev-parse --short=5 HEAD)
# Combine branch and commit hash
buildName="${branch}-${commit_hash}"
buildName="${USER}-${branch}-${commit_hash}"
gradle bootBuildImage --cleanCache --publishImage -PbuildbootDockerHostNetwork=true -Pversion=$buildName
echo "nexus.knecon.com:5001/red/${dir}-server-v1:$buildName"
@@ -1,13 +1,13 @@
plugins {
id("com.iqser.red.service.java-conventions")
id("io.freefair.lombok") version "8.2.2"
id("io.freefair.lombok") version "8.4"
}
description = "redaction-service-api-v1"
dependencies {
implementation("org.springframework:spring-web:6.0.11")
implementation("com.iqser.red.service:persistence-service-internal-api-v1:RED-6725")
implementation("org.springframework:spring-web:6.0.12")
implementation("com.iqser.red.service:persistence-service-internal-api-v1:2.351.0")
}
publishing {
@@ -0,0 +1,19 @@
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;
}
@@ -0,0 +1,27 @@
package com.iqser.red.service.redaction.v1.model;
import java.util.List;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.FieldDefaults;
import lombok.experimental.SuperBuilder;
@Data
@SuperBuilder
@AllArgsConstructor
@NoArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
@EqualsAndHashCode(callSuper = true)
public class DroolsBlacklistErrorMessage extends DroolsValidationMessage {
List<String> blacklistedKeywords;
public String getMessage() {
return String.format("Blacklisted keywords found in this rule: %s", String.join(", ", blacklistedKeywords));
}
}
@@ -0,0 +1,22 @@
package com.iqser.red.service.redaction.v1.model;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.FieldDefaults;
import lombok.experimental.SuperBuilder;
@Data
@SuperBuilder
@AllArgsConstructor
@NoArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
@EqualsAndHashCode(callSuper = true)
public class DroolsSyntaxDeprecatedWarnings extends DroolsValidationMessage {
String message;
}
@@ -4,17 +4,19 @@ import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.FieldDefaults;
import lombok.experimental.SuperBuilder;
@Data
@Builder
@SuperBuilder
@AllArgsConstructor
@NoArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
public class DroolsSyntaxErrorMessage {
@EqualsAndHashCode(callSuper = true)
public class DroolsSyntaxErrorMessage extends DroolsValidationMessage {
Integer line;
Integer column;
String message;
}
@@ -0,0 +1,39 @@
package com.iqser.red.service.redaction.v1.model;
import java.util.ArrayList;
import java.util.List;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.FieldDefaults;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
public class DroolsValidation {
@Builder.Default
List<DroolsSyntaxErrorMessage> syntaxErrorMessages = new ArrayList<>();
@Builder.Default
List<DroolsSyntaxDeprecatedWarnings> deprecatedWarnings = new ArrayList<>();
@Builder.Default
List<DroolsBlacklistErrorMessage> blacklistErrorMessages = new ArrayList<>();
public void addErrorMessage(int line, int column, String message) {
getSyntaxErrorMessages().add(DroolsSyntaxErrorMessage.builder().line(line).column(column).message(message).build());
}
public boolean isCompiled() {
return syntaxErrorMessages.isEmpty() && blacklistErrorMessages.isEmpty();
}
}
@@ -0,0 +1,21 @@
package com.iqser.red.service.redaction.v1.model;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.FieldDefaults;
import lombok.experimental.SuperBuilder;
@Data
@SuperBuilder
@AllArgsConstructor
@NoArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
@EqualsAndHashCode
public class DroolsValidationMessage {
Integer line;
Integer column;
}
@@ -0,0 +1,21 @@
package com.iqser.red.service.redaction.v1.model;
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.ManualRedactions;
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;
}
@@ -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;
}
@@ -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";
}
@@ -1,21 +1,18 @@
package com.iqser.red.service.redaction.v1.model;
import java.util.List;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.FieldDefaults;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@NoArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
public class DroolsSyntaxValidation {
public class RuleValidationModel {
String ruleFileType;
String rulesString;
boolean compiled;
List<DroolsSyntaxErrorMessage> droolsSyntaxErrorMessages;
}
@@ -0,0 +1,28 @@
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<>();
}
@@ -4,11 +4,12 @@ import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import com.iqser.red.service.redaction.v1.model.DroolsSyntaxValidation;
import com.iqser.red.service.redaction.v1.model.DroolsValidation;
import com.iqser.red.service.redaction.v1.model.RuleValidationModel;
public interface RedactionResource {
@PostMapping(value = "/rules/test", consumes = MediaType.APPLICATION_JSON_VALUE)
DroolsSyntaxValidation testRules(@RequestBody String rules);
DroolsValidation testRules(@RequestBody RuleValidationModel rulesValidationModel);
}
@@ -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.2"
id("org.springframework.boot") version "3.1.5"
id("io.spring.dependency-management") version "1.1.3"
id("org.sonarqube") version "4.3.0.3225"
id("io.freefair.lombok") version "8.2.2"
id("org.sonarqube") version "4.4.1.3373"
id("io.freefair.lombok") version "8.4"
}
description = "redaction-service-server-v1"
val layoutParserVersion = "0.25.0"
val layoutParserVersion = "0.96.0"
val jacksonVersion = "2.15.2"
val droolsVersion = "8.43.0.Final"
val pdfBoxVersion = "3.0.0-alpha2"
val persistenceServiceVersion = "2.160.0"
val droolsVersion = "9.44.0.Final"
val pdfBoxVersion = "3.0.0"
val persistenceServiceVersion = "2.380.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")
}
}
@@ -30,12 +33,13 @@ dependencies {
implementation("com.iqser.red.service:persistence-service-internal-api-v1:${persistenceServiceVersion}") { exclude(group = "org.springframework.boot") }
implementation("com.knecon.fforesight:layoutparser-service-internal-api:${layoutParserVersion}")
implementation("com.iqser.red.commons:spring-commons:2.7.0")
implementation("com.iqser.red.commons:spring-commons:6.2.0")
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.43.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,21 @@ 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.2")
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.2")
testImplementation("org.springframework.boot:spring-boot-starter-test:${springBootStarterVersion}")
testImplementation("com.knecon.fforesight:viewer-doc-processor:${layoutParserVersion}")
testImplementation("com.knecon.fforesight:layoutparser-service-processor:${layoutParserVersion}") {
exclude(
group = "com.iqser.red.service",
@@ -67,15 +80,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 +108,47 @@ 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))
}
}
fun parseDroolsImports(droolsFilePath: String): List<String> {
val imports = mutableListOf<String>()
val importPattern = Regex("^import\\s+(com\\.iqser\\.red\\.service\\.redaction\\.v1\\.[\\w.]+);")
val desiredPrefix = "com.iqser.red.service.redaction.v1"
File(droolsFilePath).forEachLine { line ->
importPattern.find(line)?.let { matchResult ->
val importPath = matchResult.groupValues[1].trim()
if (importPath.startsWith(desiredPrefix)) {
val formattedPath = importPath.replace('.', '/')
imports.add("$formattedPath.java")
}
}
}
return imports
}
val droolsImports = parseDroolsImports("redaction-service-v1/redaction-service-server-v1/src/main/resources/drools/all_rules_documine.drl")
tasks.register("generateJavaDoc", Javadoc::class) {
dependsOn("compileJava")
dependsOn("delombok")
classpath = project.sourceSets["main"].runtimeClasspath
source = fileTree("${buildDir}/generated/sources/delombok/java/main") {
include(droolsImports)
}
destinationDir = file(project.findProperty("javadocDestinationDir")?.toString() ?: "")
options.memberLevel = JavadocMemberLevel.PUBLIC
(options as StandardJavadocDocletOptions).apply {
header = "Redaction Service ${project.version}"
footer = "Redaction Service ${project.version}"
title = "API Documentation for Redaction Service ${project.version}"
}
}
@@ -6,19 +6,22 @@ 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;
import com.iqser.red.service.dictionarymerge.commons.DictionaryMergeService;
import com.iqser.red.service.redaction.v1.server.client.RulesClient;
import com.iqser.red.service.redaction.v1.server.settings.RedactionServiceSettings;
import com.iqser.red.storage.commons.StorageAutoConfiguration;
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)
@@ -33,16 +36,31 @@ public class Application {
}
@Bean
public ObservedAspect observedAspect(ObservationRegistry observationRegistry) {
return new ObservedAspect(observationRegistry);
}
@Bean
public TimedAspect timedAspect(MeterRegistry registry) {
return new TimedAspect(registry);
}
@Bean
public DictionaryMergeService dictionaryMergeService() {
return new DictionaryMergeService();
}
@Bean(initMethod = "findDeprecatedMethods")
public DeprecatedElementsFinder deprecatedElementsFinder() {
return new DeprecatedElementsFinder();
}
}
@@ -0,0 +1,109 @@
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 + ")";
}
}
@@ -1,4 +1,4 @@
package com.iqser.red.service.redaction.v1.server.settings;
package com.iqser.red.service.redaction.v1.server;
import org.springframework.boot.context.properties.ConfigurationProperties;
@@ -30,4 +30,6 @@ public class RedactionServiceSettings {
private int droolsExecutionTimeoutSecs = 300;
private boolean ruleExecutionSecured = true;
}
@@ -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;
}
}
@@ -1,6 +1,5 @@
package com.iqser.red.service.redaction.v1.server.client.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@@ -1,8 +1,8 @@
package com.iqser.red.service.redaction.v1.server.controller;
import com.iqser.red.commons.spring.ErrorMessage;
import com.iqser.red.service.redaction.v1.server.exception.NotFoundException;
import com.iqser.red.service.redaction.v1.server.exception.RulesValidationException;
import com.iqser.red.service.redaction.v1.server.utils.exception.NotFoundException;
import com.iqser.red.service.redaction.v1.server.utils.exception.RulesValidationException;
import lombok.extern.slf4j.Slf4j;
@@ -3,10 +3,11 @@ package com.iqser.red.service.redaction.v1.server.controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.iqser.red.service.redaction.v1.model.DroolsSyntaxValidation;
import com.iqser.red.service.redaction.v1.model.DroolsValidation;
import com.iqser.red.service.redaction.v1.model.RuleValidationModel;
import com.iqser.red.service.redaction.v1.resources.RedactionResource;
import com.iqser.red.service.redaction.v1.server.exception.RulesValidationException;
import com.iqser.red.service.redaction.v1.server.redaction.service.DroolsExecutionService;
import com.iqser.red.service.redaction.v1.server.service.drools.DroolsValidationService;
import com.iqser.red.service.redaction.v1.server.utils.exception.RulesValidationException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -16,14 +17,14 @@ import lombok.extern.slf4j.Slf4j;
@RequiredArgsConstructor
public class RedactionController implements RedactionResource {
private final DroolsExecutionService droolsExecutionService;
private final DroolsValidationService droolsValidationService;
@Override
public DroolsSyntaxValidation testRules(@RequestBody String rules) {
public DroolsValidation testRules(@RequestBody RuleValidationModel rulesValidationModel) {
try {
return droolsExecutionService.testRules(rules);
return droolsValidationService.testRules(rulesValidationModel);
} catch (Exception e) {
throw new RulesValidationException("Could not test rules: " + e.getMessage(), e);
}
@@ -1,10 +1,11 @@
package com.iqser.red.service.redaction.v1.server.controller;
import java.util.Collections;
import org.springframework.web.bind.annotation.RestController;
import com.iqser.red.service.redaction.v1.model.RuleBuilderModel;
import com.iqser.red.service.redaction.v1.resources.RuleBuilderResource;
import com.iqser.red.service.redaction.v1.server.redaction.service.RuleBuilderModelService;
import lombok.RequiredArgsConstructor;
@@ -12,13 +13,15 @@ import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class RuleBuilderController implements RuleBuilderResource {
private final RuleBuilderModelService ruleBuilderModelService;
@Override
public RuleBuilderModel getRuleBuilderModel() {
return ruleBuilderModelService.getRuleBuilderModel();
RuleBuilderModel ruleBuilderModel = new RuleBuilderModel();
ruleBuilderModel.setWhenClauses(Collections.emptyList());
ruleBuilderModel.setThenConditions(Collections.emptyList());
return ruleBuilderModel;
}
}
@@ -1,8 +0,0 @@
package com.iqser.red.service.redaction.v1.server.document.graph.entity;
public enum EntityType {
ENTITY,
RECOMMENDATION,
FALSE_POSITIVE,
FALSE_RECOMMENDATION
}
@@ -1,41 +0,0 @@
package com.iqser.red.service.redaction.v1.server.document.graph.entity;
import java.util.Objects;
public record RuleIdentifier(String type, Integer unit, Integer id) {
public static RuleIdentifier fromString(String identifier) {
String[] values = identifier.split("\\.");
if (values.length != 3) {
throw new IllegalArgumentException("Illegal rule identifier provided: " + identifier);
}
String type = values[0];
Integer group = Integer.parseInt(values[1]);
Integer id = Integer.parseInt(values[2]);
return new RuleIdentifier(type, group, id);
}
public static RuleIdentifier empty() {
return new RuleIdentifier("", null, null);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(type());
if (Objects.nonNull(unit()) && Objects.nonNull(id())) {
sb.append(".").append(unit()).append(".").append(id());
} else if (Objects.nonNull(id())) {
sb.append(".*.").append(id());
} else if (Objects.nonNull(unit())) {
sb.append(".").append(unit()).append(".*");
}
return sb.toString();
}
}
@@ -1,95 +0,0 @@
package com.iqser.red.service.redaction.v1.server.document.graph.nodes;
import java.awt.geom.Rectangle2D;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Set;
import com.iqser.red.service.redaction.v1.server.document.graph.DocumentTree;
import com.iqser.red.service.redaction.v1.server.document.graph.entity.Entity;
import com.iqser.red.service.redaction.v1.server.document.graph.entity.ManualChangeOverwrite;
import com.iqser.red.service.redaction.v1.server.document.graph.entity.MatchedRule;
import com.iqser.red.service.redaction.v1.server.document.graph.entity.TextEntity;
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock;
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlockCollector;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.FieldDefaults;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
public class Image implements GenericSemanticNode, Entity {
List<Integer> treeId;
String id;
ImageType imageType;
boolean transparent;
Rectangle2D position;
@Builder.Default
PriorityQueue<MatchedRule> matchedRuleList = new PriorityQueue<>();
@Builder.Default
ManualChangeOverwrite manualOverwrite = new ManualChangeOverwrite();
@EqualsAndHashCode.Exclude
Page page;
@EqualsAndHashCode.Exclude
DocumentTree documentTree;
@Builder.Default
@EqualsAndHashCode.Exclude
Set<TextEntity> entities = new HashSet<>();
@Override
public NodeType getType() {
return NodeType.IMAGE;
}
@Override
public TextBlock getTextBlock() {
return streamAllSubNodes().filter(SemanticNode::isLeaf).map(SemanticNode::getLeafTextBlock).collect(new TextBlockCollector());
}
@Override
public Set<Page> getPages() {
return Collections.singleton(page);
}
@Override
public String toString() {
return treeId + ": " + NodeType.IMAGE + ": " + imageType.toString() + " " + position;
}
@Override
public Map<Page, Rectangle2D> getBBox() {
Map<Page, Rectangle2D> bBoxPerPage = new HashMap<>();
bBoxPerPage.put(page, position);
return bBoxPerPage;
}
}
@@ -1,95 +0,0 @@
package com.iqser.red.service.redaction.v1.server.document.services;
import java.awt.geom.Rectangle2D;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.NoSuchElementException;
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.document.graph.entity.Entity;
import com.iqser.red.service.redaction.v1.server.document.graph.entity.PositionOnPage;
import com.iqser.red.service.redaction.v1.server.document.graph.entity.TextEntity;
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.Image;
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.ImageType;
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.SemanticNode;
import com.iqser.red.service.redaction.v1.server.document.utils.RectangleTransformations;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class ManualChangesApplicationService {
private final EntityCreationService entityCreationService;
public void recategorize(Entity entityToBeReCategorized, ManualRecategorization 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 (entityToBeReCategorized instanceof TextEntity textEntity) {
TextEntity recategorizedEntity = entityCreationService.copyEntity(textEntity, manualRecategorization.getType(), textEntity.getEntityType(), textEntity.getDeepestFullyContainingNode());
recategorizedEntity.setPositionsOnPagePerPage(textEntity.getPositionsOnPagePerPage());
recategorizedEntity.getManualOverwrite().addChange(manualRecategorization);
textEntity.removeFromGraph();
}
}
public void resizeEntityAndReinsert(TextEntity entityToBeResized, ManualResizeRedaction manualResizeRedaction) {
PositionOnPage positionOnPageToBeResized = entityToBeResized.getPositionsOnPagePerPage()
.stream()
.filter(redactionPosition -> redactionPosition.getId().equals(manualResizeRedaction.getAnnotationId()))
.findFirst()
.orElseThrow(() -> new NoSuchElementException("No redaction position with matching annotation id found!"));
positionOnPageToBeResized.setRectanglePerLine(manualResizeRedaction.getPositions().stream().map(ManualChangesApplicationService::toRectangle2D).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);
}
private void removeAndUpdateAndReInsertEntity(TextEntity entityToBeResized, ManualResizeRedaction manualResizeRedaction, int newStartOffset) {
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);
}
public void resizeImage(Image image, ManualResizeRedaction manualResizeRedaction) {
if (manualResizeRedaction.getPositions().isEmpty() || manualResizeRedaction.getPositions() == null) {
return;
}
var bBox = RectangleTransformations.rectangle2DBBox(manualResizeRedaction.getPositions().stream().map(ManualChangesApplicationService::toRectangle2D).toList());
image.setPosition(bBox);
image.getManualOverwrite().addChange(manualResizeRedaction);
}
private 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());
}
}
@@ -1,24 +0,0 @@
package com.iqser.red.service.redaction.v1.server.exception;
import lombok.Data;
@Data
public class DroolsTimeoutException extends RuntimeException {
private static final String DROOLS_TIMEOUT_MESSAGE = "Timeout during rules execution. Possible endless loop?";
private boolean reported;
public DroolsTimeoutException(Throwable cause, boolean reported) {
super(DROOLS_TIMEOUT_MESSAGE, cause);
this.reported = reported;
}
public DroolsTimeoutException(boolean reported) {
super(DROOLS_TIMEOUT_MESSAGE);
this.reported = reported;
}
}
@@ -0,0 +1,438 @@
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("Adding manual add Entries and removing skipped or imported entries");
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;
}
public long getNumberOfAffectedAnnotations(ManualRedactions manualRedactions) {
return createManualRedactionWrappers(manualRedactions).stream()
.map(ManualRedactionWrapper::getId)
.distinct()
.count();
}
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())
.withChange("section", imageRecategorization.getSection())
.withChange("legalBasis", imageRecategorization.getLegalBasis())
.withChange("value", imageRecategorization.getValue()));
}
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);
}
}
}
@@ -0,0 +1,75 @@
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);
}
}
}
}
@@ -0,0 +1,93 @@
package com.iqser.red.service.redaction.v1.server.migration;
import java.util.Collections;
import java.util.List;
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.ManualChange;
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.ManualRedactionType;
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLogEntry;
public class MigrationMapper {
public 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());
}
public static com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.ManualChange toEntityLogManualChanges(com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.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());
}
public 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;
};
}
public 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;
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;
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;
case FORCE_HINT -> com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.ManualRedactionType.FORCE;
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 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 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(MigrationMapper::toEntityLogEngine)
.collect(Collectors.toSet());
}
public List<ManualChange> migrateManualChanges(List<com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.ManualChange> manualChanges) {
if (manualChanges == null) {
return Collections.emptyList();
}
return manualChanges.stream()
.map(MigrationMapper::toEntityLogManualChanges)
.toList();
}
}
@@ -0,0 +1,93 @@
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(),
migrationRequest.getFileId());
log.info("Storing migrated entityLog and ids to migrate in DB for file {}", migrationRequest.getFileId());
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, found {} annotation ids for migration in the db, {} new manual entries, for dossierId {} and fileId {}",
migratedEntityLog.getEntityLog().getEntityLogEntry().size(),
migratedEntityLog.getMigratedIds().getMappings().size(),
migratedEntityLog.getMigratedIds().getManualRedactionEntriesToAdd().size(),
migrationRequest.getDossierId(),
migrationRequest.getFileId());
log.info("");
}
@SneakyThrows
public void sendFinished(MigrationResponse migrationResponse) {
rabbitTemplate.convertAndSend(MIGRATION_RESPONSE_QUEUE, migrationResponse);
}
}
@@ -0,0 +1,288 @@
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.ManualRedactionEntry;
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.server.model.MigratedEntityLog;
import com.iqser.red.service.redaction.v1.server.model.MigrationEntity;
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.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.service.DictionaryService;
import com.iqser.red.service.redaction.v1.server.service.ManualChangesApplicationService;
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.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;
DictionaryService dictionaryService;
ManualChangesApplicationService manualChangesApplicationService;
public MigratedEntityLog migrate(RedactionLog redactionLog, Document document, String dossierTemplateId, ManualRedactions manualRedactions, String fileId) {
log.info("Migrating entities for file {}", fileId);
List<MigrationEntity> entitiesToMigrate = calculateMigrationEntitiesFromRedactionLog(redactionLog, document, dossierTemplateId, fileId);
MigratedIds migratedIds = entitiesToMigrate.stream()
.collect(new MigratedIdsCollector());
log.info("applying manual changes to migrated entities for file {}", fileId);
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();
log.info("Writing migrated entities to entityLog for file {}", fileId);
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());
List<ManualRedactionEntry> manualRedactionEntriesToAdd = entitiesToMigrate.stream()
.filter(MigrationEntity::needsManualEntry)
.map(MigrationEntity::buildManualRedactionEntry)
.toList();
idsToMigrateInDb.setManualRedactionEntriesToAdd(manualRedactionEntriesToAdd);
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())
.flatMap(Collection::stream)
.collect(Collectors.groupingBy(BaseAnnotation::getAnnotationId));
entitiesToMigrate.forEach(migrationEntity -> migrationEntity.applyManualChanges(manualChangesPerAnnotationId.getOrDefault(migrationEntity.getOldId(),
Collections.emptyList()),
manualChangesApplicationService));
}
private static long getNumberOfApprovedEntries(RedactionLog redactionLog) {
return redactionLog.getRedactionLogEntry().size();
}
private List<MigrationEntity> calculateMigrationEntitiesFromRedactionLog(RedactionLog redactionLog, Document document, String dossierTemplateId, String fileId) {
List<MigrationEntity> images = getImageBasedMigrationEntities(redactionLog, document, fileId);
List<MigrationEntity> textMigrationEntities = getTextBasedMigrationEntities(redactionLog, document, dossierTemplateId, fileId);
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 fileId) {
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(MigrationEntity.fromRedactionLogImage(redactionLogImage, closestImage, fileId));
}
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, String fileId) {
List<MigrationEntity> entitiesToMigrate = redactionLog.getRedactionLogEntry()
.stream()
.filter(redactionLogEntry -> !redactionLogEntry.isImage())
.map(entry -> MigrationEntity.fromRedactionLogEntry(entry, dictionaryService.isHint(entry.getType(), dossierTemplateId), fileId))
.toList();
List<PrecursorEntity> precursorEntities = entitiesToMigrate.stream()
.map(MigrationEntity::getPrecursorEntity)
.toList();
log.info("Finding all possible entities");
Map<String, List<TextEntity>> tempEntitiesByValue = entityFindingUtility.findAllPossibleEntitiesAndGroupByValue(document, precursorEntities);
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 migratedEntity = EntityFromPrecursorCreationService.createCorrectEntity(migrationEntity.getPrecursorEntity(), optionalTextEntity.get(), true);
migrationEntity.setMigratedEntity(migratedEntity);
migrationEntity.setOldId(migrationEntity.getPrecursorEntity().getId());
migrationEntity.setNewId(migratedEntity.getId());
}
tempEntitiesByValue.values()
.stream()
.flatMap(Collection::stream)
.forEach(TextEntity::removeFromGraph);
return entitiesToMigrate;
}
}
@@ -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;
}
@@ -0,0 +1,18 @@
package com.iqser.red.service.redaction.v1.server.model;
import org.kie.api.runtime.KieContainer;
public record KieWrapper(KieContainer container, long rulesVersion) {
public static KieWrapper empty() {
return new KieWrapper(null, -1);
}
public boolean isPresent() {
return container != null && rulesVersion >= 0;
}
}
@@ -0,0 +1,23 @@
package com.iqser.red.service.redaction.v1.server.model;
import java.util.List;
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 com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualRedactionEntry;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MigratedEntityLog {
MigratedIds migratedIds;
EntityLog entityLog;
}
@@ -0,0 +1,444 @@
package com.iqser.red.service.redaction.v1.server.model;
import static com.iqser.red.service.redaction.v1.server.service.EntityLogCreatorService.buildEntryState;
import static com.iqser.red.service.redaction.v1.server.service.EntityLogCreatorService.buildEntryType;
import java.awt.geom.Rectangle2D;
import java.time.OffsetDateTime;
import java.util.Collections;
import java.util.LinkedList;
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.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.annotations.Rectangle;
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.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.dossiertemplate.type.DictionaryEntryType;
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.migration.MigrationMapper;
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 com.iqser.red.service.redaction.v1.server.service.ManualChangesApplicationService;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Data
@Builder
@AllArgsConstructor
@RequiredArgsConstructor
public final class MigrationEntity {
private final PrecursorEntity precursorEntity;
private final RedactionLogEntry redactionLogEntry;
private IEntity migratedEntity;
private String oldId;
private String newId;
private String fileId;
@Builder.Default
List<BaseAnnotation> manualChanges = new LinkedList<>();
public static MigrationEntity fromRedactionLogEntry(RedactionLogEntry redactionLogEntry, boolean hint, String fileId) {
PrecursorEntity precursorEntity = createPrecursorEntity(redactionLogEntry, hint);
if (precursorEntity.getEntityType().equals(EntityType.HINT) && !redactionLogEntry.isHint() && !redactionLogEntry.isRedacted()) {
precursorEntity.ignore(precursorEntity.getRuleIdentifier(), precursorEntity.getReason());
} else if (redactionLogEntry.lastChangeIsRemoved()) {
precursorEntity.remove(precursorEntity.getRuleIdentifier(), precursorEntity.getReason());
} else if (lastManualChangeIsRemove(redactionLogEntry)) {
precursorEntity.ignore(precursorEntity.getRuleIdentifier(), precursorEntity.getReason());
} else if (precursorEntity.isApplied() && redactionLogEntry.isRecommendation()) {
precursorEntity.skip(precursorEntity.getRuleIdentifier(), precursorEntity.getReason());
} else if (precursorEntity.isApplied()) {
precursorEntity.apply(precursorEntity.getRuleIdentifier(), precursorEntity.getReason(), precursorEntity.getLegalBasis());
} else {
precursorEntity.skip(precursorEntity.getRuleIdentifier(), precursorEntity.getReason());
}
return MigrationEntity.builder().precursorEntity(precursorEntity).redactionLogEntry(redactionLogEntry).oldId(redactionLogEntry.getId()).fileId(fileId).build();
}
public static MigrationEntity fromRedactionLogImage(RedactionLogEntry redactionLogImage, Image image, String fileId) {
return MigrationEntity.builder().redactionLogEntry(redactionLogImage).migratedEntity(image).oldId(redactionLogImage.getId()).newId(image.getId()).fileId(fileId).build();
}
private static boolean lastManualChangeIsRemove(RedactionLogEntry redactionLogEntry) {
if (redactionLogEntry.getManualChanges() == null) {
return false;
}
return redactionLogEntry.getManualChanges()
.stream()
.reduce((a, b) -> b)
.map(m -> m.getManualRedactionType().equals(ManualRedactionType.REMOVE_LOCALLY))
.orElse(false);
}
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())
.engines(MigrationMapper.getMigratedEngines(redactionLogEntry))
.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 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;
default -> EntryType.FALSE_POSITIVE;
};
}
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(MigrationMapper::toEntityLogChanges)
.toList());
entityLogEntry.setReference(migrateSetOfIds(redactionLogEntry.getReference(), oldToNewIdMapping));
entityLogEntry.setImportedRedactionIntersections(migrateSetOfIds(redactionLogEntry.getImportedRedactionIntersections(), oldToNewIdMapping));
entityLogEntry.setEngines(MigrationMapper.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))
.isPresent();
}
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());
}
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(image.getManualOverwrite().getSection()
.orElse(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(precursorEntity.getManualOverwrite().getSection()
.orElse(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(entity.getManualOverwrite().getSection()
.orElse(redactionLogEntry.getSection()))
.textAfter(entity.getTextAfter())
.textBefore(entity.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());
}
public boolean hasManualChangesOrComments() {
return !(redactionLogEntry.getManualChanges() == null || redactionLogEntry.getManualChanges().isEmpty()) || //
!(redactionLogEntry.getComments() == null || redactionLogEntry.getComments().isEmpty()) //
|| hasManualChanges();
}
public boolean hasManualChanges() {
return !manualChanges.isEmpty();
}
public void applyManualChanges(List<BaseAnnotation> manualChangesToApply, ManualChangesApplicationService manualChangesApplicationService) {
manualChanges.addAll(manualChangesToApply);
manualChangesToApply.forEach(manualChange -> {
if (manualChange instanceof ManualResizeRedaction manualResizeRedaction && migratedEntity instanceof TextEntity textEntity) {
manualResizeRedaction.setAnnotationId(newId);
manualChangesApplicationService.resize(textEntity, manualResizeRedaction);
} else {
migratedEntity.getManualOverwrite().addChange(manualChange);
}
});
}
public ManualRedactionEntry buildManualRedactionEntry() {
assert hasManualChanges();
// currently we need to insert a manual redaction entry, whenever an entity has been resized.
String user = manualChanges.stream()
.filter(mc -> mc instanceof ManualResizeRedaction)
.findFirst()
.orElse(manualChanges.get(0)).getUser();
OffsetDateTime requestDate = manualChanges.stream()
.filter(mc -> mc instanceof ManualResizeRedaction)
.findFirst()
.orElse(manualChanges.get(0)).getRequestDate();
return ManualRedactionEntry.builder()
.annotationId(newId)
.fileId(fileId)
.user(user)
.requestDate(requestDate)
.type(redactionLogEntry.getType())
.value(redactionLogEntry.getValue())
.reason(redactionLogEntry.getReason())
.legalBasis(redactionLogEntry.getLegalBasis())
.section(redactionLogEntry.getSection())
.rectangle(false)
.addToDictionary(false)
.addToDossierDictionary(false)
.positions(buildPositions(migratedEntity))
.textAfter(redactionLogEntry.getTextAfter())
.textBefore(redactionLogEntry.getTextBefore())
.dictionaryEntryType(DictionaryEntryType.ENTRY)
.build();
}
private List<Rectangle> buildPositions(IEntity entity) {
if (entity instanceof TextEntity textEntity) {
var positionsOnPage = textEntity.getPositionsOnPagePerPage()
.get(0);
return positionsOnPage.getRectanglePerLine()
.stream()
.map(p -> new Rectangle((float) p.getX(), (float) p.getY(), (float) p.getWidth(), (float) p.getHeight(), positionsOnPage.getPage().getNumber()))
.toList();
}
if (entity instanceof PrecursorEntity pEntity) {
return pEntity.getManualOverwrite().getPositions()
.orElse(pEntity.getEntityPosition())
.stream()
.map(p -> new Rectangle((float) p.rectangle2D().getX(),
(float) p.rectangle2D().getY(),
(float) p.rectangle2D().getWidth(),
(float) p.rectangle2D().getHeight(),
p.pageNumber()))
.toList();
}
if (entity instanceof Image image) {
Rectangle2D position = image.getManualOverwrite().getPositions()
.map(p -> p.get(0).rectangle2D())
.orElse(image.getPosition());
return List.of(new Rectangle((float) position.getX(), (float) position.getY(), (float) position.getWidth(), (float) position.getHeight(), image.getPage().getNumber()));
} else {
throw new UnsupportedOperationException();
}
}
public boolean needsManualEntry() {
return manualChanges.stream()
.anyMatch(mc -> mc instanceof ManualResizeRedaction && !((ManualResizeRedaction) mc).getUpdateDictionary()) && !(migratedEntity instanceof Image);
}
}
@@ -1,10 +1,10 @@
package com.iqser.red.service.redaction.v1.server.redaction.adapter;
package com.iqser.red.service.redaction.v1.server.model;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Stream;
import com.iqser.red.service.redaction.v1.server.document.graph.TextRange;
import com.iqser.red.service.redaction.v1.server.model.document.TextRange;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
@@ -27,13 +27,15 @@ public class NerEntities {
public boolean hasEntitiesOfType(String type) {
return nerEntityList.stream().anyMatch(nerEntity -> nerEntity.type.equals(type));
return nerEntityList.stream()
.anyMatch(nerEntity -> nerEntity.type.equals(type));
}
public Stream<NerEntity> streamEntitiesOfType(String type) {
return nerEntityList.stream().filter(nerEntity -> nerEntity.type().equals(type));
return nerEntityList.stream()
.filter(nerEntity -> nerEntity.type().equals(type));
}
@@ -0,0 +1,195 @@
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;
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;
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)
.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)
.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)
.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;
}
}
}
}
@@ -1,7 +1,8 @@
package com.iqser.red.service.redaction.v1.server.redaction.model;
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());
}
}
@@ -0,0 +1,41 @@
package com.iqser.red.service.redaction.v1.server.model.component;
import java.util.Collection;
import java.util.List;
import com.iqser.red.service.redaction.v1.server.model.drools.RuleIdentifier;
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 Component {
RuleIdentifier matchedRule;
String name;
String value;
String valueDescription;
List<Entity> references;
public boolean addReference(Entity entity) {
return references.add(entity);
}
public boolean addReferences(Collection<Entity> entities) {
return references.addAll(entities);
}
}
@@ -0,0 +1,105 @@
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;
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.analysislog.entitylog.EntryType;
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.Position;
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 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 Entity {
String id;
String type;
EntryType entryType;
EntryState state;
String value;
String reason;
String matchedRule;
String legalBasis;
boolean imported;
SemanticNode containingNode;
String section;
float[] color;
List<Position> positions;
String textBefore;
String textAfter;
int startOffset;
int endOffset;
int length;
boolean imageHasTransparency;
boolean isDictionaryEntry;
boolean isDossierDictionaryEntry;
boolean excluded;
List<Change> changes;
List<ManualChange> manualChanges;
Set<Engine> engines;
Set<String> reference;
Set<String> importedRedactionIntersections;
public static Entity fromEntityLogEntry(EntityLogEntry e, Document document) {
return Entity.builder()
.id(e.getId())
.type(e.getType())
.entryType(e.getEntryType())
.state(e.getState())
.value(e.getValue())
.reason(e.getReason())
.matchedRule(e.getMatchedRule())
.legalBasis(e.getLegalBasis())
.imported(e.isImported())
.section(e.getSection())
.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(Optional.ofNullable(e.getValue())
.orElse("").length())
.imageHasTransparency(e.isImageHasTransparency())
.isDictionaryEntry(e.isDictionaryEntry())
.isDossierDictionaryEntry(e.isDossierDictionaryEntry())
.excluded(e.isExcluded())
.changes(e.getChanges())
.manualChanges(e.getManualChanges())
.engines(e.getEngines())
.reference(e.getReference())
.importedRedactionIntersections(e.getImportedRedactionIntersections())
.build();
}
}
@@ -1,24 +1,24 @@
package com.iqser.red.service.redaction.v1.server.redaction.model.dictionary;
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.exception.NotFoundException;
import com.iqser.red.service.redaction.v1.server.document.graph.entity.MatchedRule;
import com.iqser.red.service.redaction.v1.server.document.graph.entity.TextEntity;
import com.iqser.red.service.redaction.v1.server.redaction.utils.Patterns;
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;
@@ -53,7 +53,8 @@ public class Dictionary {
public boolean hasLocalEntries() {
return dictionaryModels.stream().anyMatch(dm -> !dm.getLocalEntriesWithMatchedRules().isEmpty());
return dictionaryModels.stream()
.anyMatch(dm -> !dm.getLocalEntriesWithMatchedRules().isEmpty());
}
@@ -107,39 +108,62 @@ 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();
}
}
@@ -1,4 +1,4 @@
package com.iqser.red.service.redaction.v1.server.redaction.model.dictionary;
package com.iqser.red.service.redaction.v1.server.model.dictionary;
import java.util.HashSet;
import java.util.Set;
@@ -1,4 +1,4 @@
package com.iqser.red.service.redaction.v1.server.redaction.model.dictionary;
package com.iqser.red.service.redaction.v1.server.model.dictionary;
import java.util.Set;
@@ -1,4 +1,4 @@
package com.iqser.red.service.redaction.v1.server.redaction.model.dictionary;
package com.iqser.red.service.redaction.v1.server.model.dictionary;
import lombok.AllArgsConstructor;
import lombok.Data;
@@ -1,19 +1,20 @@
package com.iqser.red.service.redaction.v1.server.redaction.model.dictionary;
package com.iqser.red.service.redaction.v1.server.model.dictionary;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Locale;
import java.util.Set;
import java.util.stream.Collectors;
import com.iqser.red.service.dictionarymerge.commons.DictionaryEntry;
import com.iqser.red.service.dictionarymerge.commons.DictionaryEntryModel;
import com.iqser.red.service.redaction.v1.server.document.graph.entity.MatchedRule;
import com.iqser.red.service.redaction.v1.server.model.document.entity.MatchedRule;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
@Data
@AllArgsConstructor
@Slf4j
public class DictionaryModel implements Serializable {
private final String type;
@@ -27,6 +28,7 @@ public class DictionaryModel implements Serializable {
private final Set<DictionaryEntryModel> falseRecommendations;
private transient SearchImplementation entriesSearch;
private transient SearchImplementation deletionEntriesSearch;
private transient SearchImplementation falsePositiveSearch;
private transient SearchImplementation falseRecommendationsSearch;
@@ -54,16 +56,6 @@ public class DictionaryModel implements Serializable {
this.entries = entries;
this.falsePositives = falsePositives;
this.falseRecommendations = falseRecommendations;
this.entriesSearch = new SearchImplementation(this.entries.stream().filter(e -> !e.isDeleted()).map(DictionaryEntryModel::getValue).collect(Collectors.toList()),
caseInsensitive);
this.falsePositiveSearch = new SearchImplementation(this.falsePositives.stream().filter(e -> !e.isDeleted()).map(DictionaryEntryModel::getValue).collect(Collectors.toList()),
caseInsensitive);
this.falseRecommendationsSearch = new SearchImplementation(this.falseRecommendations.stream()
.filter(e -> !e.isDeleted())
.map(DictionaryEntry::getValue)
.collect(Collectors.toList()), caseInsensitive);
}
@@ -79,20 +71,34 @@ public class DictionaryModel implements Serializable {
public SearchImplementation getEntriesSearch() {
if (entriesSearch == null) {
this.entriesSearch = new SearchImplementation(this.entries.stream().filter(e -> !e.isDeleted()).map(DictionaryEntry::getValue).collect(Collectors.toList()),
caseInsensitive);
this.entriesSearch = new SearchImplementation(this.entries.stream()
.filter(e -> !e.isDeleted())
.map(DictionaryEntry::getValue)
.collect(Collectors.toList()), caseInsensitive);
}
return entriesSearch;
}
public SearchImplementation getDeletionEntriesSearch() {
if (deletionEntriesSearch == null) {
this.deletionEntriesSearch = new SearchImplementation(this.entries.stream()
.filter(DictionaryEntry::isDeleted)
.map(DictionaryEntry::getValue)
.collect(Collectors.toList()), caseInsensitive);
}
return deletionEntriesSearch;
}
public SearchImplementation getFalsePositiveSearch() {
if (falsePositiveSearch == null) {
this.falsePositiveSearch = new SearchImplementation(this.falsePositives.stream()
.filter(e -> !e.isDeleted())
.map(DictionaryEntry::getValue)
.collect(Collectors.toList()), caseInsensitive);
.filter(e -> !e.isDeleted())
.map(DictionaryEntry::getValue)
.collect(Collectors.toList()), caseInsensitive);
}
return falsePositiveSearch;
}
@@ -102,11 +108,20 @@ public class DictionaryModel implements Serializable {
if (falseRecommendationsSearch == null) {
this.falseRecommendationsSearch = new SearchImplementation(this.falseRecommendations.stream()
.filter(e -> !e.isDeleted())
.map(DictionaryEntry::getValue)
.collect(Collectors.toList()), caseInsensitive);
.filter(e -> !e.isDeleted())
.map(DictionaryEntry::getValue)
.collect(Collectors.toList()), caseInsensitive);
}
return falseRecommendationsSearch;
}
public Set<MatchedRule> getMatchedRulesForLocalDictionaryEntry(String value) {
var cleanedValue = isCaseInsensitive() ? value.toLowerCase(Locale.US) : value;
return localEntriesWithMatchedRules.get(cleanedValue);
}
}
@@ -1,4 +1,4 @@
package com.iqser.red.service.redaction.v1.server.redaction.model.dictionary;
package com.iqser.red.service.redaction.v1.server.model.dictionary;
import java.util.ArrayList;
import java.util.HashMap;
@@ -1,4 +1,4 @@
package com.iqser.red.service.redaction.v1.server.redaction.model.dictionary;
package com.iqser.red.service.redaction.v1.server.model.dictionary;
import lombok.AllArgsConstructor;
import lombok.Builder;
@@ -1,4 +1,4 @@
package com.iqser.red.service.redaction.v1.server.redaction.model.dictionary;
package com.iqser.red.service.redaction.v1.server.model.dictionary;
import java.util.ArrayList;
import java.util.Collection;
@@ -9,7 +9,7 @@ import java.util.stream.Collectors;
import org.ahocorasick.trie.Trie;
import com.iqser.red.service.redaction.v1.server.document.graph.TextRange;
import com.iqser.red.service.redaction.v1.server.model.document.TextRange;
import lombok.Data;
@@ -76,7 +76,9 @@ public class SearchImplementation {
if (ignoreCase) {
textToCheck = textToCheck.toLowerCase(Locale.ROOT);
}
return this.pattern.matcher(textToCheck).results().findAny().isPresent();
return this.pattern.matcher(textToCheck).results()
.findAny()
.isPresent();
} else {
return this.trie.containsMatch(textToCheck);
}
@@ -89,9 +91,14 @@ public class SearchImplementation {
return new ArrayList<>();
}
if (this.pattern != null) {
return this.pattern.matcher(text).results().map(r -> new TextRange(r.start(), r.end())).collect(Collectors.toList());
return this.pattern.matcher(text).results()
.map(r -> new TextRange(r.start(), r.end()))
.collect(Collectors.toList());
} else {
return this.trie.parseText(text).stream().map(r -> new TextRange(r.getStart(), r.getEnd() + 1)).collect(Collectors.toList());
return this.trie.parseText(text)
.stream()
.map(r -> new TextRange(r.getStart(), r.getEnd() + 1))
.collect(Collectors.toList());
}
}
@@ -103,9 +110,14 @@ public class SearchImplementation {
}
CharSequence subSequence = text.subSequence(region.start(), region.end());
if (this.pattern != null) {
return this.pattern.matcher(subSequence).results().map(r -> new TextRange(r.start() + region.start(), r.end() + region.start())).collect(Collectors.toList());
return this.pattern.matcher(subSequence).results()
.map(r -> new TextRange(r.start() + region.start(), r.end() + region.start()))
.collect(Collectors.toList());
} else {
return this.trie.parseText(subSequence).stream().map(r -> new TextRange(r.getStart() + region.start(), r.getEnd() + region.start() + 1)).collect(Collectors.toList());
return this.trie.parseText(subSequence)
.stream()
.map(r -> new TextRange(r.getStart() + region.start(), r.getEnd() + region.start() + 1))
.collect(Collectors.toList());
}
}
@@ -120,9 +132,14 @@ public class SearchImplementation {
if (ignoreCase) {
textToCheck = textToCheck.toLowerCase(Locale.ROOT);
}
return this.pattern.matcher(textToCheck).results().map(r -> new MatchPosition(r.start(), r.end())).collect(Collectors.toList());
return this.pattern.matcher(textToCheck).results()
.map(r -> new MatchPosition(r.start(), r.end()))
.collect(Collectors.toList());
} else {
return this.trie.parseText(textToCheck).stream().map(r -> new MatchPosition(r.getStart(), r.getEnd() + 1)).collect(Collectors.toList());
return this.trie.parseText(textToCheck)
.stream()
.map(r -> new MatchPosition(r.getStart(), r.getEnd() + 1))
.collect(Collectors.toList());
}
}
@@ -1,4 +1,4 @@
package com.iqser.red.service.redaction.v1.server.redaction.model.dictionary;
package com.iqser.red.service.redaction.v1.server.model.dictionary;
import java.util.HashMap;
import java.util.Map;
@@ -1,4 +1,4 @@
package com.iqser.red.service.redaction.v1.server.document.graph;
package com.iqser.red.service.redaction.v1.server.model.document;
import java.util.LinkedList;
import java.util.List;
@@ -1,4 +1,6 @@
package com.iqser.red.service.redaction.v1.server.document.data;
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;
@@ -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,20 +1,21 @@
package com.iqser.red.service.redaction.v1.server.document.graph;
package com.iqser.red.service.redaction.v1.server.model.document;
import static java.lang.String.format;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.GenericSemanticNode;
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.NodeType;
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.SemanticNode;
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.TableCell;
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.Document;
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.Table;
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock;
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlockCollector;
import com.iqser.red.service.redaction.v1.server.model.document.nodes.Document;
import com.iqser.red.service.redaction.v1.server.model.document.nodes.GenericSemanticNode;
import com.iqser.red.service.redaction.v1.server.model.document.nodes.NodeType;
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.model.document.textblock.TextBlockCollector;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
@@ -39,7 +40,10 @@ public class DocumentTree {
public TextBlock buildTextBlock() {
return allEntriesInOrder().map(Entry::getNode).filter(SemanticNode::isLeaf).map(SemanticNode::getLeafTextBlock).collect(new TextBlockCollector());
return allEntriesInOrder().map(Entry::getNode)
.filter(SemanticNode::isLeaf)
.map(SemanticNode::getLeafTextBlock)
.collect(new TextBlockCollector());
}
@@ -113,13 +117,16 @@ public class DocumentTree {
public Stream<SemanticNode> childNodes(List<Integer> treeId) {
return getEntryById(treeId).children.stream().map(Entry::getNode);
return getEntryById(treeId).children.stream()
.map(Entry::getNode);
}
public Stream<SemanticNode> childNodesOfType(List<Integer> treeId, NodeType nodeType) {
return getEntryById(treeId).children.stream().filter(entry -> entry.node.getType().equals(nodeType)).map(Entry::getNode);
return getEntryById(treeId).children.stream()
.filter(entry -> entry.node.getType().equals(nodeType))
.map(Entry::getNode);
}
@@ -135,6 +142,48 @@ public class DocumentTree {
}
public Optional<SemanticNode> getNextSibling(List<Integer> treeId) {
var siblingTreeId = getNextSiblingId(treeId);
if (!entryExists(siblingTreeId)) {
return Optional.empty();
}
return Optional.of(getEntryById(siblingTreeId).getNode());
}
public List<Integer> getNextSiblingId(List<Integer> treeId) {
List<Integer> siblingTreeId = new LinkedList<>();
for (int i = 0; i < treeId.size() - 1; i++) {
siblingTreeId.add(treeId.get(i));
}
siblingTreeId.add(treeId.get(treeId.size() - 1) + 1);
return siblingTreeId;
}
public Optional<SemanticNode> getPreviousSibling(List<Integer> treeId) {
var siblingTreeId = getPreviousSiblingId(treeId);
if (!entryExists(siblingTreeId)) {
return Optional.empty();
}
return Optional.of(getEntryById(siblingTreeId).getNode());
}
public List<Integer> getPreviousSiblingId(List<Integer> treeId) {
List<Integer> siblingTreeId = new LinkedList<>();
for (int i = 0; i < treeId.size() - 1; i++) {
siblingTreeId.add(treeId.get(i));
}
siblingTreeId.add(treeId.get(treeId.size() - 1) - 1);
return siblingTreeId;
}
public Entry getEntryById(List<Integer> treeId) {
if (treeId.isEmpty()) {
@@ -156,26 +205,32 @@ public class DocumentTree {
public Stream<Entry> allEntriesInOrder() {
return Stream.of(root).flatMap(DocumentTree::flatten);
return Stream.of(root)
.flatMap(DocumentTree::flatten);
}
public Stream<Entry> allSubEntriesInOrder(List<Integer> parentId) {
return getEntryById(parentId).children.stream().flatMap(DocumentTree::flatten);
return getEntryById(parentId).children.stream()
.flatMap(DocumentTree::flatten);
}
@Override
public String toString() {
return String.join("\n", allEntriesInOrder().map(Entry::toString).toList());
return String.join("\n",
allEntriesInOrder().map(Entry::toString)
.toList());
}
private static Stream<Entry> flatten(Entry entry) {
return Stream.concat(Stream.of(entry), entry.children.stream().flatMap(DocumentTree::flatten));
return Stream.concat(Stream.of(entry),
entry.children.stream()
.flatMap(DocumentTree::flatten));
}
@@ -1,4 +1,4 @@
package com.iqser.red.service.redaction.v1.server.document.graph;
package com.iqser.red.service.redaction.v1.server.model.document;
import static java.lang.String.format;
@@ -6,13 +6,14 @@ import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock;
import com.iqser.red.service.redaction.v1.server.model.document.textblock.TextBlock;
import lombok.EqualsAndHashCode;
import lombok.Setter;
@Setter
@EqualsAndHashCode
@SuppressWarnings("PMD.AvoidFieldNameMatchingMethodName")
public class TextRange implements Comparable<TextRange> {
private int start;
@@ -91,8 +92,13 @@ public class TextRange implements Comparable<TextRange> {
public List<TextRange> split(List<Integer> splitIndices) {
if (splitIndices.stream().anyMatch(idx -> !this.contains(idx))) {
throw new IndexOutOfBoundsException(format("%s splitting indices are out of range for %s", splitIndices.stream().filter(idx -> !this.contains(idx)).toList(), this));
if (splitIndices.stream()
.anyMatch(idx -> !this.contains(idx))) {
throw new IndexOutOfBoundsException(format("%s splitting indices are out of range for %s",
splitIndices.stream()
.filter(idx -> !this.contains(idx))
.toList(),
this));
}
List<TextRange> splitBoundaries = new LinkedList<>();
int previousIndex = start;
@@ -112,8 +118,14 @@ public class TextRange implements Comparable<TextRange> {
public static TextRange merge(Collection<TextRange> boundaries) {
int minStart = boundaries.stream().mapToInt(TextRange::start).min().orElseThrow(IllegalArgumentException::new);
int maxEnd = boundaries.stream().mapToInt(TextRange::end).max().orElseThrow(IllegalArgumentException::new);
int minStart = boundaries.stream()
.mapToInt(TextRange::start)
.min()
.orElseThrow(IllegalArgumentException::new);
int maxEnd = boundaries.stream()
.mapToInt(TextRange::end)
.max()
.orElseThrow(IllegalArgumentException::new);
return new TextRange(minStart, maxEnd);
}
@@ -0,0 +1,10 @@
package com.iqser.red.service.redaction.v1.server.model.document.entity;
public enum EntityType {
ENTITY,
HINT,
RECOMMENDATION,
FALSE_POSITIVE,
FALSE_RECOMMENDATION,
DICTIONARY_REMOVAL
}
@@ -1,13 +1,16 @@
package com.iqser.red.service.redaction.v1.server.document.graph.entity;
package com.iqser.red.service.redaction.v1.server.model.document.entity;
import java.util.Collection;
import java.util.HashSet;
import java.util.PriorityQueue;
import java.util.Set;
import com.iqser.red.service.redaction.v1.server.model.document.TextRange;
import com.iqser.red.service.redaction.v1.server.model.drools.RuleIdentifier;
import lombok.NonNull;
public interface Entity {
public interface IEntity {
PriorityQueue<MatchedRule> getMatchedRuleList();
@@ -15,28 +18,66 @@ public interface Entity {
ManualChangeOverwrite getManualOverwrite();
// Don't use default accessor pattern (e.g. isIgnored()), as it might lead to errors in drools due to property-specific optimization of the drools planner.
String getValue();
TextRange getTextRange();
String type();
default int length() {
return value().length();
}
default String value() {
return getManualOverwrite().getValue()
.orElse(getValue() == null ? "" : getValue());
}
// Don't use default accessor pattern (e.g. isApplied()), as it might lead to errors in drools due to property-specific optimization of the drools planner.
default boolean applied() {
return getManualOverwrite().getApplied()
.orElse(getMatchedRule().isApplied());
}
default boolean skipped() {
return !applied();
}
default boolean ignored() {
return getManualOverwrite().getIgnored().orElse(getMatchedRule().isIgnored());
return getManualOverwrite().getIgnored()
.orElse(getMatchedRule().isIgnored());
}
default boolean removed() {
return getManualOverwrite().getRemoved().orElse(getMatchedRule().isRemoved());
return getManualOverwrite().getRemoved()
.orElse(getMatchedRule().isRemoved());
}
default boolean resized() {
return getManualOverwrite().getResized().orElse(false);
return getManualOverwrite().getResized()
.orElse(false);
}
default boolean applied() {
default boolean active() {
return getManualOverwrite().getApplied().orElse(getMatchedRule().isApplied());
return !(removed() || ignored());
}
@@ -52,12 +93,6 @@ public interface Entity {
}
default boolean active() {
return !(removed() || ignored());
}
default void redact(@NonNull String ruleIdentifier, String reason, @NonNull String legalBasis) {
if (legalBasis.isBlank() || legalBasis.isEmpty()) {
@@ -79,17 +114,6 @@ public interface Entity {
}
default void force(@NonNull String ruleIdentifier, String reason, String legalBasis) {
addMatchedRule(MatchedRule.builder()
.ruleIdentifier(RuleIdentifier.fromString(ruleIdentifier))
.reason(reason)
.legalBasis(getLegalBasisOrPreviousLegalBasisOrPlaceHolder(legalBasis))
.applied(true)
.build());
}
default void skip(@NonNull String ruleIdentifier, String reason) {
addMatchedRule(MatchedRule.builder().ruleIdentifier(RuleIdentifier.fromString(ruleIdentifier)).reason(reason).build());
@@ -108,30 +132,18 @@ public interface Entity {
}
private String getLegalBasisOrPreviousLegalBasisOrPlaceHolder(String legalBasis) {
if (legalBasis == null || legalBasis.isBlank() || legalBasis.isEmpty()) {
if (getMatchedRule() == null || !getMatchedRule().isApplied()) {
return "n-a";
}
return getMatchedRule().getLegalBasis();
}
return legalBasis;
}
default void applyWithLineBreaks(@NonNull String ruleIdentifier, String reason, @NonNull String legalBasis) {
if (legalBasis.isBlank() || legalBasis.isEmpty()) {
throw new IllegalArgumentException("legal basis cannot be empty when redacting an entity");
}
getMatchedRuleList().add(MatchedRule.builder()
.ruleIdentifier(RuleIdentifier.fromString(ruleIdentifier))
.reason(reason)
.legalBasis(legalBasis)
.applied(true)
.writeValueWithLineBreaks(true)
.build());
.ruleIdentifier(RuleIdentifier.fromString(ruleIdentifier))
.reason(reason)
.legalBasis(legalBasis)
.applied(true)
.writeValueWithLineBreaks(true)
.build());
}
@@ -141,12 +153,12 @@ public interface Entity {
throw new IllegalArgumentException("legal basis cannot be empty when redacting an entity");
}
getMatchedRuleList().add(MatchedRule.builder()
.ruleIdentifier(RuleIdentifier.fromString(ruleIdentifier))
.reason(reason)
.legalBasis(legalBasis)
.applied(true)
.references(new HashSet<>(references))
.build());
.ruleIdentifier(RuleIdentifier.fromString(ruleIdentifier))
.reason(reason)
.legalBasis(legalBasis)
.applied(true)
.references(new HashSet<>(references))
.build());
}
@@ -200,7 +212,8 @@ public interface Entity {
default String legalBasis() {
return getManualOverwrite().getLegalBasis().orElse(getMatchedRule().getLegalBasis());
return getManualOverwrite().getLegalBasis()
.orElse(getMatchedRule().getLegalBasis());
}
}
@@ -1,4 +1,4 @@
package com.iqser.red.service.redaction.v1.server.document.graph.entity;
package com.iqser.red.service.redaction.v1.server.model.document.entity;
import java.util.Collections;
import java.util.Comparator;
@@ -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,25 @@ 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();
section = recategorization.getSection();
value = recategorization.getValue();
if (recategorization.getLegalBasis() != null && !recategorization.getLegalBasis().isEmpty()) {
legalBasis = recategorization.getLegalBasis();
}
}
descriptions.add(MANUAL_CHANGE_DESCRIPTIONS.get(manualChange.getClass()));
@@ -114,6 +144,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 +237,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);
}
}
@@ -1,10 +1,13 @@
package com.iqser.red.service.redaction.v1.server.document.graph.entity;
package com.iqser.red.service.redaction.v1.server.model.document.entity;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import com.iqser.red.service.redaction.v1.server.model.drools.RuleIdentifier;
import com.iqser.red.service.redaction.v1.server.model.drools.RuleType;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
@@ -19,28 +22,44 @@ import lombok.experimental.FieldDefaults;
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public final class MatchedRule implements Comparable<MatchedRule> {
public static final String FINAL_TYPE = "FINAL";
public static final String ELIMINATION_RULE_TYPE = "X";
private static final List<String> RULE_TYPE_PRIORITIES = List.of(FINAL_TYPE, ELIMINATION_RULE_TYPE);
public static final RuleType FINAL_TYPE = RuleType.fromString("FINAL");
public static final RuleType ELIMINATION_RULE_TYPE = RuleType.fromString("X");
public static final RuleType IMPORTED_TYPE = RuleType.fromString("IMP");
public static final RuleType DICTIONARY_TYPE = RuleType.fromString("DICT");
private static final List<RuleType> RULE_TYPE_PRIORITIES = List.of(FINAL_TYPE, ELIMINATION_RULE_TYPE, IMPORTED_TYPE, DICTIONARY_TYPE);
@Builder.Default
RuleIdentifier ruleIdentifier = RuleIdentifier.empty();
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();
public static MatchedRule empty() {
return MatchedRule.builder().build();
return MatchedRule.builder().ruleIdentifier(RuleIdentifier.empty()).build();
}
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();
}
@@ -80,7 +99,19 @@ public final class MatchedRule implements Comparable<MatchedRule> {
@Override
public String toString() {
return "MatchedRule[ruleIdentifier=" + ruleIdentifier + ", reason=" + reason + ", legalBasis=" + legalBasis + ", applied=" + applied + ", writeValueWithLineBreaks=" + writeValueWithLineBreaks + ", references=" + references + ']';
return "MatchedRule[ruleIdentifier="
+ ruleIdentifier
+ ", reason="
+ reason
+ ", legalBasis="
+ legalBasis
+ ", applied="
+ applied
+ ", writeValueWithLineBreaks="
+ writeValueWithLineBreaks
+ ", references="
+ references
+ ']';
}
}
@@ -1,9 +1,9 @@
package com.iqser.red.service.redaction.v1.server.document.graph.entity;
package com.iqser.red.service.redaction.v1.server.model.document.entity;
import java.awt.geom.Rectangle2D;
import java.util.List;
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.Page;
import com.iqser.red.service.redaction.v1.server.model.document.nodes.Page;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
@@ -1,6 +1,7 @@
package com.iqser.red.service.redaction.v1.server.document.graph.entity;
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;
@@ -10,11 +11,11 @@ import java.util.Map;
import java.util.PriorityQueue;
import java.util.Set;
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Engine;
import com.iqser.red.service.redaction.v1.server.document.graph.TextRange;
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.Page;
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.SemanticNode;
import com.iqser.red.service.redaction.v1.server.redaction.utils.IdBuilder;
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.document.nodes.Page;
import com.iqser.red.service.redaction.v1.server.model.document.nodes.SemanticNode;
import com.iqser.red.service.redaction.v1.server.utils.IdBuilder;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
@@ -28,21 +29,23 @@ import lombok.experimental.FieldDefaults;
@AllArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class TextEntity implements Entity {
@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 ManualChangesApplicationService::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;
@@ -50,7 +53,6 @@ public class TextEntity implements Entity {
@Builder.Default
Set<Engine> engines = new HashSet<>();
// inferred on graph insertion
String value;
String textBefore;
@@ -63,33 +65,66 @@ public class TextEntity implements Entity {
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);
return intersectingNodes.stream()
.anyMatch(clazz::isInstance);
}
public boolean occursInNode(SemanticNode semanticNode) {
return intersectingNodes.stream().anyMatch(node -> node.equals(semanticNode));
return intersectingNodes.stream()
.anyMatch(node -> node.equals(semanticNode));
}
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());
}
@@ -126,8 +161,10 @@ public class TextEntity implements Entity {
.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();
positionsOnPagePerPage = rectanglesPerLinePerPage.entrySet()
.stream()
.map(entry -> buildPositionOnPage(firstPage, id, entry))
.toList();
}
return positionsOnPagePerPage;
}
@@ -145,19 +182,37 @@ public class TextEntity implements Entity {
public boolean containedBy(TextEntity textEntity) {
return this.textRange.containedBy(textEntity.getTextRange());
return this.textRange.containedBy(textEntity.getTextRange()) //
|| duplicateTextRanges.stream()
.anyMatch(duplicateTextRange -> duplicateTextRange.containedBy(textEntity.textRange)) //
|| duplicateTextRanges.stream()
.anyMatch(duplicateTextRange -> textEntity.getDuplicateTextRanges()
.stream()
.anyMatch(duplicateTextRange::containedBy));
}
public boolean contains(TextEntity textEntity) {
return this.textRange.contains(textEntity.getTextRange());
return this.textRange.contains(textEntity.getTextRange()) //
|| duplicateTextRanges.stream()
.anyMatch(duplicateTextRange -> duplicateTextRange.contains(textEntity.textRange)) //
|| duplicateTextRanges.stream()
.anyMatch(duplicateTextRange -> textEntity.getDuplicateTextRanges()
.stream()
.anyMatch(duplicateTextRange::contains));
}
public boolean intersects(TextEntity textEntity) {
return this.textRange.intersects(textEntity.getTextRange());
return this.textRange.intersects(textEntity.getTextRange()) //
|| duplicateTextRanges.stream()
.anyMatch(duplicateTextRange -> duplicateTextRange.intersects(textEntity.textRange)) //
|| duplicateTextRanges.stream()
.anyMatch(duplicateTextRange -> textEntity.getDuplicateTextRanges()
.stream()
.anyMatch(duplicateTextRange::intersects));
}
@@ -175,7 +230,8 @@ public class TextEntity implements Entity {
public boolean matchesAnnotationId(String manualRedactionId) {
return getPositionsOnPagePerPage().stream().anyMatch(entityPosition -> entityPosition.getId().equals(manualRedactionId));
return getPositionsOnPagePerPage().stream()
.anyMatch(entityPosition -> entityPosition.getId().equals(manualRedactionId));
}
@@ -194,11 +250,27 @@ public class TextEntity implements Entity {
});
sb.delete(sb.length() - 2, sb.length());
sb.append("], type = \"");
sb.append(type);
sb.append(type());
sb.append("\", EntityType.");
sb.append(entityType);
sb.append("]");
return sb.toString();
}
@Override
public String type() {
return getManualOverwrite().getType()
.orElse(type);
}
@Override
public String value() {
return getManualOverwrite().getValue()
.orElse(getMatchedRule().isWriteValueWithLineBreaks() ? getValueWithLineBreaks() : value);
}
}
@@ -1,4 +1,4 @@
package com.iqser.red.service.redaction.v1.server.document.graph.nodes;
package com.iqser.red.service.redaction.v1.server.model.document.nodes;
import java.awt.geom.Rectangle2D;
import java.util.Collections;
@@ -10,15 +10,17 @@ import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.iqser.red.service.redaction.v1.server.document.graph.DocumentTree;
import com.iqser.red.service.redaction.v1.server.document.graph.entity.TextEntity;
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock;
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlockCollector;
import com.iqser.red.service.redaction.v1.server.model.document.DocumentTree;
import com.iqser.red.service.redaction.v1.server.model.document.entity.TextEntity;
import com.iqser.red.service.redaction.v1.server.model.document.textblock.TextBlock;
import com.iqser.red.service.redaction.v1.server.model.document.textblock.TextBlockCollector;
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.LayoutEngine;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.FieldDefaults;
@@ -27,8 +29,14 @@ 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();
@Builder.Default
Set<LayoutEngine> engines = new HashSet<>(Set.of(LayoutEngine.ALGORITHM));
Set<Page> pages;
DocumentTree documentTree;
Integer numberOfPages;
@@ -49,7 +57,7 @@ public class Document implements GenericSemanticNode {
public TextBlock getTextBlock() {
if (textBlock == null) {
textBlock = streamTerminalTextBlocksInOrder().collect(new TextBlockCollector());
textBlock = GenericSemanticNode.super.getTextBlock();
}
return textBlock;
}
@@ -57,20 +65,14 @@ public class Document implements GenericSemanticNode {
public List<Section> getMainSections() {
return streamChildrenOfType(NodeType.SECTION).map(node -> (Section) node).collect(Collectors.toList());
return streamChildrenOfType(NodeType.SECTION).map(node -> (Section) node)
.collect(Collectors.toList());
}
public Stream<TextBlock> streamTerminalTextBlocksInOrder() {
return streamAllNodes().filter(SemanticNode::isLeaf).map(SemanticNode::getLeafTextBlock);
}
@Override
public List<Integer> getTreeId() {
return Collections.emptyList();
return streamAllNodes().filter(SemanticNode::isLeaf).map(SemanticNode::getTextBlock);
}
@@ -91,13 +93,16 @@ public class Document implements GenericSemanticNode {
@Override
public Headline getHeadline() {
return streamAllSubNodesOfType(NodeType.HEADLINE).map(node -> (Headline) node).findFirst().orElseGet(Headline::empty);
return streamAllSubNodesOfType(NodeType.HEADLINE).map(node -> (Headline) node)
.findFirst()
.orElseGet(Headline::empty);
}
private Stream<SemanticNode> streamAllNodes() {
return documentTree.allEntriesInOrder().map(DocumentTree.Entry::getNode);
return documentTree.allEntriesInOrder()
.map(DocumentTree.Entry::getNode);
}
@@ -0,0 +1,34 @@
package com.iqser.red.service.redaction.v1.server.model.document.nodes;
import java.util.stream.Stream;
import com.iqser.red.service.redaction.v1.server.model.document.textblock.TextBlock;
import com.iqser.red.service.redaction.v1.server.model.document.textblock.TextBlockCollector;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.SuperBuilder;
@Data
@EqualsAndHashCode(callSuper = true)
@SuperBuilder
public class DuplicatedParagraph extends Paragraph {
TextBlock unsortedLeafTextBlock;
@Override
public TextBlock getTextBlock() {
return Stream.of(leafTextBlock, unsortedLeafTextBlock).collect(new TextBlockCollector());
}
@Override
public String toString() {
return super.toString();
}
}
@@ -1,12 +1,15 @@
package com.iqser.red.service.redaction.v1.server.document.graph.nodes;
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.document.graph.DocumentTree;
import com.iqser.red.service.redaction.v1.server.document.graph.entity.TextEntity;
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock;
import com.iqser.red.service.redaction.v1.server.model.document.DocumentTree;
import com.iqser.red.service.redaction.v1.server.model.document.entity.TextEntity;
import com.iqser.red.service.redaction.v1.server.model.document.textblock.TextBlock;
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.LayoutEngine;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
@@ -21,21 +24,25 @@ 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();
@Builder.Default
Set<LayoutEngine> engines = new HashSet<>(Set.of(LayoutEngine.ALGORITHM));
@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 +78,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;
}
}
@@ -1,4 +1,4 @@
package com.iqser.red.service.redaction.v1.server.document.graph.nodes;
package com.iqser.red.service.redaction.v1.server.model.document.nodes;
public interface GenericSemanticNode extends SemanticNode {
@@ -1,12 +1,15 @@
package com.iqser.red.service.redaction.v1.server.document.graph.nodes;
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.document.graph.entity.TextEntity;
import com.iqser.red.service.redaction.v1.server.document.graph.DocumentTree;
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock;
import com.iqser.red.service.redaction.v1.server.model.document.entity.TextEntity;
import com.iqser.red.service.redaction.v1.server.model.document.DocumentTree;
import com.iqser.red.service.redaction.v1.server.model.document.textblock.TextBlock;
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.LayoutEngine;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
@@ -21,21 +24,25 @@ 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();
@Builder.Default
Set<LayoutEngine> engines = new HashSet<>(Set.of(LayoutEngine.ALGORITHM));
@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 +78,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;
}
}
@@ -1,13 +1,16 @@
package com.iqser.red.service.redaction.v1.server.document.graph.nodes;
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.document.graph.DocumentTree;
import com.iqser.red.service.redaction.v1.server.document.graph.entity.TextEntity;
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.AtomicTextBlock;
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock;
import com.iqser.red.service.redaction.v1.server.model.document.DocumentTree;
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.knecon.fforesight.service.layoutparser.internal.api.data.redaction.LayoutEngine;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
@@ -22,19 +25,23 @@ import lombok.experimental.FieldDefaults;
@AllArgsConstructor
@NoArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Headline implements GenericSemanticNode {
@Builder.Default
Set<LayoutEngine> engines = new HashSet<>(Set.of(LayoutEngine.ALGORITHM));
@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 +78,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() {
@@ -89,7 +106,9 @@ public class Headline implements GenericSemanticNode {
public boolean hasParagraphs() {
return getParent().streamAllSubNodesOfType(NodeType.PARAGRAPH).findFirst().isPresent();
return getParent().streamAllSubNodesOfType(NodeType.PARAGRAPH)
.findFirst()
.isPresent();
}
}
@@ -0,0 +1,165 @@
package com.iqser.red.service.redaction.v1.server.model.document.nodes;
import java.awt.geom.Rectangle2D;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Set;
import com.iqser.red.service.redaction.v1.server.model.document.DocumentTree;
import com.iqser.red.service.redaction.v1.server.model.document.TextRange;
import com.iqser.red.service.redaction.v1.server.model.document.entity.IEntity;
import com.iqser.red.service.redaction.v1.server.model.document.entity.ManualChangeOverwrite;
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.model.document.textblock.TextBlock;
import com.iqser.red.service.redaction.v1.server.model.document.textblock.TextBlockCollector;
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.LayoutEngine;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.FieldDefaults;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Image implements GenericSemanticNode, IEntity {
@Builder.Default
Set<LayoutEngine> engines = new HashSet<>(Set.of(LayoutEngine.ALGORITHM));
@EqualsAndHashCode.Include
List<Integer> treeId;
String id;
ImageType imageType;
boolean transparent;
Rectangle2D position;
@Builder.Default
PriorityQueue<MatchedRule> matchedRuleList = new PriorityQueue<>();
@Builder.Default
ManualChangeOverwrite manualOverwrite = new ManualChangeOverwrite();
@EqualsAndHashCode.Exclude
Page page;
@EqualsAndHashCode.Exclude
DocumentTree documentTree;
@Builder.Default
@EqualsAndHashCode.Exclude
Set<TextEntity> entities = new HashSet<>();
@Override
public NodeType getType() {
return NodeType.IMAGE;
}
@Override
public TextBlock getTextBlock() {
return streamAllSubNodes().filter(SemanticNode::isLeaf)
.map(SemanticNode::getLeafTextBlock)
.collect(new TextBlockCollector());
}
@Override
public Set<Page> getPages() {
return Collections.singleton(page);
}
@Override
public TextRange getTextRange() {
return GenericSemanticNode.super.getTextRange();
}
@Override
public String type() {
return getManualOverwrite().getType()
.orElse(imageType.toString().toLowerCase(Locale.ENGLISH));
}
@Override
public String toString() {
return treeId + ": " + getValue() + " " + position;
}
@Override
public Map<Page, Rectangle2D> getBBox() {
Map<Page, Rectangle2D> bBoxPerPage = new HashMap<>();
bBoxPerPage.put(page, position);
return bBoxPerPage;
}
@Override
public String getValue() {
return NodeType.IMAGE + ":" + camelCase(imageType.toString());
}
private String camelCase(String name) {
return name.charAt(0) + name.substring(1).toLowerCase(Locale.ENGLISH);
}
public boolean mostlyContainedBy(Image image, double containmentThreshold) {
Map<Page, Rectangle2D> bboxImage = image.getBBox();
Map<Page, Rectangle2D> bbox = this.getBBox();
//image needs to be on the same page
if (bboxImage.get(this.page) != null) {
Rectangle2D intersection = bboxImage.get(this.page).createIntersection(bbox.get(this.page));
double calculatedIntersection = intersection.getWidth() * intersection.getHeight();
double area = bbox.get(this.page).getWidth() * bbox.get(this.page).getHeight();
return (calculatedIntersection / area) > containmentThreshold;
}
return false;
}
public boolean mostlyContains(Image image, double containmentThreshold) {
Map<Page, Rectangle2D> bboxImage = image.getBBox();
Map<Page, Rectangle2D> bbox = this.getBBox();
Rectangle2D intersection = bbox.get(this.page).createIntersection(bboxImage.get(this.page));
double calculatedIntersection = intersection.getWidth() * intersection.getHeight();
double area = bbox.get(this.page).getWidth() * bbox.get(this.page).getHeight();
return (area / calculatedIntersection) > containmentThreshold;
}
public int length() {
return 0;
}
}
@@ -1,4 +1,4 @@
package com.iqser.red.service.redaction.v1.server.document.graph.nodes;
package com.iqser.red.service.redaction.v1.server.model.document.nodes;
import java.util.Locale;
@@ -1,4 +1,4 @@
package com.iqser.red.service.redaction.v1.server.document.graph.nodes;
package com.iqser.red.service.redaction.v1.server.model.document.nodes;
import java.util.Locale;
@@ -16,6 +16,6 @@ public enum NodeType {
public String toString() {
return this.name().charAt(0) + this.name().substring(1).toLowerCase(Locale.ROOT);
return this.name().charAt(0) + this.name().substring(1).toLowerCase(Locale.ENGLISH);
}
}
@@ -1,12 +1,12 @@
package com.iqser.red.service.redaction.v1.server.document.graph.nodes;
package com.iqser.red.service.redaction.v1.server.model.document.nodes;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.iqser.red.service.redaction.v1.server.document.graph.entity.TextEntity;
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock;
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlockCollector;
import com.iqser.red.service.redaction.v1.server.model.document.entity.TextEntity;
import com.iqser.red.service.redaction.v1.server.model.document.textblock.TextBlock;
import com.iqser.red.service.redaction.v1.server.model.document.textblock.TextBlockCollector;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
@@ -23,32 +23,32 @@ 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<>();
public TextBlock getMainBodyTextBlock() {
return mainBody.stream().filter(SemanticNode::isLeaf).map(SemanticNode::getLeafTextBlock).collect(new TextBlockCollector());
return mainBody.stream()
.filter(SemanticNode::isLeaf)
.map(SemanticNode::getLeafTextBlock)
.collect(new TextBlockCollector());
}
@@ -58,18 +58,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();
}
}
@@ -1,12 +1,15 @@
package com.iqser.red.service.redaction.v1.server.document.graph.nodes;
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.document.graph.DocumentTree;
import com.iqser.red.service.redaction.v1.server.document.graph.entity.TextEntity;
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock;
import com.iqser.red.service.redaction.v1.server.model.document.DocumentTree;
import com.iqser.red.service.redaction.v1.server.model.document.entity.TextEntity;
import com.iqser.red.service.redaction.v1.server.model.document.textblock.TextBlock;
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.LayoutEngine;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
@@ -14,23 +17,29 @@ import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.FieldDefaults;
import lombok.experimental.SuperBuilder;
@Data
@Builder
@SuperBuilder
@AllArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
@FieldDefaults(level = AccessLevel.PROTECTED)
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Paragraph implements GenericSemanticNode {
@Builder.Default
Set<LayoutEngine> engines = new HashSet<>(Set.of(LayoutEngine.ALGORITHM));
@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 +68,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;
}
}
@@ -1,19 +1,23 @@
package com.iqser.red.service.redaction.v1.server.document.graph.nodes;
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.document.graph.entity.TextEntity;
import com.iqser.red.service.redaction.v1.server.document.graph.DocumentTree;
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock;
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlockCollector;
import com.iqser.red.service.redaction.v1.server.model.document.entity.TextEntity;
import com.iqser.red.service.redaction.v1.server.model.document.DocumentTree;
import com.iqser.red.service.redaction.v1.server.model.document.textblock.TextBlock;
import com.iqser.red.service.redaction.v1.server.model.document.textblock.TextBlockCollector;
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.LayoutEngine;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;
import lombok.extern.slf4j.Slf4j;
@@ -22,18 +26,23 @@ 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;
@Builder.Default
Set<LayoutEngine> engines = new HashSet<>(Set.of(LayoutEngine.ALGORITHM));
@Override
public NodeType getType() {
@@ -44,7 +53,8 @@ public class Section implements GenericSemanticNode {
public boolean hasTables() {
return streamAllSubNodesOfType(NodeType.TABLE).findAny().isPresent();
return streamAllSubNodesOfType(NodeType.TABLE).findAny()
.isPresent();
}
@@ -59,7 +69,7 @@ public class Section implements GenericSemanticNode {
public TextBlock getTextBlock() {
if (textBlock == null) {
textBlock = streamAllSubNodes().filter(SemanticNode::isLeaf).map(SemanticNode::getLeafTextBlock).collect(new TextBlockCollector());
textBlock = GenericSemanticNode.super.getTextBlock();
}
return textBlock;
}
@@ -92,4 +102,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;
}
}
@@ -1,4 +1,4 @@
package com.iqser.red.service.redaction.v1.server.document.graph.nodes;
package com.iqser.red.service.redaction.v1.server.model.document.nodes;
import java.util.Collections;
import java.util.LinkedList;
@@ -72,7 +72,11 @@ public class SectionIdentifier {
}
identifiers.add(Integer.parseInt(numericalIdentifier.trim()));
}
return new SectionIdentifier(Format.NUMERICAL, identifierString, identifiers.stream().toList(), false);
return new SectionIdentifier(Format.NUMERICAL,
identifierString,
identifiers.stream()
.toList(),
false);
}
@@ -1,4 +1,4 @@
package com.iqser.red.service.redaction.v1.server.document.graph.nodes;
package com.iqser.red.service.redaction.v1.server.model.document.nodes;
import static java.lang.String.format;
@@ -9,17 +9,21 @@ import java.util.HashMap;
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 java.util.stream.Stream;
import com.iqser.red.service.redaction.v1.server.document.graph.TextRange;
import com.iqser.red.service.redaction.v1.server.document.graph.DocumentTree;
import com.iqser.red.service.redaction.v1.server.document.graph.entity.TextEntity;
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.AtomicTextBlock;
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock;
import com.iqser.red.service.redaction.v1.server.document.utils.RectangleTransformations;
import com.iqser.red.service.redaction.v1.server.document.utils.RedactionSearchUtility;
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.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.model.document.textblock.TextBlockCollector;
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;
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.LayoutEngine;
public interface SemanticNode {
@@ -38,7 +42,11 @@ public interface SemanticNode {
*
* @return TextBlock containing all AtomicTextBlocks that are located under this Node.
*/
TextBlock getTextBlock();
default TextBlock getTextBlock() {
return streamAllSubNodes().filter(SemanticNode::isLeaf).map(SemanticNode::getTextBlock).collect(new TextBlockCollector());
}
/**
@@ -60,6 +68,7 @@ public interface SemanticNode {
return getTextBlock().getPages();
}
/**
* Finds the first page associated with this Node.
*
@@ -67,7 +76,10 @@ public interface SemanticNode {
*/
default Page getFirstPage() {
return getTextBlock().getPages().stream().min(Comparator.comparingInt(Page::getNumber)).orElseThrow();
return getTextBlock().getPages()
.stream()
.min(Comparator.comparingInt(Page::getNumber))
.orElseThrow();
}
@@ -78,16 +90,23 @@ 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);
return getPages().stream()
.anyMatch(page -> page.getNumber() == pageNumber);
}
@@ -170,6 +189,30 @@ public interface SemanticNode {
}
/**
* Returns the next sibling node of this SemanticNode in the document tree, if any.
* If there is no next sibling node, an empty Optional is returned.
*
* @return Optional containing the next sibling node, or empty if there is none
*/
default Optional<SemanticNode> getNextSibling() {
return getDocumentTree().getNextSibling(getTreeId());
}
/**
* Returns the previous sibling node of this SemanticNode in the document tree, if any.
* If there is no previous sibling node, an empty Optional is returned.
*
* @return Optional containing the previous sibling node, or empty if there is none
*/
default Optional<SemanticNode> getPreviousSibling() {
return getDocumentTree().getPreviousSibling(getTreeId());
}
/**
* Leaf means a SemanticNode has direct access to a TextBlock, by default this is false and must be overridden.
* Currently only Sections, Images, and Tables are not leaves.
@@ -215,7 +258,9 @@ 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));
}
@@ -224,11 +269,14 @@ public interface SemanticNode {
* Ignores Entity with ignored == true or removed == true.
*
* @param types an array of strings representing the types of entities to check for
* @return true, if this SemanticNode has at least one Entity of any of the provided types
* @return true, if this SemanticNode has at least one Entity of the provided types
*/
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)));
}
@@ -243,9 +291,10 @@ public interface SemanticNode {
return getEntities().stream()
.filter(TextEntity::active)
.map(TextEntity::getType)
.map(TextEntity::type)
.collect(Collectors.toUnmodifiableSet())
.containsAll(Arrays.stream(types).toList());
.containsAll(Arrays.stream(types)
.toList());
}
@@ -258,7 +307,10 @@ 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();
}
@@ -271,7 +323,10 @@ public interface SemanticNode {
*/
default List<TextEntity> getEntitiesOfType(List<String> types) {
return getEntities().stream().filter(TextEntity::active).filter(redactionEntity -> redactionEntity.isAnyType(types)).toList();
return getEntities().stream()
.filter(TextEntity::active)
.filter(redactionEntity -> redactionEntity.isAnyType(types))
.toList();
}
@@ -284,7 +339,11 @@ public interface SemanticNode {
*/
default List<TextEntity> getEntitiesOfType(String... types) {
return getEntities().stream().filter(TextEntity::active).filter(redactionEntity -> redactionEntity.isAnyType(Arrays.stream(types).toList())).toList();
return getEntities().stream()
.filter(TextEntity::active)
.filter(redactionEntity -> redactionEntity.isAnyType(Arrays.stream(types)
.toList()))
.toList();
}
@@ -298,7 +357,8 @@ public interface SemanticNode {
TextBlock textBlock = getTextBlock();
if (!textBlock.getAtomicTextBlocks().isEmpty()) {
return getTextBlock().getAtomicTextBlocks().get(0).getNumberOnPage();
return getTextBlock().getAtomicTextBlocks()
.get(0).getNumberOnPage();
} else {
return -1;
}
@@ -328,6 +388,15 @@ public interface SemanticNode {
}
Set<LayoutEngine> getEngines();
default void addEngine(LayoutEngine engine) {
getEngines().add(engine);
}
/**
* Checks whether this SemanticNode contains all the provided Strings.
*
@@ -336,7 +405,8 @@ public interface SemanticNode {
*/
default boolean containsAllStrings(String... strings) {
return Arrays.stream(strings).allMatch(this::containsString);
return Arrays.stream(strings)
.allMatch(this::containsString);
}
@@ -348,7 +418,8 @@ public interface SemanticNode {
*/
default boolean containsAnyString(String... strings) {
return Arrays.stream(strings).anyMatch(this::containsString);
return Arrays.stream(strings)
.anyMatch(this::containsString);
}
@@ -360,7 +431,8 @@ public interface SemanticNode {
*/
default boolean containsAnyString(List<String> strings) {
return strings.stream().anyMatch(this::containsString);
return strings.stream()
.anyMatch(this::containsString);
}
@@ -384,7 +456,8 @@ public interface SemanticNode {
*/
default boolean containsAnyStringIgnoreCase(String... strings) {
return Arrays.stream(strings).anyMatch(this::containsStringIgnoreCase);
return Arrays.stream(strings)
.anyMatch(this::containsStringIgnoreCase);
}
@@ -396,7 +469,101 @@ public interface SemanticNode {
*/
default boolean containsAllStringsIgnoreCase(String... strings) {
return Arrays.stream(strings).allMatch(this::containsStringIgnoreCase);
return Arrays.stream(strings)
.allMatch(this::containsStringIgnoreCase);
}
/**
* 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));
}
@@ -426,16 +593,21 @@ 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 pageNumber the pagenumber of the rectangle
*
* @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
*/
default boolean intersectsRectangle(int x, int y, int w, int h, int pageNumber) {
return getBBox().entrySet().stream().filter(entry -> entry.getKey().getNumber() == pageNumber).map(Map.Entry::getValue).anyMatch(rect -> rect.intersects(x, y, w, h));
return getBBox().entrySet()
.stream()
.filter(entry -> entry.getKey().getNumber() == pageNumber)
.map(Map.Entry::getValue)
.anyMatch(rect -> rect.intersects(x, y, w, h));
}
@@ -452,7 +624,6 @@ public interface SemanticNode {
if (textBlock.containsTextRange(textEntity.getTextRange())) {
textEntity.setDeepestFullyContainingNode(this);
}
textEntity.addIntersectingNode(this);
streamChildren().filter(semanticNode -> semanticNode.getTextRange().intersects(textEntity.getTextRange()))
.forEach(node -> node.addThisToEntityIfIntersects(textEntity));
@@ -489,7 +660,8 @@ public interface SemanticNode {
*/
default Stream<SemanticNode> streamAllSubNodes() {
return getDocumentTree().allSubEntriesInOrder(getTreeId()).map(DocumentTree.Entry::getNode);
return getDocumentTree().allSubEntriesInOrder(getTreeId())
.map(DocumentTree.Entry::getNode);
}
@@ -500,7 +672,9 @@ public interface SemanticNode {
*/
default Stream<SemanticNode> streamAllSubNodesOfType(NodeType nodeType) {
return getDocumentTree().allSubEntriesInOrder(getTreeId()).filter(entry -> entry.getType().equals(nodeType)).map(DocumentTree.Entry::getNode);
return getDocumentTree().allSubEntriesInOrder(getTreeId())
.filter(entry -> entry.getType().equals(nodeType))
.map(DocumentTree.Entry::getNode);
}
@@ -526,6 +700,28 @@ 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
@@ -534,12 +730,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();
}
@@ -562,28 +757,55 @@ 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, List<AtomicTextBlock>> atomicTextBlockPerPage = getTextBlock().getAtomicTextBlocks().stream().collect(Collectors.groupingBy(AtomicTextBlock::getPage));
atomicTextBlockPerPage.forEach((page, atbs) -> bBoxPerPage.put(page, RectangleTransformations.atomicTextBlockBBox(atbs)));
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));
}
}
@@ -1,19 +1,23 @@
package com.iqser.red.service.redaction.v1.server.document.graph.nodes;
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.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import com.iqser.red.service.redaction.v1.server.document.graph.DocumentTree;
import com.iqser.red.service.redaction.v1.server.document.graph.entity.TextEntity;
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock;
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlockCollector;
import com.iqser.red.service.redaction.v1.server.model.document.DocumentTree;
import com.iqser.red.service.redaction.v1.server.model.document.entity.TextEntity;
import com.iqser.red.service.redaction.v1.server.model.document.textblock.TextBlock;
import com.iqser.red.service.redaction.v1.server.model.document.textblock.TextBlockCollector;
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.LayoutEngine;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
@@ -26,8 +30,12 @@ import lombok.experimental.FieldDefaults;
@Builder
@AllArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Table implements SemanticNode {
@Builder.Default
Set<LayoutEngine> engines = new HashSet<>(Set.of(LayoutEngine.ALGORITHM));
@EqualsAndHashCode.Include
List<Integer> treeId;
DocumentTree documentTree;
@@ -37,9 +45,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.
@@ -49,8 +68,7 @@ public class Table implements SemanticNode {
*/
public Stream<TextEntity> streamEntitiesWhereRowContainsStringsIgnoreCase(List<String> strings) {
return IntStream.range(0, numberOfRows)
.boxed()
return IntStream.range(0, numberOfRows).boxed()
.filter(row -> rowContainsStringsIgnoreCase(row, strings))
.flatMap(this::streamRow)
.map(TableCell::getEntities)
@@ -67,8 +85,11 @@ public class Table implements SemanticNode {
*/
public boolean rowContainsStringsIgnoreCase(Integer row, List<String> strings) {
String rowText = streamRow(row).map(TableCell::getTextBlock).collect(new TextBlockCollector()).getSearchText().toLowerCase(Locale.ROOT);
return strings.stream().map(String::toLowerCase).allMatch(rowText::contains);
String rowText = streamRow(row).map(TableCell::getTextBlock)
.collect(new TextBlockCollector()).getSearchText().toLowerCase(Locale.ROOT);
return strings.stream()
.map(String::toLowerCase)
.allMatch(rowText::contains);
}
@@ -81,9 +102,13 @@ public class Table implements SemanticNode {
*/
public Stream<TextEntity> streamEntitiesWhereRowHasHeaderAndValue(String header, String value) {
List<Integer> vertebrateStudyCols = streamHeaders().filter(headerNode -> headerNode.containsString(header)).map(TableCell::getCol).toList();
List<Integer> vertebrateStudyCols = streamHeaders().filter(headerNode -> headerNode.containsString(header))
.map(TableCell::getCol)
.toList();
return streamTableCells().filter(tableCellNode -> vertebrateStudyCols.stream()
.anyMatch(vertebrateStudyCol -> getCell(tableCellNode.getRow(), vertebrateStudyCol).containsString(value))).map(TableCell::getEntities).flatMap(Collection::stream);
.anyMatch(vertebrateStudyCol -> getCell(tableCellNode.getRow(), vertebrateStudyCol).containsString(value)))
.map(TableCell::getEntities)
.flatMap(Collection::stream);
}
@@ -96,9 +121,13 @@ public class Table implements SemanticNode {
*/
public Stream<TextEntity> streamEntitiesWhereRowHasHeaderAndAnyValue(String header, List<String> values) {
List<Integer> colsWithHeader = streamHeaders().filter(headerNode -> headerNode.containsString(header)).map(TableCell::getCol).toList();
List<Integer> colsWithHeader = streamHeaders().filter(headerNode -> headerNode.containsString(header))
.map(TableCell::getCol)
.toList();
return streamTableCells().filter(tableCellNode -> colsWithHeader.stream()
.anyMatch(colWithHeader -> getCell(tableCellNode.getRow(), colWithHeader).containsAnyString(values))).map(TableCell::getEntities).flatMap(Collection::stream);
.anyMatch(colWithHeader -> getCell(tableCellNode.getRow(), colWithHeader).containsAnyString(values)))
.map(TableCell::getEntities)
.flatMap(Collection::stream);
}
@@ -111,16 +140,33 @@ public class Table implements SemanticNode {
*/
public Stream<TextEntity> streamEntitiesWhereRowContainsEntitiesOfType(List<String> types) {
List<Integer> rowsWithEntityOfType = getEntities().stream()
.filter(TextEntity::active)
.filter(redactionEntity -> types.stream().anyMatch(type -> type.equals(redactionEntity.getType())))
.map(TextEntity::getIntersectingNodes)
.filter(node -> node instanceof TableCell)
.map(node -> (TableCell) node)
.map(TableCell::getRow)
.toList();
return IntStream.range(0, numberOfRows).boxed()
.filter(rowNumber -> streamTextEntitiesInRow(rowNumber).map(TextEntity::type)
.anyMatch(types::contains))
.flatMap(this::streamRow)
.map(TableCell::getEntities)
.flatMap(Collection::stream);
}
return rowsWithEntityOfType.stream().flatMap(this::streamRow).map(TableCell::getEntities).flatMap(Collection::stream);
/**
* Streams all entities in this table, that appear in a row, which contains at least one entity of each of the provided types.
* Ignores Entity with ignored == true or removed == true.
*
* @param types type strings to check whether a row contains an entity like them
* @return Stream of all entities in this table, that appear in a row, which contains at least one entity of each of the provided types.
*/
public Stream<TextEntity> streamEntitiesWhereRowContainsEntitiesOfEachType(List<String> types) {
return IntStream.range(0, numberOfRows).boxed()
.filter(rowNumber -> {
Set<String> entityTypes = streamTextEntitiesInRow(rowNumber).map(TextEntity::type)
.collect(Collectors.toSet());
return entityTypes.containsAll(types);
})
.flatMap(this::streamRow)
.map(TableCell::getEntities)
.flatMap(Collection::stream);
}
@@ -133,18 +179,43 @@ public class Table implements SemanticNode {
*/
public Stream<TextEntity> streamEntitiesWhereRowContainsNoEntitiesOfType(List<String> types) {
return IntStream.range(0, numberOfRows)
.boxed()
.filter(rowNumber -> streamRow(rowNumber).map(TableCell::getEntities)
.flatMap(Collection::stream)
.filter(TextEntity::active)
.noneMatch(entity -> types.contains(entity.getType())))
return IntStream.range(0, numberOfRows).boxed()
.filter(rowNumber -> streamTextEntitiesInRow(rowNumber).map(TextEntity::type)
.noneMatch(types::contains))
.flatMap(this::streamRow)
.map(TableCell::getEntities)
.flatMap(Collection::stream);
}
/**
* Streams all Entities in the given row.
*
* @param rowNumber the row number to look for
* @return stream of TextEntities occurring in row
*/
public Stream<TextEntity> streamTextEntitiesInRow(int rowNumber) {
return streamRow(rowNumber).map(TableCell::getEntities)
.flatMap(Collection::stream)
.filter(TextEntity::active);
}
/**
* Streams all Entities in the given col.
*
* @param colNumber the column number to look for
* @return stream of TextEntities occurring in row
*/
public Stream<TextEntity> streamTextEntitiesInCol(int colNumber) {
return streamCol(colNumber).map(TableCell::getEntities)
.flatMap(Collection::stream)
.filter(TextEntity::active);
}
/**
* Returns a TableCell at the provided row and column location.
*
@@ -158,7 +229,8 @@ public class Table implements SemanticNode {
throw new IllegalArgumentException(format("row %d, col %d is out of bounds for number of rows of %d and number of cols %d", row, col, numberOfRows, numberOfCols));
}
int idx = row * numberOfCols + col;
return (TableCell) documentTree.getEntryById(treeId).getChildren().get(idx).getNode();
return (TableCell) documentTree.getEntryById(treeId).getChildren()
.get(idx).getNode();
}
@@ -173,6 +245,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.hasEntitiesOfType(type));
}
/**
* Streams all TableCells in this Table which have the provided header row-wise.
*
@@ -195,7 +279,8 @@ public class Table implements SemanticNode {
*/
public Stream<TableCell> streamCol(int col) {
return IntStream.range(0, numberOfRows).boxed().map(row -> getCell(row, col));
return IntStream.range(0, numberOfRows).boxed()
.map(row -> getCell(row, col));
}
@@ -207,7 +292,8 @@ public class Table implements SemanticNode {
*/
public Stream<TableCell> streamRow(int row) {
return IntStream.range(0, numberOfCols).boxed().map(col -> getCell(row, col));
return IntStream.range(0, numberOfCols).boxed()
.map(col -> getCell(row, col));
}
@@ -231,7 +317,8 @@ public class Table implements SemanticNode {
*/
public Stream<TableCell> streamHeadersForCell(int row, int col) {
return Stream.concat(streamRow(row), streamCol(col)).filter(TableCell::isHeader);
return Stream.concat(streamRow(row), streamCol(col))
.filter(TableCell::isHeader);
}
@@ -255,7 +342,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 +380,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.
*/
@@ -317,7 +408,7 @@ public class Table implements SemanticNode {
public TextBlock getTextBlock() {
if (textBlock == null) {
textBlock = streamAllSubNodes().filter(SemanticNode::isLeaf).map(SemanticNode::getLeafTextBlock).collect(new TextBlockCollector());
textBlock = SemanticNode.super.getTextBlock();
}
return textBlock;
}
@@ -1,4 +1,4 @@
package com.iqser.red.service.redaction.v1.server.document.graph.nodes;
package com.iqser.red.service.redaction.v1.server.model.document.nodes;
import java.awt.geom.Rectangle2D;
import java.util.HashMap;
@@ -7,10 +7,11 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import com.iqser.red.service.redaction.v1.server.document.graph.entity.TextEntity;
import com.iqser.red.service.redaction.v1.server.document.graph.DocumentTree;
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock;
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlockCollector;
import com.iqser.red.service.redaction.v1.server.model.document.entity.TextEntity;
import com.iqser.red.service.redaction.v1.server.model.document.DocumentTree;
import com.iqser.red.service.redaction.v1.server.model.document.textblock.TextBlock;
import com.iqser.red.service.redaction.v1.server.model.document.textblock.TextBlockCollector;
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.LayoutEngine;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
@@ -23,8 +24,13 @@ import lombok.experimental.FieldDefaults;
@Builder
@AllArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class TableCell implements GenericSemanticNode {
@Builder.Default
Set<LayoutEngine> engines = new HashSet<>(Set.of(LayoutEngine.ALGORITHM));
@EqualsAndHashCode.Include
List<Integer> treeId;
int row;
int col;
@@ -36,11 +42,9 @@ public class TableCell implements GenericSemanticNode {
TextBlock textBlock;
@EqualsAndHashCode.Exclude
DocumentTree documentTree;
@Builder.Default
@EqualsAndHashCode.Exclude
Set<TextEntity> entities = new HashSet<>();
@@ -75,7 +79,9 @@ public class TableCell implements GenericSemanticNode {
}
if (textBlock == null) {
textBlock = streamAllSubNodes().filter(SemanticNode::isLeaf).map(SemanticNode::getLeafTextBlock).collect(new TextBlockCollector());
textBlock = streamAllSubNodes().filter(SemanticNode::isLeaf)
.map(SemanticNode::getLeafTextBlock)
.collect(new TextBlockCollector());
}
return textBlock;
}
@@ -1,21 +1,24 @@
package com.iqser.red.service.redaction.v1.server.document.graph.textblock;
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;
import com.iqser.red.service.redaction.v1.server.document.graph.TextRange;
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.Page;
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.SemanticNode;
import com.iqser.red.service.redaction.v1.server.document.utils.RectangleTransformations;
import com.iqser.red.service.redaction.v1.server.model.document.TextRange;
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.utils.RectangleTransformations;
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.DocumentPositionData;
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.DocumentTextData;
@@ -40,6 +43,7 @@ public class AtomicTextBlock implements TextBlock {
//string coordinates
TextRange textRange;
String searchText;
List<String> words;
List<Integer> lineBreaks;
//position coordinates
@@ -57,6 +61,7 @@ public class AtomicTextBlock implements TextBlock {
return lineBreaks.size() + 1;
}
public static AtomicTextBlock empty(Long textBlockIdx, int stringOffset, Page page, int numberOnPage, SemanticNode parent) {
return AtomicTextBlock.builder()
@@ -73,10 +78,7 @@ public class AtomicTextBlock implements TextBlock {
}
public static AtomicTextBlock fromAtomicTextBlockData(DocumentTextData atomicTextBlockData,
DocumentPositionData atomicPositionBlockData,
SemanticNode parent,
Page page) {
public static AtomicTextBlock fromAtomicTextBlockData(DocumentTextData atomicTextBlockData, DocumentPositionData atomicPositionBlockData, SemanticNode parent, Page page) {
return AtomicTextBlock.builder()
.id(atomicTextBlockData.getId())
@@ -84,8 +86,10 @@ public class AtomicTextBlock implements TextBlock {
.page(page)
.textRange(new TextRange(atomicTextBlockData.getStart(), atomicTextBlockData.getEnd()))
.searchText(atomicTextBlockData.getSearchText())
.lineBreaks(Arrays.stream(atomicTextBlockData.getLineBreaks()).boxed().toList())
.stringIdxToPositionIdx(Arrays.stream(atomicPositionBlockData.getStringIdxToPositionIdx()).boxed().toList())
.lineBreaks(Arrays.stream(atomicTextBlockData.getLineBreaks()).boxed()
.toList())
.stringIdxToPositionIdx(Arrays.stream(atomicPositionBlockData.getStringIdxToPositionIdx()).boxed()
.toList())
.positions(toRectangle2DList(atomicPositionBlockData.getPositions()))
.parent(parent)
.build();
@@ -94,7 +98,9 @@ public class AtomicTextBlock implements TextBlock {
private static List<Rectangle2D> toRectangle2DList(float[][] positions) {
return Arrays.stream(positions).map(floatArr -> (Rectangle2D) new Rectangle2D.Float(floatArr[0], floatArr[1], floatArr[2], floatArr[3])).toList();
return Arrays.stream(positions)
.map(floatArr -> (Rectangle2D) new Rectangle2D.Float(floatArr[0], floatArr[1], floatArr[2], floatArr[3]))
.toList();
}
@@ -115,6 +121,21 @@ public class AtomicTextBlock implements TextBlock {
}
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() {
@@ -126,9 +147,9 @@ public class AtomicTextBlock implements TextBlock {
public int getNextLinebreak(int fromIndex) {
return lineBreaks.stream()//
.filter(linebreak -> linebreak > fromIndex - textRange.start()) //
.findFirst() //
.orElse(searchText.length()) + textRange.start();
.filter(linebreak -> linebreak > fromIndex - textRange.start()) //
.findFirst() //
.orElse(searchText.length()) + textRange.start();
}
@@ -136,9 +157,9 @@ public class AtomicTextBlock implements TextBlock {
public int getPreviousLinebreak(int fromIndex) {
return lineBreaks.stream()//
.filter(linebreak -> linebreak <= fromIndex - textRange.start())//
.reduce((a, b) -> b)//
.orElse(0) + textRange.start();
.filter(linebreak -> linebreak <= fromIndex - textRange.start())//
.reduce((a, b) -> b)//
.orElse(0) + textRange.start();
}
@@ -191,7 +212,10 @@ public class AtomicTextBlock implements TextBlock {
return "";
}
Set<Integer> lbInBoundary = lineBreaks.stream().map(i -> i + textRange.start()).filter(textRange::contains).collect(Collectors.toSet());
Set<Integer> lbInBoundary = lineBreaks.stream()
.map(i -> i + textRange.start())
.filter(textRange::contains)
.collect(Collectors.toSet());
if (textRange.end() == getTextRange().end()) {
lbInBoundary.add(getTextRange().end());
}
@@ -217,7 +241,10 @@ public class AtomicTextBlock implements TextBlock {
private List<Integer> getAllLineBreaksInBoundary(TextRange textRange) {
return getLineBreaks().stream().map(linebreak -> linebreak + this.textRange.start()).filter(textRange::contains).toList();
return getLineBreaks().stream()
.map(linebreak -> linebreak + this.textRange.start())
.filter(textRange::contains)
.toList();
}
@@ -1,4 +1,4 @@
package com.iqser.red.service.redaction.v1.server.document.graph.textblock;
package com.iqser.red.service.redaction.v1.server.model.document.textblock;
import static java.lang.String.format;
@@ -7,11 +7,12 @@ 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;
import com.iqser.red.service.redaction.v1.server.document.graph.TextRange;
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.Page;
import com.iqser.red.service.redaction.v1.server.model.document.TextRange;
import com.iqser.red.service.redaction.v1.server.model.document.nodes.Page;
import lombok.AccessLevel;
import lombok.Data;
@@ -43,7 +44,8 @@ public class ConcatenatedTextBlock implements TextBlock {
this.atomicTextBlocks.add(firstTextBlock);
textRange = new TextRange(firstTextBlock.getTextRange().start(), firstTextBlock.getTextRange().end());
atomicTextBlocks.subList(1, atomicTextBlocks.size()).forEach(this::concat);
atomicTextBlocks.subList(1, atomicTextBlocks.size())
.forEach(this::concat);
}
@@ -64,13 +66,25 @@ public class ConcatenatedTextBlock implements TextBlock {
private AtomicTextBlock getAtomicTextBlockByStringIndex(int stringIdx) {
return atomicTextBlocks.stream().filter(textBlock -> textBlock.getTextRange().contains(stringIdx)).findAny().orElseThrow(IndexOutOfBoundsException::new);
return atomicTextBlocks.stream()
.filter(textBlock -> textBlock.getTextRange().contains(stringIdx))
.findAny()
.orElseThrow(IndexOutOfBoundsException::new);
}
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,10 +100,21 @@ public class ConcatenatedTextBlock implements TextBlock {
}
@Override
public List<String> getWords() {
return atomicTextBlocks.stream()
.map(AtomicTextBlock::getWords)
.flatMap(Collection::stream)
.toList();
}
@Override
public int numberOfLines() {
return atomicTextBlocks.stream().mapToInt(AtomicTextBlock::numberOfLines).sum();
return atomicTextBlocks.stream()
.mapToInt(AtomicTextBlock::numberOfLines).sum();
}
@@ -110,17 +135,20 @@ public class ConcatenatedTextBlock implements TextBlock {
@Override
public List<Integer> getLineBreaks() {
return getAtomicTextBlocks().stream().flatMap(atomicTextBlock -> atomicTextBlock.getLineBreaks().stream()).toList();
return getAtomicTextBlocks().stream()
.flatMap(atomicTextBlock -> atomicTextBlock.getLineBreaks()
.stream())
.toList();
}
@Override
public Rectangle2D getPosition(int stringIdx) {
return getAtomicTextBlockByStringIndex(stringIdx).getPosition(stringIdx);
}
public TextRange getLineTextRange(int lineNumber) {
if (atomicTextBlocks.size() == 1) {
@@ -136,10 +164,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()) {
@@ -185,7 +213,8 @@ public class ConcatenatedTextBlock implements TextBlock {
AtomicTextBlock lastTextBlock = textBlocks.get(textBlocks.size() - 1);
rectanglesPerLinePerPage = mergeEntityPositionsWithSamePageNode(rectanglesPerLinePerPage,
lastTextBlock.getPositionsPerPage(new TextRange(lastTextBlock.getTextRange().start(), stringTextRange.end())));
lastTextBlock.getPositionsPerPage(new TextRange(lastTextBlock.getTextRange().start(),
stringTextRange.end())));
return rectanglesPerLinePerPage;
}
@@ -222,7 +251,10 @@ public class ConcatenatedTextBlock implements TextBlock {
private Map<Page, List<Rectangle2D>> mergeEntityPositionsWithSamePageNode(Map<Page, List<Rectangle2D>> map1, Map<Page, List<Rectangle2D>> map2) {
Map<Page, List<Rectangle2D>> mergedMap = new HashMap<>(map1);
map2.forEach((pageNode, rectangles) -> mergedMap.merge(pageNode, rectangles, (l1, l2) -> Stream.concat(l1.stream(), l2.stream()).toList()));
map2.forEach((pageNode, rectangles) -> mergedMap.merge(pageNode,
rectangles,
(l1, l2) -> Stream.concat(l1.stream(), l2.stream())
.toList()));
return mergedMap;
}
@@ -1,4 +1,4 @@
package com.iqser.red.service.redaction.v1.server.document.graph.textblock;
package com.iqser.red.service.redaction.v1.server.model.document.textblock;
import static java.lang.String.format;
@@ -10,15 +10,18 @@ import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import com.iqser.red.service.redaction.v1.server.document.graph.TextRange;
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.Page;
import com.iqser.red.service.redaction.v1.server.document.utils.RectangleTransformations;
import com.iqser.red.service.redaction.v1.server.model.document.TextRange;
import com.iqser.red.service.redaction.v1.server.model.document.nodes.Page;
import com.iqser.red.service.redaction.v1.server.utils.RectangleTransformations;
public interface TextBlock extends CharSequence {
String getSearchText();
List<String> getWords();
List<AtomicTextBlock> getAtomicTextBlocks();
@@ -34,7 +37,6 @@ public interface TextBlock extends CharSequence {
TextRange getLineTextRange(int lineNumber);
List<Integer> getLineBreaks();
@@ -70,6 +72,7 @@ public interface TextBlock extends CharSequence {
return RectangleTransformations.rectangle2DBBox(getLinePositions(lineNumber));
}
default String searchTextWithLineBreaks() {
return subSequenceWithLineBreaks(getTextRange());
@@ -84,7 +87,9 @@ public interface TextBlock extends CharSequence {
default Set<Page> getPages() {
return getAtomicTextBlocks().stream().map(AtomicTextBlock::getPage).collect(Collectors.toUnmodifiableSet());
return getAtomicTextBlocks().stream()
.map(AtomicTextBlock::getPage)
.collect(Collectors.toUnmodifiableSet());
}
@@ -1,4 +1,4 @@
package com.iqser.red.service.redaction.v1.server.document.graph.textblock;
package com.iqser.red.service.redaction.v1.server.model.document.textblock;
import java.util.Set;
import java.util.function.BiConsumer;
@@ -0,0 +1,15 @@
package com.iqser.red.service.redaction.v1.server.model.drools;
import lombok.AccessLevel;
import lombok.Data;
import lombok.experimental.FieldDefaults;
@Data
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public class BasicQuery {
String name;
int line;
String code;
}
@@ -0,0 +1,42 @@
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;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.experimental.FieldDefaults;
@Getter
@AllArgsConstructor
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public class BasicRule {
@EqualsAndHashCode.Include
RuleIdentifier identifier;
String name;
String code;
String agendaGroup;
int line;
public static BasicRule fromRuleDescr(RuleDescr rule, String rulesString) {
RuleIdentifier identifier = RuleIdentifier.fromName(rule.getName());
String nameWithoutIdentifier = rule.getName().replace(identifier + ":", "");
String code = rulesString.substring(rule.getStartCharacter(), rule.getEndCharacter());
String agendaGroup = rule.getAttributes().getOrDefault("agenda-group", new AttributeDescr("agenda-group", "DEFAULT")).getValue();
return new BasicRule(identifier, nameWithoutIdentifier, code, agendaGroup, rule.getLine());
}
@Override
public String toString() {
return "BasicRule[identifier=" + identifier + ", name=" + name + ", code=" + code + ']';
}
}
@@ -0,0 +1,16 @@
package com.iqser.red.service.redaction.v1.server.model.drools;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
public record RuleClass(RuleType ruleType, List<RuleUnit> ruleUnits) {
public Optional<RuleUnit> findRuleUnitByInteger(Integer unit) {
return ruleUnits.stream()
.filter(ruleUnit -> Objects.equals(ruleUnit.unit(), unit))
.findFirst();
}
}
@@ -0,0 +1,97 @@
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;
import com.iqser.red.service.redaction.v1.model.DroolsValidation;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.FieldDefaults;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
public final class RuleFileBluePrint {
String imports;
int importLine;
String globals;
int globalsLine;
List<BasicQuery> queries;
List<RuleClass> ruleClasses;
DroolsValidation droolsValidation;
public Optional<RuleClass> findRuleClassByType(RuleType ruleType) {
return ruleClasses.stream()
.filter(ruleClass -> Objects.equals(ruleClass.ruleType(), ruleType))
.findFirst();
}
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())) {
return findRuleClassByType(ruleIdentifier.type()).map(RuleClass::ruleUnits)
.orElse(Collections.emptyList())
.stream()
.flatMap(ruleUnit -> ruleUnit.rules()
.stream()
.filter(rule -> rule.getIdentifier().matches(ruleIdentifier)))
.toList();
}
return findRuleClassByType(ruleIdentifier.type()).flatMap(ruleClass -> ruleClass.findRuleUnitByInteger(ruleIdentifier.unit()))
.map(ruleUnit -> ruleUnit.rules()
.stream()
.filter(rule -> rule.getIdentifier().matches(ruleIdentifier)))
.orElse(Stream.empty())
.toList();
}
public List<RuleIdentifier> getAllRuleIdentifiers() {
return streamAllRules().map(BasicRule::getIdentifier)
.collect(Collectors.toList());
}
public Stream<BasicRule> streamAllRules() {
return getRuleClasses().stream()
.map(RuleClass::ruleUnits)
.flatMap(Collection::stream)
.map(RuleUnit::rules)
.flatMap(Collection::stream);
}
@Override
public String toString() {
return "RuleFileBluePrint[imports=" + imports + ", globals=" + globals + ", queries=" + queries + ", ruleClasses=" + ruleClasses + ']';
}
}
@@ -0,0 +1,81 @@
package com.iqser.red.service.redaction.v1.server.model.drools;
import java.util.Objects;
import lombok.NonNull;
public record RuleIdentifier(@NonNull RuleType type, Integer unit, Integer id) {
public static RuleIdentifier fromName(String name) {
String identifier = name.split(":")[0];
return fromString(identifier);
}
public static RuleIdentifier fromString(String identifier) {
String[] values = identifier.split("\\.");
if (values.length != 3) {
throw new IllegalArgumentException(String.format("Identifier %s must conform to the pattern \"[a-zA-Z0-9]+.\\d+.\\d+\"", identifier));
}
RuleType type = RuleType.fromString(values[0]);
Integer group = Integer.parseInt(values[1]);
Integer id = Integer.parseInt(values[2]);
return new RuleIdentifier(type, group, id);
}
public static RuleIdentifier empty() {
return new RuleIdentifier(new RuleType(""), null, null);
}
/**
* This is used to filter rules, if the field Integer unit or Integer id is null, the field will match any other RuleIdentifier.
* Therefore, to compare RuleIdentifiers one should always use the function matches.
*
* @param ruleIdentifier RuleIdentifier to check if this one matches it.
* @return true, if all fields match. If a field is null, it matches any field.
*/
public boolean matches(RuleIdentifier ruleIdentifier) {
return ruleIdentifier.type().equals(this.type()) && //
(Objects.isNull(ruleIdentifier.unit()) || Objects.isNull(this.unit()) || Objects.equals(this.unit(), ruleIdentifier.unit())) && //
(Objects.isNull(ruleIdentifier.id()) || Objects.isNull(this.id()) || Objects.equals(this.id(), ruleIdentifier.id()));
}
@Override
public String toString() {
if (type.name().isBlank() || type.name().isEmpty()) {
return "";
}
StringBuilder sb = new StringBuilder();
sb.append(type().name());
if (Objects.nonNull(unit())) {
sb.append(".");
sb.append(unit());
} else {
sb.append(".*");
}
if (Objects.nonNull(id())) {
sb.append(".");
sb.append(id());
} else {
sb.append(".*");
}
return sb.toString();
}
public String toRuleUnitString() {
return String.format("Rule unit: %s.%d", type.name(), unit);
}
}
@@ -0,0 +1,18 @@
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]+$");
public static RuleType fromString(String value) {
if (!alphaNumeric.matcher(value).matches()) {
throw new IllegalArgumentException(String.format("The rule type %s must only contain numbers and letters!", value));
}
return new RuleType(value);
}
}
@@ -0,0 +1,7 @@
package com.iqser.red.service.redaction.v1.server.model.drools;
import java.util.List;
public record RuleUnit(Integer unit, List<BasicRule> rules) {
}
@@ -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;
@@ -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() {
@@ -55,6 +71,17 @@ public class MessagingConfiguration {
}
@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,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;
@@ -1,28 +1,35 @@
package com.iqser.red.service.redaction.v1.server.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;
import java.io.IOException;
import java.time.OffsetDateTime;
import java.time.temporal.ChronoUnit;
import org.springframework.amqp.AmqpRejectAndDontRequeueException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.ObjectMapper;
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.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.exception.DroolsTimeoutException;
import com.iqser.red.service.redaction.v1.server.redaction.service.AnalyzeService;
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;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.AmqpRejectAndDontRequeueException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.time.OffsetDateTime;
import java.time.temporal.ChronoUnit;
import static com.iqser.red.service.redaction.v1.server.queue.MessagingConfiguration.*;
import static java.lang.String.format;
@Slf4j
@Service
@@ -33,6 +40,7 @@ public class RedactionMessageReceiver {
private final AnalyzeService analyzeService;
private final FileStatusProcessingUpdateClient fileStatusProcessingUpdateClient;
private final RulesClient rulesClient;
private final UnprocessedChangesService unprocessedChangesService;
@SneakyThrows
@@ -43,14 +51,14 @@ public class RedactionMessageReceiver {
// This prevents from endless retries oom errors.
if (message.getMessageProperties().isRedelivered()) {
var errorMessage = format("Error during last processing of request with dossierId: %s and fileId: %s, do not retry.",
analyzeRequest.getDossierId(),
analyzeRequest.getFileId());
analyzeRequest.getDossierId(),
analyzeRequest.getFileId());
fileStatusProcessingUpdateClient.analysisFailed(analyzeRequest.getDossierId(),
analyzeRequest.getFileId(),
new FileErrorInfo(errorMessage,
priority ? REDACTION_PRIORITY_QUEUE : REDACTION_QUEUE,
"redaction-service",
OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS)));
analyzeRequest.getFileId(),
new FileErrorInfo(errorMessage,
priority ? REDACTION_PRIORITY_QUEUE : REDACTION_QUEUE,
"redaction-service",
OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS)));
throw new AmqpRejectAndDontRequeueException(errorMessage);
}
@@ -64,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()) {
@@ -75,9 +84,9 @@ public class RedactionMessageReceiver {
log.debug(analyzeRequest.getManualRedactions().toString());
result = analyzeService.analyze(analyzeRequest);
log.info("Successfully analyzed dossier {} file {} took: {} s",
analyzeRequest.getDossierId(),
analyzeRequest.getFileId(),
format("%.2f", result.getDuration() / 1000.0));
analyzeRequest.getDossierId(),
analyzeRequest.getFileId(),
format("%.2f", result.getDuration() / 1000.0));
log.info("----------------------------------------------------------------------------------");
break;
@@ -87,37 +96,53 @@ public class RedactionMessageReceiver {
log.debug(analyzeRequest.getManualRedactions().toString());
result = analyzeService.reanalyze(analyzeRequest);
log.info("Successfully reanalyzed dossier {} file {} took: {} s",
analyzeRequest.getDossierId(),
analyzeRequest.getFileId(),
format("%.2f", result.getDuration() / 1000.0));
analyzeRequest.getDossierId(),
analyzeRequest.getFileId(),
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);
} catch (Exception e) {
if (e instanceof DroolsTimeoutException && !((DroolsTimeoutException) e).isReported()) {
rulesClient.setRulesTimeoutDetected(analyzeRequest.getDossierTemplateId());
if (shouldRespond) {
result.setMessageType(analyzeRequest.getMessageType());
fileStatusProcessingUpdateClient.analysisSuccessful(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), result);
}
log.warn("Failed to process analyze request: {}", analyzeRequest, e);
var timestamp = OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS);
fileStatusProcessingUpdateClient.analysisFailed(analyzeRequest.getDossierId(),
analyzeRequest.getFileId(),
new FileErrorInfo(e.getMessage(), priority ? REDACTION_PRIORITY_QUEUE : REDACTION_QUEUE, "redaction-service", timestamp));
} catch (DroolsTimeoutException droolsTimeoutException) {
if (!droolsTimeoutException.isReported()) {
rulesClient.setRulesTimeoutDetected(analyzeRequest.getDossierTemplateId(), droolsTimeoutException.getRuleFileType());
}
sendAnalysisFailed(analyzeRequest, priority, droolsTimeoutException);
} catch (Exception e) {
sendAnalysisFailed(analyzeRequest, priority, e);
}
}
private void sendAnalysisFailed(AnalyzeRequest analyzeRequest, boolean priority, Exception e) {
log.warn("Failed to process analyze request: {}", analyzeRequest, e);
var timestamp = OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS);
fileStatusProcessingUpdateClient.analysisFailed(analyzeRequest.getDossierId(),
analyzeRequest.getFileId(),
new FileErrorInfo(e.getMessage(), priority ? REDACTION_PRIORITY_QUEUE : REDACTION_QUEUE, "redaction-service", timestamp));
}
@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);
@@ -126,8 +151,8 @@ public class RedactionMessageReceiver {
timestamp = timestamp != null ? timestamp : OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS);
log.info("Failed to process analyze request, errorCause: {}, timestamp: {}", errorCause, timestamp);
fileStatusProcessingUpdateClient.analysisFailed(analyzeRequest.getDossierId(),
analyzeRequest.getFileId(),
new FileErrorInfo(errorCause, REDACTION_DQL, "redaction-service", timestamp));
analyzeRequest.getFileId(),
new FileErrorInfo(errorCause, REDACTION_DQL, "redaction-service", timestamp));
}
}
@@ -1,240 +0,0 @@
package com.iqser.red.service.redaction.v1.server.redaction.adapter;
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.document.graph.TextRange;
import com.iqser.red.service.redaction.v1.server.document.graph.entity.EntityType;
import com.iqser.red.service.redaction.v1.server.document.graph.entity.TextEntity;
import com.iqser.red.service.redaction.v1.server.document.graph.entity.PositionOnPage;
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.Page;
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.SemanticNode;
import com.iqser.red.service.redaction.v1.server.document.services.EntityCreationService;
import com.iqser.red.service.redaction.v1.server.document.services.EntityEnrichmentService;
import com.iqser.red.service.redaction.v1.server.redaction.model.ManualEntity;
import com.iqser.red.service.redaction.v1.server.redaction.model.RectangleWithPage;
import com.iqser.red.service.redaction.v1.server.redaction.model.dictionary.SearchImplementation;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
public class CustomEntityCreationAdapter {
private static final double MATCH_THRESHOLD = 5; // sum of distances for each corner
private final EntityCreationService entityCreationService;
@Autowired
public CustomEntityCreationAdapter(EntityEnrichmentService entityEnrichmentService) {
entityCreationService = new EntityCreationService(entityEnrichmentService);
}
public List<ManualEntity> toRedactionEntity(RedactionLog redactionLog, SemanticNode node) {
List<ManualEntity> manualEntities = redactionLog.getRedactionLogEntry().stream().map(ManualEntity::fromRedactionLogEntry).toList();
return toRedactionEntity(manualEntities, node);
}
public List<ManualEntity> createRedactionEntitiesIfFoundAndReturnNotFoundEntries(Set<ManualRedactionEntry> manualRedactionEntries, SemanticNode node) {
List<ManualEntity> manualEntities = manualRedactionEntries.stream()
.filter(manualRedactionEntry -> !(manualRedactionEntry.isAddToDictionary() || manualRedactionEntry.isAddToDossierDictionary()))
.map(ManualEntity::fromManualRedactionEntry)
.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.forceByBoundary(closestTextRange, manualEntity.getType(), manualEntity.getEntityType(), node);
if (manualEntity.isApplied()) {
correctEntity.force(manualEntity.getRuleIdentifier(), manualEntity.getReason(), manualEntity.getLegalBasis());
} else {
correctEntity.skip(manualEntity.getRuleIdentifier(), manualEntity.getReason());
}
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.ROOT));
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.forceByBoundary(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);
}
}
@@ -1,7 +0,0 @@
package com.iqser.red.service.redaction.v1.server.redaction.model;
import org.kie.api.runtime.KieContainer;
public record KieWrapper( KieContainer container,long rulesVersion) {
}
@@ -1,91 +0,0 @@
package com.iqser.red.service.redaction.v1.server.redaction.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.document.graph.entity.Entity;
import com.iqser.red.service.redaction.v1.server.document.graph.entity.EntityType;
import com.iqser.red.service.redaction.v1.server.document.graph.entity.ManualChangeOverwrite;
import com.iqser.red.service.redaction.v1.server.document.graph.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 Entity {
// 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(redactionLogEntry.isRecommendation() ? EntityType.RECOMMENDATION : EntityType.ENTITY)
.applied(redactionLogEntry.isRedacted())
.isDictionaryEntry(redactionLogEntry.isDictionaryEntry())
.isDossierDictionaryEntry(redactionLogEntry.isDossierDictionaryEntry())
.rectangle(redactionLogEntry.isRectangle())
.build();
}
public static ManualEntity fromManualRedactionEntry(ManualRedactionEntry manualRedactionEntry) {
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(EntityType.ENTITY)
.applied(true)
.isDictionaryEntry(false)
.isDossierDictionaryEntry(false)
.rectangle(manualRedactionEntry.isRectangle())
.manualOverwrite(manualChangeOverwrite)
.build();
}
}

Some files were not shown because too many files have changed in this diff Show More