From 8abd6e8d695fa64d6cccac76b779e5f8e99d2fd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kov=C3=A1=C4=8Dik?= Date: Thu, 9 Jan 2025 14:53:31 +0100 Subject: [PATCH 01/92] [NAE-2031] Dashboard bug fix - add property to component on dashboard --- src/main/resources/petriNets/engine-processes/dashboard.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/resources/petriNets/engine-processes/dashboard.xml b/src/main/resources/petriNets/engine-processes/dashboard.xml index 6eea9d570b..6b6aa0c1c3 100644 --- a/src/main/resources/petriNets/engine-processes/dashboard.xml +++ b/src/main/resources/petriNets/engine-processes/dashboard.xml @@ -30,6 +30,7 @@ <component> <name>dashboard</name> + <property key="resolve_data">true</property> </component> </data> <i18n locale="sk"> From 4cef68862e4e775ae70a73c778ffd5fa64263325 Mon Sep 17 00:00:00 2001 From: Machac <machac@netgrif.com> Date: Thu, 13 Feb 2025 13:00:39 +0100 Subject: [PATCH 02/92] [NAE-2053] Optimize ElasticCaseService queries to eliminate maxClauseCount error - ElasticCaseService, ElasticViewPermissionService: refactored queries to use termsQuery and filter, reducing clause count. - pom.xml: updated spring-session to spring-session-core. --- docker-compose.yml | 2 +- pom.xml | 5 +- .../elastic/service/ElasticCaseService.java | 70 ++++++++----------- .../service/ElasticViewPermissionService.java | 64 +++++++++-------- 4 files changed, 69 insertions(+), 72 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 8c0780e377..6ca387520c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,7 +15,7 @@ services: memory: "512M" docker-elastic: - image: elasticsearch:7.17.4 + image: elasticsearch:7.17.26 environment: - cluster.name=elasticsearch - discovery.type=single-node diff --git a/pom.xml b/pom.xml index 98e0fe201c..62ba500b42 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ <groupId>com.netgrif</groupId> <artifactId>application-engine</artifactId> - <version>6.2.9-SNAPSHOT</version> + <version>6.2.10</version> <packaging>jar</packaging> <name>NETGRIF Application Engine</name> @@ -441,8 +441,7 @@ </dependency> <dependency> <groupId>org.springframework.session</groupId> - <artifactId>spring-session</artifactId> - <version>2.0.0.M2</version> + <artifactId>spring-session-core</artifactId> </dependency> <dependency> <groupId>xmlunit</groupId> diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java index d8881d0385..e8dcfe1ebd 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java @@ -142,8 +142,8 @@ public Page<Case> search(List<CaseSearchRequest> requests, LoggedUser user, Page List<Case> casePage; long total; if (query != null) { - SearchHits<ElasticCase> hits = template.search(query, ElasticCase.class, IndexCoordinates.of(caseIndex)); - Page<ElasticCase> indexedCases = (Page)SearchHitSupport.unwrapSearchHits(SearchHitSupport.searchPageFor(hits, query.getPageable())); + SearchHits<ElasticCase> hits = template.search(query, ElasticCase.class, IndexCoordinates.of(caseIndex)); + Page<ElasticCase> indexedCases = (Page) SearchHitSupport.unwrapSearchHits(SearchHitSupport.searchPageFor(hits, query.getPageable())); casePage = workflowService.findAllById(indexedCases.get().map(ElasticCase::getStringId).collect(Collectors.toList())); total = indexedCases.getTotalElements(); } else { @@ -169,14 +169,16 @@ public long count(List<CaseSearchRequest> requests, LoggedUser user, Locale loca } private NativeSearchQuery buildQuery(List<CaseSearchRequest> requests, LoggedUser user, Pageable pageable, Locale locale, Boolean isIntersection) { - List<BoolQueryBuilder> singleQueries = requests.stream().map(request -> buildSingleQuery(request, user, locale)).collect(Collectors.toList()); + List<BoolQueryBuilder> singleQueries = requests.stream() + .map(request -> buildSingleQuery(request, user, locale)) + .collect(Collectors.toList()); if (isIntersection && !singleQueries.stream().allMatch(Objects::nonNull)) { // one of the queries evaluates to empty set => the entire result is an empty set return null; } else if (!isIntersection) { singleQueries = singleQueries.stream().filter(Objects::nonNull).collect(Collectors.toList()); - if (singleQueries.size() == 0) { + if (singleQueries.isEmpty()) { // all queries result in an empty set => the entire result is an empty set return null; } @@ -209,10 +211,7 @@ private BoolQueryBuilder buildSingleQuery(CaseSearchRequest request, LoggedUser // TODO: filtered query https://stackoverflow.com/questions/28116404/filtered-query-using-nativesearchquerybuilder-in-spring-data-elasticsearch - if (resultAlwaysEmpty) - return null; - else - return query; + return resultAlwaysEmpty ? null : query; } private void buildPetriNetQuery(CaseSearchRequest request, LoggedUser user, BoolQueryBuilder query) { @@ -220,17 +219,22 @@ private void buildPetriNetQuery(CaseSearchRequest request, LoggedUser user, Bool return; } - BoolQueryBuilder petriNetQuery = boolQuery(); + List<String> identifiers = request.process.stream() + .filter(p -> p.identifier != null) + .map(p -> p.identifier) + .collect(Collectors.toList()); + List<String> processIds = request.process.stream() + .filter(p -> p.processId != null) + .map(p -> p.processId) + .collect(Collectors.toList()); - for (CaseSearchRequest.PetriNet process : request.process) { - if (process.identifier != null) { - petriNetQuery.should(termQuery("processIdentifier", process.identifier)); - } - if (process.processId != null) { - petriNetQuery.should(termQuery("processId", process.processId)); - } + BoolQueryBuilder petriNetQuery = boolQuery(); + if (!identifiers.isEmpty()) { + petriNetQuery.should(termsQuery("processIdentifier", identifiers)); + } + if (!processIds.isEmpty()) { + petriNetQuery.should(termsQuery("processId", processIds)); } - query.filter(petriNetQuery); } @@ -332,13 +336,7 @@ private void buildRoleQuery(CaseSearchRequest request, BoolQueryBuilder query) { if (request.role == null || request.role.isEmpty()) { return; } - - BoolQueryBuilder roleQuery = boolQuery(); - for (String roleId : request.role) { - roleQuery.should(termQuery("enabledRoles", roleId)); - } - - query.filter(roleQuery); + query.filter(termsQuery("enabledRoles", request.role)); } /** @@ -416,20 +414,14 @@ private void buildCaseIdQuery(CaseSearchRequest request, BoolQueryBuilder query) if (request.stringId == null || request.stringId.isEmpty()) { return; } - - BoolQueryBuilder caseIdQuery = boolQuery(); - request.stringId.forEach(caseId -> caseIdQuery.should(termQuery("stringId", caseId))); - query.filter(caseIdQuery); + query.filter(termsQuery("stringId", request.stringId)); } private void buildUriNodeIdQuery(CaseSearchRequest request, BoolQueryBuilder query) { if (request.uriNodeId == null || request.uriNodeId.isEmpty()) { return; } - - BoolQueryBuilder caseIdQuery = boolQuery(); - caseIdQuery.should(termQuery("uriNodeId", request.uriNodeId)); - query.filter(caseIdQuery); + query.filter(termQuery("uriNodeId", request.uriNodeId)); } /** @@ -458,14 +450,12 @@ private boolean buildGroupQuery(CaseSearchRequest request, LoggedUser user, Loca Map<String, Object> processQuery = new HashMap<>(); processQuery.put("group", request.group); List<PetriNetReference> groupProcesses = this.petriNetService.search(processQuery, user, new FullPageRequest(), locale).getContent(); - if (groupProcesses.size() == 0) + if (groupProcesses.isEmpty()) return true; - - BoolQueryBuilder groupQuery = boolQuery(); - groupProcesses.stream().map(PetriNetReference::getIdentifier) - .map(netIdentifier -> termQuery("processIdentifier", netIdentifier)) - .forEach(groupQuery::should); - query.filter(groupQuery); + List<String> identifiers = groupProcesses.stream() + .map(PetriNetReference::getIdentifier) + .collect(Collectors.toList()); + query.filter(termsQuery("processIdentifier", identifiers)); return false; } -} \ No newline at end of file +} diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticViewPermissionService.java b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticViewPermissionService.java index b3c707b640..cc736dd348 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticViewPermissionService.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticViewPermissionService.java @@ -8,11 +8,13 @@ public abstract class ElasticViewPermissionService { protected void buildViewPermissionQuery(BoolQueryBuilder query, LoggedUser user) { - BoolQueryBuilder viewPermsExists = boolQuery(); - BoolQueryBuilder viewPermNotExists = boolQuery(); - viewPermsExists.should(existsQuery("viewRoles")); - viewPermsExists.should(existsQuery("viewUserRefs")); - viewPermNotExists.mustNot(viewPermsExists); + // Check if viewRoles or viewUserRefs exist + BoolQueryBuilder viewPermsExists = boolQuery() + .should(existsQuery("viewRoles")) + .should(existsQuery("viewUserRefs")); + // Condition where these attributes do NOT exist + BoolQueryBuilder viewPermNotExists = boolQuery() + .mustNot(viewPermsExists); /* Build positive view role query */ BoolQueryBuilder positiveViewRole = buildPositiveViewRoleQuery(viewPermNotExists, user); @@ -38,42 +40,45 @@ protected void buildViewPermissionQuery(BoolQueryBuilder query, LoggedUser user) query.filter(permissionQuery); } + /** + * Build a positive view role query using termsQuery for efficiency. + * This reduces the number of clauses by sending all roles at once. + */ private BoolQueryBuilder buildPositiveViewRoleQuery(BoolQueryBuilder viewPermNotExists, LoggedUser user) { BoolQueryBuilder positiveViewRole = boolQuery(); - BoolQueryBuilder positiveViewRoleQuery = boolQuery(); - for (String roleId : user.getProcessRoles()) { - positiveViewRoleQuery.should(termQuery("viewRoles", roleId)); + if (!user.getProcessRoles().isEmpty()) { + positiveViewRole.should(termsQuery("viewRoles", user.getProcessRoles())); } positiveViewRole.should(viewPermNotExists); - positiveViewRole.should(positiveViewRoleQuery); return positiveViewRole; } + /** + * Build a negative view role query by excluding negative roles. + */ private BoolQueryBuilder buildNegativeViewRoleQuery(LoggedUser user) { BoolQueryBuilder negativeViewRole = boolQuery(); - BoolQueryBuilder negativeViewRoleQuery = boolQuery(); - for (String roleId : user.getProcessRoles()) { - negativeViewRoleQuery.should(termQuery("negativeViewRoles", roleId)); + if (!user.getProcessRoles().isEmpty()) { + negativeViewRole.mustNot(termsQuery("negativeViewRoles", user.getProcessRoles())); } - negativeViewRole.mustNot(negativeViewRoleQuery); return negativeViewRole; } + /** + * Build a positive view user query using filter (as score is not needed). + */ private BoolQueryBuilder buildPositiveViewUser(BoolQueryBuilder viewPermNotExists, LoggedUser user) { - BoolQueryBuilder positiveViewUser = boolQuery(); - BoolQueryBuilder positiveViewUserQuery = boolQuery(); - positiveViewUserQuery.must(termQuery("viewUsers", user.getId())); - positiveViewUser.should(viewPermNotExists); - positiveViewUser.should(positiveViewUserQuery); - return positiveViewUser; + return boolQuery() + .should(viewPermNotExists) + .filter(termQuery("viewUsers", user.getId())); } + /** + * Build a negative view user query to exclude the specified user. + */ private BoolQueryBuilder buildNegativeViewUser(LoggedUser user) { - BoolQueryBuilder negativeViewUser = boolQuery(); - BoolQueryBuilder negativeViewUserQuery = boolQuery(); - negativeViewUserQuery.should(termQuery("negativeViewUsers", user.getId())); - negativeViewUser.mustNot(negativeViewUserQuery); - return negativeViewUser; + return boolQuery() + .mustNot(termQuery("negativeViewUsers", user.getId())); } private BoolQueryBuilder setMinus(BoolQueryBuilder positiveSet, BoolQueryBuilder negativeSet) { @@ -83,10 +88,13 @@ private BoolQueryBuilder setMinus(BoolQueryBuilder positiveSet, BoolQueryBuilder return positiveSetMinusNegativeSet; } + /** + * Unions two queries using OR with a minimum_should_match of 1. + */ private BoolQueryBuilder union(BoolQueryBuilder setA, BoolQueryBuilder setB) { - BoolQueryBuilder unionSet = boolQuery(); - unionSet.should(setA); - unionSet.should(setB); - return unionSet; + return boolQuery() + .should(setA) + .should(setB) + .minimumShouldMatch(1); } } From 31e21f4983a98e6bff99bc1fafebc7b15b429c05 Mon Sep 17 00:00:00 2001 From: Machac <machac@netgrif.com> Date: Mon, 17 Feb 2025 10:34:05 +0100 Subject: [PATCH 03/92] Release/6.2.10 --- CHANGELOG.md | 9 ++++++++- docker-compose.yml | 2 +- pom.xml | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56a4d2031d..8afbe5f6c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -Full Changelog: [https://github.com/netgrif/application-engine/commits/v6.2.9](https://github.com/netgrif/application-engine/commits/v6.2.9) +Full Changelog: [https://github.com/netgrif/application-engine/commits/v6.2.10](https://github.com/netgrif/application-engine/commits/v6.2.10) + +## [6.2.10](https://github.com/netgrif/application-engine/releases/tag/v6.2.9) (2024-02-17) + +### Fixed + +- [NAE-1921] User field value cannot be cleared +- [NAE-2053] Optimize ElasticCaseService queries to eliminate maxClauseCount error ## [6.2.9](https://github.com/netgrif/application-engine/releases/tag/v6.2.9) (2023-05-04) diff --git a/docker-compose.yml b/docker-compose.yml index 8c0780e377..6ca387520c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,7 +15,7 @@ services: memory: "512M" docker-elastic: - image: elasticsearch:7.17.4 + image: elasticsearch:7.17.26 environment: - cluster.name=elasticsearch - discovery.type=single-node diff --git a/pom.xml b/pom.xml index 98e0fe201c..9750dcad80 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ <groupId>com.netgrif</groupId> <artifactId>application-engine</artifactId> - <version>6.2.9-SNAPSHOT</version> + <version>6.2.10</version> <packaging>jar</packaging> <name>NETGRIF Application Engine</name> From 67b38ea98a84f766ff75e91e593277dec3205be7 Mon Sep 17 00:00:00 2001 From: Machac <machac@netgrif.com> Date: Mon, 17 Feb 2025 11:50:12 +0100 Subject: [PATCH 04/92] [NAE-2053] Optimize ElasticCaseService queries to eliminate maxClauseCount error - ElasticCaseService: incorporation of reminders from PR --- .../elastic/service/ElasticCaseService.java | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java index e8dcfe1ebd..aeb7f34ffa 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java @@ -219,14 +219,17 @@ private void buildPetriNetQuery(CaseSearchRequest request, LoggedUser user, Bool return; } - List<String> identifiers = request.process.stream() - .filter(p -> p.identifier != null) - .map(p -> p.identifier) - .collect(Collectors.toList()); - List<String> processIds = request.process.stream() - .filter(p -> p.processId != null) - .map(p -> p.processId) - .collect(Collectors.toList()); + Set<String> identifiers = new HashSet<>(); + Set<String> processIds = new HashSet<>(); + + request.process.forEach(p -> { + if (p.identifier != null) { + identifiers.add(p.identifier); + } + if (p.processId != null) { + processIds.add(p.processId); + } + }); BoolQueryBuilder petriNetQuery = boolQuery(); if (!identifiers.isEmpty()) { @@ -450,8 +453,9 @@ private boolean buildGroupQuery(CaseSearchRequest request, LoggedUser user, Loca Map<String, Object> processQuery = new HashMap<>(); processQuery.put("group", request.group); List<PetriNetReference> groupProcesses = this.petriNetService.search(processQuery, user, new FullPageRequest(), locale).getContent(); - if (groupProcesses.isEmpty()) + if (groupProcesses.isEmpty()) { return true; + } List<String> identifiers = groupProcesses.stream() .map(PetriNetReference::getIdentifier) .collect(Collectors.toList()); From 1fce28b6d8c45ee733c0466cf995d9f2c2f1b9d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Ma=C5=BE=C3=A1ri?= <mazari@netgrif.com> Date: Wed, 26 Feb 2025 09:14:07 +0100 Subject: [PATCH 05/92] [NAE-2061] Release 6.4.1 CE - update CHANGELOG.md --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a39d421e88..f1c231b0fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -Full Changelog: [https://github.com/netgrif/application-engine/commits/v6.4.0](https://github.com/netgrif/application-engine/commits/v6.4.0) +Full Changelog: [https://github.com/netgrif/application-engine/commits/v6.4.1](https://github.com/netgrif/application-engine/commits/v6.4.1) + +## [6.4.1](https://github.com/netgrif/application-engine/releases/tag/v6.4.1) (2025-02-26) + +### Fixed +- [NAE-2031] Dashboard bug fix ## [6.4.0](https://github.com/netgrif/application-engine/releases/tag/v6.4.0) (2024-12-24) From 586d76d6628ec37d6962e559f6b4dd3cdcd5562a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Ma=C5=BE=C3=A1ri?= <mazari@netgrif.com> Date: Wed, 12 Mar 2025 09:31:53 +0100 Subject: [PATCH 06/92] [NAE-2061] - Release 6.4.1 CE - update changelog and version --- CHANGELOG.md | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1c231b0fb..deb494397a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Full Changelog: [https://github.com/netgrif/application-engine/commits/v6.4.1](https://github.com/netgrif/application-engine/commits/v6.4.1) -## [6.4.1](https://github.com/netgrif/application-engine/releases/tag/v6.4.1) (2025-02-26) +## [6.4.1](https://github.com/netgrif/application-engine/releases/tag/v6.4.1) (2025-03-12) ### Fixed - [NAE-2031] Dashboard bug fix diff --git a/pom.xml b/pom.xml index 7c90aec54b..78c23ddbb0 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ <groupId>com.netgrif</groupId> <artifactId>application-engine</artifactId> - <version>6.4.1-SNAPSHOT</version> + <version>6.4.1</version> <packaging>jar</packaging> <name>NETGRIF Application Engine</name> From 2c54a1a25efa6f3804bfd1b0b472b868ecbaf31b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Ma=C5=BE=C3=A1ri?= <mazari@netgrif.com> Date: Wed, 19 Mar 2025 08:14:42 +0100 Subject: [PATCH 07/92] [NAE-2061] - Release 6.4.1 CE - update release date in changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index deb494397a..a36e2dd5c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Full Changelog: [https://github.com/netgrif/application-engine/commits/v6.4.1](https://github.com/netgrif/application-engine/commits/v6.4.1) -## [6.4.1](https://github.com/netgrif/application-engine/releases/tag/v6.4.1) (2025-03-12) +## [6.4.1](https://github.com/netgrif/application-engine/releases/tag/v6.4.1) (2025-03-19) ### Fixed - [NAE-2031] Dashboard bug fix From 9621c43872583790cee5736f82211279254da635 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20=C5=A0ir=C3=A1=C5=88?= <siran@netgrif.com> Date: Thu, 15 May 2025 16:28:05 +0200 Subject: [PATCH 08/92] [NAE-2100] Case view export button as NAE feature - implement exporting cases from case view - added ExportController, XlsExportService, XlsExportConfiguration, XlsExportDateUtils - added attribute allow export to preference_item and MenuItemBody - extend IElasticCaseService with existing method to avoid code duplication - add simple test --- pom.xml | 2 +- .../elastic/service/ElasticCaseService.java | 2 +- .../interfaces/IElasticCaseService.java | 5 + .../configuration/XlsExportProperties.java | 22 ++ .../engine/export/domain/CellFactory.java | 41 +++ .../engine/export/domain/CellType.java | 21 ++ .../engine/export/domain/ExportedField.java | 65 +++++ .../export/service/XlsExportService.java | 249 ++++++++++++++++++ .../service/interfaces/IXlsExportService.java | 18 ++ .../export/utils/XlsExportDateUtils.java | 113 ++++++++ .../engine/export/web/ExportController.java | 66 +++++ .../requestbodies/FilteredCasesRequest.java | 20 ++ .../workflow/domain/menu/MenuItemBody.java | 3 + .../domain/menu/MenuItemConstants.java | 1 + .../engine-processes/preference_item.xml | 23 +- .../export/service/XlsExportServiceTest.java | 74 ++++++ 16 files changed, 722 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/netgrif/application/engine/export/configuration/XlsExportProperties.java create mode 100644 src/main/java/com/netgrif/application/engine/export/domain/CellFactory.java create mode 100644 src/main/java/com/netgrif/application/engine/export/domain/CellType.java create mode 100644 src/main/java/com/netgrif/application/engine/export/domain/ExportedField.java create mode 100644 src/main/java/com/netgrif/application/engine/export/service/XlsExportService.java create mode 100644 src/main/java/com/netgrif/application/engine/export/service/interfaces/IXlsExportService.java create mode 100644 src/main/java/com/netgrif/application/engine/export/utils/XlsExportDateUtils.java create mode 100644 src/main/java/com/netgrif/application/engine/export/web/ExportController.java create mode 100644 src/main/java/com/netgrif/application/engine/export/web/requestbodies/FilteredCasesRequest.java create mode 100644 src/test/groovy/com/netgrif/application/engine/export/service/XlsExportServiceTest.java diff --git a/pom.xml b/pom.xml index 78c23ddbb0..ac1ed53e5d 100644 --- a/pom.xml +++ b/pom.xml @@ -307,7 +307,7 @@ <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> - <version>2.7</version> + <version>2.14.0</version> </dependency> <dependency> <groupId>de.rototor.pdfbox</groupId> diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java index 4c692096cf..d352d4a41f 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java @@ -173,7 +173,7 @@ public String findUriNodeId(Case aCase) { return elasticCase.getUriNodeId(); } - protected NativeSearchQuery buildQuery(List<CaseSearchRequest> requests, LoggedUser user, Pageable pageable, Locale locale, Boolean isIntersection) { + public NativeSearchQuery buildQuery(List<CaseSearchRequest> requests, LoggedUser user, Pageable pageable, Locale locale, Boolean isIntersection) { List<BoolQueryBuilder> singleQueries = requests.stream().map(request -> buildSingleQuery(request, user, locale)).collect(Collectors.toList()); if (isIntersection && !singleQueries.stream().allMatch(Objects::nonNull)) { diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/interfaces/IElasticCaseService.java b/src/main/java/com/netgrif/application/engine/elastic/service/interfaces/IElasticCaseService.java index 024f945aec..0632acbccc 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/interfaces/IElasticCaseService.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/interfaces/IElasticCaseService.java @@ -4,8 +4,11 @@ import com.netgrif.application.engine.elastic.domain.ElasticCase; import com.netgrif.application.engine.elastic.web.requestbodies.CaseSearchRequest; import com.netgrif.application.engine.workflow.domain.Case; +import org.elasticsearch.index.query.BoolQueryBuilder; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.data.elasticsearch.core.query.NativeSearchQuery; import org.springframework.scheduling.annotation.Async; import java.util.List; @@ -27,4 +30,6 @@ public interface IElasticCaseService { void removeByPetriNetId(String processId); String findUriNodeId(Case aCase); + + NativeSearchQuery buildQuery(List<CaseSearchRequest> requests, LoggedUser user, Pageable pageable, Locale locale, Boolean isIntersection); } \ No newline at end of file diff --git a/src/main/java/com/netgrif/application/engine/export/configuration/XlsExportProperties.java b/src/main/java/com/netgrif/application/engine/export/configuration/XlsExportProperties.java new file mode 100644 index 0000000000..346f883f02 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/export/configuration/XlsExportProperties.java @@ -0,0 +1,22 @@ +package com.netgrif.application.engine.export.configuration; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Data +@Configuration +@ConfigurationProperties(prefix = "nae.xls.export") +public class XlsExportProperties { + + private String exportFileName = "export"; + private String sheetName = "export"; + private long maxRows = 10000L; + private int pageSize = 100; + private boolean addMetaData = true; + private String trimWarningMessage = "Tento dokument obsahuje maximálny povolený počet záznamov pre export. Pre zvýšenie limitu exportu záznamov prosím kontaktuje svojho administrátora."; + private String datePattern = "dd.MM.yyyy"; + private String dateTimePattern = "dd.MM.yyyy HH:mm:ss"; + private boolean exportAllImmediateFields = true; + +} diff --git a/src/main/java/com/netgrif/application/engine/export/domain/CellFactory.java b/src/main/java/com/netgrif/application/engine/export/domain/CellFactory.java new file mode 100644 index 0000000000..02b4abc76d --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/export/domain/CellFactory.java @@ -0,0 +1,41 @@ +package com.netgrif.application.engine.export.domain; + +import com.netgrif.application.engine.petrinet.domain.dataset.Field; +import com.netgrif.application.engine.petrinet.domain.dataset.FieldType; +import com.netgrif.application.engine.petrinet.domain.dataset.UserFieldValue; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.util.CellUtil; + +import java.util.Map; + +import static com.netgrif.application.engine.export.utils.XlsExportDateUtils.*; + +public class CellFactory { + + public static String DATE_PATTERN = "yyyy-MM-dd"; + public static String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss"; + + private static final Map<FieldType, CellType> CELL_TYPE_MAP = Map.of( + FieldType.BOOLEAN, new CellType((short) 0x0, (cell, value) -> cell.setCellValue(value.equals(true))), + FieldType.NUMBER, new CellType((short) 0x2, (cell, value) -> cell.setCellValue(Math.round(Double.parseDouble(value.toString()) * 100.0) / 100.0)), + FieldType.DATE, new CellType((short) 0xe, (cell, value) -> cell.setCellValue(dateToString(convertToLocalDateIfNeeded(value), DATE_PATTERN))), + FieldType.DATETIME, new CellType((short) 0x16, (cell, value) -> cell.setCellValue(dateTimeToString(convertToLocalDateTimeIfNeeded(value), DATE_TIME_PATTERN))), + FieldType.USER, new CellType((short) 0x0, (cell, value) -> cell.setCellValue(((UserFieldValue) value).getFullName())) + ); + private static final CellType DEFAULT_CELL_TYPE = new CellType((short) 0x0, (cell, value) -> cell.setCellValue(value.toString())); + + public static Cell create(Row row, int columnIndex, Field<?> field) { + return create(row, columnIndex, field.getType(), field.getValue()); + } + + public static Cell create(Row row, int columnIndex, FieldType fieldType, Object value) { + CellType cellType = CELL_TYPE_MAP.getOrDefault(fieldType, DEFAULT_CELL_TYPE); + Cell cell = row.createCell(columnIndex); + CellUtil.setCellStyleProperty(cell, CellUtil.DATA_FORMAT, cellType.getFormat()); + if (value != null) + cellType.getCellValueSetter().accept(cell, value); + return cell; + } + +} diff --git a/src/main/java/com/netgrif/application/engine/export/domain/CellType.java b/src/main/java/com/netgrif/application/engine/export/domain/CellType.java new file mode 100644 index 0000000000..a169e07d9c --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/export/domain/CellType.java @@ -0,0 +1,21 @@ +package com.netgrif.application.engine.export.domain; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.poi.ss.usermodel.Cell; + +import java.util.function.BiConsumer; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CellType { + + /** + * @see org.apache.poi.ss.usermodel.BuiltinFormats + */ + private short format; + private BiConsumer<Cell, Object> cellValueSetter; + +} diff --git a/src/main/java/com/netgrif/application/engine/export/domain/ExportedField.java b/src/main/java/com/netgrif/application/engine/export/domain/ExportedField.java new file mode 100644 index 0000000000..26ba5c1877 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/export/domain/ExportedField.java @@ -0,0 +1,65 @@ +package com.netgrif.application.engine.export.domain; + +import com.netgrif.application.engine.petrinet.domain.Imported; +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + + +@Data +public class ExportedField extends Imported { + + public static final ExportedField STRING_ID = new ExportedField("meta-stringId", "ID Prípadu", true); + public static final ExportedField AUTHOR = new ExportedField("meta-author", "Autor", true); + public static final ExportedField CREATION_DATE = new ExportedField("meta-creationDate", "Dátum vytvorenia", true); + public static final ExportedField TITLE = new ExportedField("meta-title", "Názov", true); + public static final ExportedField VISUAL_ID = new ExportedField("meta-visualId", "Vizuálne ID", true); + + private String name; + private boolean meta; + + public ExportedField(String id, String name) { + super(); + this.setImportId(id); + this.name = name; + this.meta = false; + } + + public ExportedField(String id, String name, boolean meta) { + super(); + setImportId(id); + this.name = name; + this.meta = meta; + } + + public String getId() { + return getImportId(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ExportedField that = (ExportedField) o; + return Objects.equals(getId(), that.getId()); + } + + @Override + public int hashCode() { + return Objects.hashCode(getId()); + } + + public static List<ExportedField> convert(List<String> fieldIds, List<String> fieldNames) { + if (fieldIds == null || fieldNames == null) return new ArrayList<>(); + if (fieldIds.size() != fieldNames.size()) + throw new IllegalArgumentException("Provided fields IDs does not match to every fields name"); + List<ExportedField> list = new ArrayList<>(fieldIds.size()); + for (int i = 0; i < fieldIds.size(); i++) { + list.add(new ExportedField(fieldIds.get(i), fieldNames.get(i))); + } + return list; + } + +} diff --git a/src/main/java/com/netgrif/application/engine/export/service/XlsExportService.java b/src/main/java/com/netgrif/application/engine/export/service/XlsExportService.java new file mode 100644 index 0000000000..e09287c19f --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/export/service/XlsExportService.java @@ -0,0 +1,249 @@ +package com.netgrif.application.engine.export.service; + +import com.netgrif.application.engine.auth.domain.LoggedUser; +import com.netgrif.application.engine.elastic.domain.ElasticCase; +import com.netgrif.application.engine.elastic.service.interfaces.IElasticCasePrioritySearch; +import com.netgrif.application.engine.elastic.service.interfaces.IElasticCaseService; +import com.netgrif.application.engine.elastic.service.interfaces.IElasticIndexService; +import com.netgrif.application.engine.elastic.web.requestbodies.CaseSearchRequest; +import com.netgrif.application.engine.export.configuration.XlsExportProperties; +import com.netgrif.application.engine.export.service.interfaces.IXlsExportService; +import com.netgrif.application.engine.export.domain.CellFactory; +import com.netgrif.application.engine.export.domain.ExportedField; +import com.netgrif.application.engine.petrinet.domain.PetriNet; +import com.netgrif.application.engine.petrinet.domain.dataset.FieldType; +import com.netgrif.application.engine.petrinet.domain.dataset.MapOptionsField; +import com.netgrif.application.engine.petrinet.service.interfaces.IPetriNetService; +import com.netgrif.application.engine.workflow.domain.Case; +import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService; +import com.netgrif.application.engine.export.web.requestbodies.FilteredCasesRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.xssf.streaming.SXSSFWorkbook; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate; +import org.springframework.data.elasticsearch.core.SearchHitSupport; +import org.springframework.data.elasticsearch.core.SearchScrollHits; +import org.springframework.data.elasticsearch.core.query.NativeSearchQuery; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import java.io.File; +import java.io.FileOutputStream; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +@Slf4j +@Service +@RequiredArgsConstructor +public class XlsExportService implements IXlsExportService { + + @Autowired + private IWorkflowService workflowService; + + @Autowired + private IElasticIndexService elasticIndexService; + + @Autowired + private ElasticsearchRestTemplate elasticsearchTemplate; + + private final IElasticCaseService elasticCaseService; + private final IPetriNetService petriNetService; + private final XlsExportProperties exportProperties; + + @PostConstruct + public void init() { + if (exportProperties.getDatePattern() != null && !exportProperties.getDatePattern().isBlank()) { + CellFactory.DATE_PATTERN = exportProperties.getDatePattern(); + } + if (exportProperties.getDateTimePattern() != null && !exportProperties.getDateTimePattern().isBlank()) { + CellFactory.DATE_TIME_PATTERN = exportProperties.getDateTimePattern(); + } + } + + @Override + public File getExportFilteredCasesFile(FilteredCasesRequest request, LoggedUser user, Locale locale) throws Exception { + List<ExportedField> fieldsToExport = ExportedField.convert(request.getSelectedDataFieldIds(), request.getSelectedDataFieldNames()); + if (exportProperties.isExportAllImmediateFields()) { + fieldsToExport = insertPredefinedFields(fieldsToExport, getProcessIdentifierFromFilteredRequest(request)); + } + return getCasesToExcel(request.getQuery(), fieldsToExport, user, locale, request.getIsIntersection()); + } + + @Override + public File getExportFilteredCasesFile(List<CaseSearchRequest> requests, Boolean isIntersection, List<ExportedField> selectedField, LoggedUser user, Locale locale) throws Exception { + return getCasesToExcel(requests, insertPredefinedFields(selectedField), user, locale, isIntersection); + } + + protected List<ExportedField> insertPredefinedFields(List<ExportedField> fieldToExport) { + return insertPredefinedFields(fieldToExport, null); + } + + protected List<ExportedField> insertPredefinedFields(List<ExportedField> fieldToExport, String processIdentifier) { + if (fieldToExport == null) return new ArrayList<>(); + Set<ExportedField> fields = new LinkedHashSet<>(fieldToExport); + if (exportProperties.isAddMetaData()) { + replaceInSet(fields, ExportedField.STRING_ID); + replaceInSet(fields, ExportedField.VISUAL_ID); + replaceInSet(fields, ExportedField.AUTHOR); + replaceInSet(fields, ExportedField.TITLE); + replaceInSet(fields, ExportedField.CREATION_DATE); + } + + if (processIdentifier == null || processIdentifier.isBlank()) { + return new ArrayList<>(fields); + } + + PetriNet process = petriNetService.getNewestVersionByIdentifier(processIdentifier); + process.getImmediateFields().stream() + .filter(f -> !f.getName().getDefaultValue().isBlank()) + .map(f -> new ExportedField(f.getImportId(), f.getName().getDefaultValue())) + .forEachOrdered(fields::add); + return new ArrayList<>(fields); + } + + protected <T> void replaceInSet(Set<T> fields, T field) { + boolean added = fields.add(field); + if (!added) { + fields.remove(field); + fields.add(field); + } + } + + private File getCasesToExcel(List<CaseSearchRequest> requests, List<ExportedField> fields, LoggedUser user, Locale locale, Boolean isIntersection) throws Exception { + log.info("Exporting cases to xlsx file. Query: {}", requests); + long caseCount = elasticCaseService.count(requests, user, locale, isIntersection); + boolean isResultTrimmed = false; + if (exportProperties.getMaxRows() > 0) { + if (caseCount > exportProperties.getMaxRows()) { + log.warn("Requested case export could resulted in {} rows. Trimming result to {} row as configured in sse.export.max-rows", caseCount, exportProperties.getMaxRows()); + isResultTrimmed = true; + } + caseCount = Math.min(caseCount, exportProperties.getMaxRows()); + } + long numberOfPagesNeeded = caseCount % exportProperties.getPageSize() == 0 ? (caseCount / exportProperties.getPageSize()) : (caseCount / exportProperties.getPageSize()) + 1; + + SXSSFWorkbook workbook = new SXSSFWorkbook(exportProperties.getPageSize()); // https://poi.apache.org/components/spreadsheet/how-to.html#sxssf + Sheet sheet = workbook.createSheet(exportProperties.getSheetName()); + insertHeader(fields, sheet); + + long sizeOfProcessedRows = 0; + long counter = 0; + List<String> scrollIdsToClear = new ArrayList<>(); + NativeSearchQuery query = elasticCaseService.buildQuery(requests, user, PageRequest.of(0, exportProperties.getPageSize()), Locale.ENGLISH, true); + SearchScrollHits<?> scroll = elasticIndexService.scrollFirst(query, ElasticCase.class); + while (scroll.hasSearchHits() && counter < numberOfPagesNeeded) { + Page<ElasticCase> indexedCases = (Page) SearchHitSupport.unwrapSearchHits(SearchHitSupport.searchPageFor(scroll, query.getPageable())); + Page<Case> page = new PageImpl<>(workflowService.findAllById(indexedCases.get().map(ElasticCase::getStringId).collect(Collectors.toList())), query.getPageable(), scroll.getTotalHits()); + scrollIdsToClear.add(scroll.getScrollId()); + sizeOfProcessedRows = processPage(page, sheet, fields, sizeOfProcessedRows); + counter += 1; + scroll = elasticIndexService.scroll(scroll.getScrollId(), ElasticCase.class); + } + scrollIdsToClear.add(scroll.getScrollId()); + elasticsearchTemplate.searchScrollClear(scrollIdsToClear); + + if (isResultTrimmed) { + int lasRow = sheet.getLastRowNum() < 0 ? 0 : sheet.getLastRowNum() + 1; + sheet.createRow(lasRow).createCell(0); + sheet.createRow(lasRow + 1).createCell(0).setCellValue(exportProperties.getTrimWarningMessage()); + } + + File result = File.createTempFile("case-export-" + LocalDateTime.now().toEpochSecond(ZoneOffset.UTC), ".xlsx"); + try (FileOutputStream fout = new FileOutputStream(result)) { + workbook.write(fout); + } catch (Exception ex) { + log.error("Cannot create export for provided query", ex); + throw ex; + } finally { + workbook.close(); + workbook.dispose(); + } + log.info("Successfully exported {} cases to xlsx file.", caseCount); + return result; + } + + private void insertHeader(List<ExportedField> fields, Sheet sheet) { + Row header = sheet.createRow(0); + IntStream.range(0, fields.size()).forEach(idx -> { + Cell cell = header.createCell(idx); + cell.setCellValue(fields.get(idx).getName()); + }); + } + + private long processPage(Page<Case> page, Sheet sheet, List<ExportedField> fieldsToExport, long numberOfProcessedItems) { + int rowIndex = Math.toIntExact(numberOfProcessedItems == 0 ? ((long) page.getNumber() * exportProperties.getPageSize()) + 1 : numberOfProcessedItems); + for (Case caze : page.getContent()) { + Row row = processCase(caze, sheet, rowIndex, fieldsToExport); + if (row != null) + rowIndex++; + } + return rowIndex; + } + + private Row processCase(Case caze, Sheet sheet, int rowIndex, List<ExportedField> fieldsToExport) { + rowIndex = Math.max(rowIndex, 0); + Row row = sheet.createRow(rowIndex); + fieldsToExport.forEach(field -> processField(caze, field, row)); + return row; + } + + private Cell processField(Case caze, ExportedField fieldToExport, Row row) { + Object value = resolveFieldValue(caze, fieldToExport); + FieldType fieldType = caze.getField(fieldToExport.getId()) == null ? FieldType.TEXT : caze.getField(fieldToExport.getId()).getType(); + int cellNum = row.getLastCellNum() < 0 ? 0 : row.getLastCellNum(); + return CellFactory.create(row, cellNum, fieldType, value); + } + + private Object resolveFieldValue(Case caze, ExportedField field) { + try { + if (field.isMeta()) { + return resolveMetaFieldValue(caze, field); + } + if (caze.getField(field.getId()).getType() == FieldType.ENUMERATION_MAP) { + Map<?, ?> options = caze.getDataField(field.getId()).getOptions(); + if (options == null || options.isEmpty()) { + options = ((MapOptionsField<?, ?>) caze.getField(field.getId())).getOptions(); + } + Object value = caze.getFieldValue(field.getId()); + return value == null ? null : options.get(value.toString()).toString(); + } + return caze.getFieldValue(field.getId()); + } catch (Exception ex) { + return "ERROR"; + } + } + + private Object resolveMetaFieldValue(Case caze, ExportedField field) { + if (!field.isMeta()) return null; + if (field.getId().contains("title")) + return caze.getTitle(); + if (field.getId().contains("author")) + return caze.getAuthor().getFullName(); + if (field.getId().contains("creationDate")) + return caze.getCreationDate().format(DateTimeFormatter.ofPattern(exportProperties.getDateTimePattern())); + if (field.getId().contains("visualId")) + return caze.getVisualId(); + if (field.getId().contains("stringId")) + return caze.getStringId(); + return null; + } + + private String getProcessIdentifierFromFilteredRequest(FilteredCasesRequest request) { + return Arrays.stream(request.getQuery().get(0).query.split("\\s+")) + .filter(part -> part.startsWith("processIdentifier:")) + .map(part -> part.split(":", 2)[1]) + .findFirst() + .orElse(""); + } +} diff --git a/src/main/java/com/netgrif/application/engine/export/service/interfaces/IXlsExportService.java b/src/main/java/com/netgrif/application/engine/export/service/interfaces/IXlsExportService.java new file mode 100644 index 0000000000..4aade0845d --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/export/service/interfaces/IXlsExportService.java @@ -0,0 +1,18 @@ +package com.netgrif.application.engine.export.service.interfaces; + + +import com.netgrif.application.engine.auth.domain.LoggedUser; +import com.netgrif.application.engine.elastic.web.requestbodies.CaseSearchRequest; +import com.netgrif.application.engine.export.domain.ExportedField; +import com.netgrif.application.engine.export.web.requestbodies.FilteredCasesRequest; + +import java.io.File; +import java.util.List; +import java.util.Locale; + +public interface IXlsExportService { + + File getExportFilteredCasesFile(FilteredCasesRequest request, LoggedUser user, Locale locale) throws Exception; + + File getExportFilteredCasesFile(List<CaseSearchRequest> requests, Boolean isIntersection, List<ExportedField> selectedField, LoggedUser user, Locale locale) throws Exception; +} diff --git a/src/main/java/com/netgrif/application/engine/export/utils/XlsExportDateUtils.java b/src/main/java/com/netgrif/application/engine/export/utils/XlsExportDateUtils.java new file mode 100644 index 0000000000..b2e331e1c6 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/export/utils/XlsExportDateUtils.java @@ -0,0 +1,113 @@ +package com.netgrif.application.engine.export.utils; + +import lombok.extern.slf4j.Slf4j; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +@Slf4j +public class XlsExportDateUtils { + + + public static LocalDate convertToLocalDate(Date dateToConvert) { + return Instant.ofEpochMilli(dateToConvert.getTime()) + .atZone(ZoneId.systemDefault()) + .toLocalDate(); + } + + public static LocalDateTime convertToLocalDateTime(Date dateTimeToConvert) { + return Instant.ofEpochMilli(dateTimeToConvert.getTime()) + .atZone(ZoneId.systemDefault()) + .toLocalDateTime(); + } + + public static LocalDate parseLocalDate(String date, List<String> patterns) { + if (date == null) { + throw new IllegalArgumentException("Date is null"); + } + + for (String pattern : patterns) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern); + try { + return LocalDate.parse(date, formatter); + + } catch (DateTimeParseException ignored) { + } + } + + log.info("Date " + date + " could not be parsed using patterns " + patterns); + return null; + } + + public static LocalDateTime parseLocalDateTime(String date, List<String> patterns) { + if (date == null) { + throw new IllegalArgumentException("Date is null"); + } + + for (String pattern : patterns) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern); + try { + return LocalDateTime.parse(date, formatter); + + } catch (DateTimeParseException e) { + try { + return LocalDate.parse(date, formatter).atStartOfDay(); + + } catch (DateTimeParseException ignored) { + } + } + } + + log.info("Date " + date + " could not be parsed using patterns " + patterns); + return null; + } + + public static LocalDate convertToLocalDateIfNeeded(Object date) { + if (date == null) return null; + if (date instanceof String) { + return parseLocalDate((String) date, Arrays.asList("dd.MM.yyyy", "d.M.yyyy")); + } + if (date instanceof LocalDate) { + return (LocalDate) date; + } + if (date instanceof Date) { + return convertToLocalDate((Date) date); + } else { + throw new IllegalArgumentException("Error casting field value: Not java.util.Date, java.lang.String, nor java.time.LocalDateTime"); + } + } + + public static LocalDateTime convertToLocalDateTimeIfNeeded(Object dateTime) { + if (dateTime == null) return null; + if (dateTime instanceof String) { + return parseLocalDateTime((String) dateTime, Arrays.asList("dd.MM.yyyy HH:mm", "d.M.yyyy HH:mm", "dd.MM.yyyy HH:mm:ss", "d.M.yyyy HH:mm:ss", "dd.MM.yyyy", "d.M.yyyy")); + } + if (dateTime instanceof LocalDateTime) { + return (LocalDateTime) dateTime; + } + if (dateTime instanceof Date) { + return convertToLocalDateTime((Date) dateTime); + } else { + throw new IllegalArgumentException("Error casting field value: Not java.util.Date, java.lang.String, nor java.time.LocalDateTime"); + } + } + + public static String dateToString(LocalDate date, String pattern) { + if (date == null || pattern == null) return null; + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern); + return date.format(formatter); + } + + public static String dateTimeToString(LocalDateTime dateTime, String pattern) { + if (dateTime == null || pattern == null) return null; + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern); + return dateTime.format(formatter); + } +} diff --git a/src/main/java/com/netgrif/application/engine/export/web/ExportController.java b/src/main/java/com/netgrif/application/engine/export/web/ExportController.java new file mode 100644 index 0000000000..fd6e0f4eb9 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/export/web/ExportController.java @@ -0,0 +1,66 @@ +package com.netgrif.application.engine.export.web; + +import com.netgrif.application.engine.auth.domain.LoggedUser; +import com.netgrif.application.engine.export.configuration.XlsExportProperties; +import com.netgrif.application.engine.export.service.interfaces.IXlsExportService; +import com.netgrif.application.engine.export.web.requestbodies.FilteredCasesRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.FileSystemResource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Locale; + +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v2/export") +public class ExportController { + + private final IXlsExportService exportService; + private final XlsExportProperties exportProperties; + + @PostMapping(value = "/filteredCases", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) + public ResponseEntity<FileSystemResource> getStatisticsFile(@RequestBody FilteredCasesRequest requestBody, Authentication auth, Locale locale) throws Exception { + + LoggedUser user = (LoggedUser) auth.getPrincipal(); + File excel = exportService.getExportFilteredCasesFile(requestBody, user, locale); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); + headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=" + + (LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH_mm_ss")) + "-" + exportProperties.getExportFileName() + ".xlsx")); + headers.setContentLength(Files.size(excel.toPath())); + + return ResponseEntity + .ok() + .headers(headers) + .body(new FileSystemResource(excel) { + @Override + public InputStream getInputStream() throws IOException { + return new FileInputStream(excel) { + @Override + public void close() throws IOException { + super.close(); + Files.delete(excel.toPath()); + } + }; + } + }); + } + +} diff --git a/src/main/java/com/netgrif/application/engine/export/web/requestbodies/FilteredCasesRequest.java b/src/main/java/com/netgrif/application/engine/export/web/requestbodies/FilteredCasesRequest.java new file mode 100644 index 0000000000..38d4c58252 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/export/web/requestbodies/FilteredCasesRequest.java @@ -0,0 +1,20 @@ +package com.netgrif.application.engine.export.web.requestbodies; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.netgrif.application.engine.elastic.web.requestbodies.CaseSearchRequest; +import lombok.Data; + +import java.util.List; + +@Data +public class FilteredCasesRequest { + + @JsonFormat(with = JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY) + private List<CaseSearchRequest> query; + + private List<String> selectedDataFieldNames; + + private List<String> selectedDataFieldIds; + + private Boolean isIntersection; +} \ No newline at end of file diff --git a/src/main/java/com/netgrif/application/engine/workflow/domain/menu/MenuItemBody.java b/src/main/java/com/netgrif/application/engine/workflow/domain/menu/MenuItemBody.java index 0faac7601f..5f59e15ccd 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/domain/menu/MenuItemBody.java +++ b/src/main/java/com/netgrif/application/engine/workflow/domain/menu/MenuItemBody.java @@ -50,6 +50,7 @@ public class MenuItemBody { private List<String> caseDefaultHeaders; private boolean caseIsHeaderModeChangeable = true; private boolean caseUseDefaultHeaders = true; + private boolean caseAllowExport = false; // task view attributes private Case additionalFilter; @@ -232,6 +233,8 @@ private Map<String, Map<String, Object>> toDataSet(String parentId, String nodeP this.caseIsHeaderModeChangeable); putDataSetEntry(dataSet, MenuItemConstants.PREFERENCE_ITEM_FIELD_USE_CASE_DEFAULT_HEADERS, FieldType.BOOLEAN, this.caseUseDefaultHeaders); + putDataSetEntry(dataSet, MenuItemConstants.PREFERENCE_ITEM_FIELD_CASE_ALLOW_EXPORT, FieldType.BOOLEAN, + this.caseAllowExport); // TASK ArrayList<String> additionalFilterIdCaseRefValue = new ArrayList<>(); diff --git a/src/main/java/com/netgrif/application/engine/workflow/domain/menu/MenuItemConstants.java b/src/main/java/com/netgrif/application/engine/workflow/domain/menu/MenuItemConstants.java index 2b4f5a0fec..b449bcc809 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/domain/menu/MenuItemConstants.java +++ b/src/main/java/com/netgrif/application/engine/workflow/domain/menu/MenuItemConstants.java @@ -43,6 +43,7 @@ public enum MenuItemConstants { PREFERENCE_ITEM_FIELD_CASE_HEADERS_DEFAULT_MODE("case_headers_default_mode"), PREFERENCE_ITEM_FIELD_CASE_IS_HEADER_MODE_CHANGEABLE("case_is_header_mode_changeable"), PREFERENCE_ITEM_FIELD_USE_CASE_DEFAULT_HEADERS("case_is_header_mode_changeable"), + PREFERENCE_ITEM_FIELD_CASE_ALLOW_EXPORT("case_allow_export"), PREFERENCE_ITEM_FIELD_ADDITIONAL_FILTER_CASE("additional_filter_case"), PREFERENCE_ITEM_FIELD_MERGE_FILTERS("merge_filters"), PREFERENCE_ITEM_FIELD_TASK_VIEW_SEARCH_TYPE("task_view_search_type"), diff --git a/src/main/resources/petriNets/engine-processes/preference_item.xml b/src/main/resources/petriNets/engine-processes/preference_item.xml index 81deeec6d7..b14ef9d3fc 100644 --- a/src/main/resources/petriNets/engine-processes/preference_item.xml +++ b/src/main/resources/petriNets/engine-processes/preference_item.xml @@ -1019,6 +1019,10 @@ ]) </action> </data> + <data type="boolean" immediate="true"> + <id>case_allow_export</id> + <title name="case_allow_export">Allow export? + @@ -1098,6 +1102,7 @@ Uveďte názov uzlu, ktorý chcete pridať Zoznam uzlov reprezentujúce cieľovú URI Pridať + Povoliť export? Ikonevorschau @@ -1176,6 +1181,7 @@ Rollen, für die wird den Menüeintrag ausgeblendet Nächste URI-Teil angeben Teile der Ziel URI + Erlaube Export? @@ -1952,7 +1958,22 @@ 0 0 1 - 2 + 1 + + outline + + + + case_allow_export + + editable + required + + + 1 + 0 + 1 + 1 outline diff --git a/src/test/groovy/com/netgrif/application/engine/export/service/XlsExportServiceTest.java b/src/test/groovy/com/netgrif/application/engine/export/service/XlsExportServiceTest.java new file mode 100644 index 0000000000..dde329d905 --- /dev/null +++ b/src/test/groovy/com/netgrif/application/engine/export/service/XlsExportServiceTest.java @@ -0,0 +1,74 @@ +package com.netgrif.application.engine.export.service; + +import com.netgrif.application.engine.auth.domain.IUser; +import com.netgrif.application.engine.auth.domain.LoggedUser; +import com.netgrif.application.engine.auth.service.interfaces.IUserService; +import com.netgrif.application.engine.elastic.web.requestbodies.CaseSearchRequest; +import com.netgrif.application.engine.export.service.interfaces.IXlsExportService; +import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService; +import com.netgrif.application.engine.export.web.requestbodies.FilteredCasesRequest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.io.File; +import java.util.List; +import java.util.Locale; +import java.util.stream.IntStream; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SpringBootTest +@ActiveProfiles({"test"}) +@ExtendWith(SpringExtension.class) +public class XlsExportServiceTest { + + @Autowired + private IXlsExportService xlsExportService; + + @Autowired + private IWorkflowService workflowService; + + @Autowired + private IUserService userService; + + @Test + void shouldCreateXlsxFile() throws Exception { + LoggedUser user = getSuperUser(); + + IntStream.range(0,5).forEach(idx -> workflowService.createCaseByIdentifier("preference_item", "Test case", "", user)); + + FilteredCasesRequest request = getTestRequest(); + File excel = xlsExportService.getExportFilteredCasesFile(request, getSuperUser(), Locale.ENGLISH); + assertNotNull(excel); + assertTrue(excel.getName().endsWith(".xlsx")); + assertTrue(excel.length() > 0); + + // Clean up + boolean deleted = excel.delete(); + assertTrue(deleted); + } + + LoggedUser getSuperUser() { + IUser user = userService.findByEmail("super@netgrif.com", true); + return user.transformToLoggedUser(); + } + + FilteredCasesRequest getTestRequest() { + FilteredCasesRequest request = new FilteredCasesRequest(); + request.setQuery(List.of( + CaseSearchRequest.builder() + .query("processIdentifier:preference_item") + .build())); + request.setSelectedDataFieldNames(List.of("Menu Item Identifier", "Item URI", "Menu icon identifier", "Name of the item", "Tab icon identifier", "Name of the item")); + request.setSelectedDataFieldIds(List.of("menu_item_identifier", "nodePath", "menu_icon", "menu_name", "tab_icon", "tab_name")); + request.setIsIntersection(true); + return request; + } + + +} From 820f3b0913887064b0ffc7e5d151b68dc0b0f698 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20=C5=A0ir=C3=A1=C5=88?= Date: Fri, 16 May 2025 14:23:22 +0200 Subject: [PATCH 09/92] NAE-2100 - Case view export button as NAE feature - resolve comments - add documentation and validations for xls export properties - update error messages - update controller mapping - update bean initialization in XlsExportService - update test --- .../configuration/XlsExportProperties.java | 72 ++++++++++++++++++- .../engine/export/domain/ExportedField.java | 5 +- .../export/service/XlsExportService.java | 33 ++++----- .../export/utils/XlsExportDateUtils.java | 4 +- .../engine/export/web/ExportController.java | 2 +- .../export/service/XlsExportServiceTest.java | 17 ++--- 6 files changed, 96 insertions(+), 37 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/export/configuration/XlsExportProperties.java b/src/main/java/com/netgrif/application/engine/export/configuration/XlsExportProperties.java index 346f883f02..5d948bafaf 100644 --- a/src/main/java/com/netgrif/application/engine/export/configuration/XlsExportProperties.java +++ b/src/main/java/com/netgrif/application/engine/export/configuration/XlsExportProperties.java @@ -3,20 +3,90 @@ import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; +import org.springframework.validation.annotation.Validated; +import javax.validation.constraints.Min; + +/** + * Configuration properties for XLS export functionality. + *

+ * Properties are prefixed with nae.xls.export in the configuration files + * (e.g., application.yml or application.properties). + *

+ * These settings control the behavior and formatting of exported Excel files. + */ @Data +@Validated @Configuration @ConfigurationProperties(prefix = "nae.xls.export") public class XlsExportProperties { + /** + * The default name of the exported XLS file (without extension). + *

+ * Example: export + */ private String exportFileName = "export"; + + /** + * The name of the sheet inside the exported XLS file. + *

+ * Example: export + */ private String sheetName = "export"; + + /** + * The maximum number of rows allowed in the export. + *

+ * Must be 0 or greater. If set to 0, row limit is effectively disabled. + * Default is 10,000. + */ + @Min(0) private long maxRows = 10000L; + + /** + * The number of records per export page. + *

+ * Must be greater than 0. Default is 100. + */ + @Min(1) private int pageSize = 100; + + /** + * Whether to include metadata (such as case stringId, case author) in the exported document. + *

+ * Default is true. + */ private boolean addMetaData = true; + + /** + * Whether to export all immediate fields of exported cases. + *

+ * Default is true. + */ + private boolean exportAllImmediateFields = true; + + /** + * Warning message shown when the number of exported records exceeds the configured limit. + *

+ * Default:
+ * Tento dokument obsahuje maximálny povolený počet záznamov pre export. + * Pre zvýšenie limitu exportu záznamov prosím kontaktuje svojho administrátora. + */ private String trimWarningMessage = "Tento dokument obsahuje maximálny povolený počet záznamov pre export. Pre zvýšenie limitu exportu záznamov prosím kontaktuje svojho administrátora."; + + /** + * Date format used in the exported XLS file. + *

+ * Example: dd.MM.yyyy + */ private String datePattern = "dd.MM.yyyy"; + + /** + * Date-time format used in the exported XLS file. + *

+ * Example: dd.MM.yyyy HH:mm:ss + */ private String dateTimePattern = "dd.MM.yyyy HH:mm:ss"; - private boolean exportAllImmediateFields = true; } diff --git a/src/main/java/com/netgrif/application/engine/export/domain/ExportedField.java b/src/main/java/com/netgrif/application/engine/export/domain/ExportedField.java index 26ba5c1877..3742c86f25 100644 --- a/src/main/java/com/netgrif/application/engine/export/domain/ExportedField.java +++ b/src/main/java/com/netgrif/application/engine/export/domain/ExportedField.java @@ -21,10 +21,7 @@ public class ExportedField extends Imported { private boolean meta; public ExportedField(String id, String name) { - super(); - this.setImportId(id); - this.name = name; - this.meta = false; + this(id, name, false); } public ExportedField(String id, String name, boolean meta) { diff --git a/src/main/java/com/netgrif/application/engine/export/service/XlsExportService.java b/src/main/java/com/netgrif/application/engine/export/service/XlsExportService.java index e09287c19f..43b7fff4f9 100644 --- a/src/main/java/com/netgrif/application/engine/export/service/XlsExportService.java +++ b/src/main/java/com/netgrif/application/engine/export/service/XlsExportService.java @@ -2,7 +2,6 @@ import com.netgrif.application.engine.auth.domain.LoggedUser; import com.netgrif.application.engine.elastic.domain.ElasticCase; -import com.netgrif.application.engine.elastic.service.interfaces.IElasticCasePrioritySearch; import com.netgrif.application.engine.elastic.service.interfaces.IElasticCaseService; import com.netgrif.application.engine.elastic.service.interfaces.IElasticIndexService; import com.netgrif.application.engine.elastic.web.requestbodies.CaseSearchRequest; @@ -48,15 +47,9 @@ @RequiredArgsConstructor public class XlsExportService implements IXlsExportService { - @Autowired - private IWorkflowService workflowService; - - @Autowired - private IElasticIndexService elasticIndexService; - - @Autowired - private ElasticsearchRestTemplate elasticsearchTemplate; - + private final IWorkflowService workflowService; + private final IElasticIndexService elasticIndexService; + private final ElasticsearchRestTemplate elasticsearchTemplate; private final IElasticCaseService elasticCaseService; private final IPetriNetService petriNetService; private final XlsExportProperties exportProperties; @@ -74,9 +67,7 @@ public void init() { @Override public File getExportFilteredCasesFile(FilteredCasesRequest request, LoggedUser user, Locale locale) throws Exception { List fieldsToExport = ExportedField.convert(request.getSelectedDataFieldIds(), request.getSelectedDataFieldNames()); - if (exportProperties.isExportAllImmediateFields()) { - fieldsToExport = insertPredefinedFields(fieldsToExport, getProcessIdentifierFromFilteredRequest(request)); - } + fieldsToExport = insertPredefinedFields(fieldsToExport, getProcessIdentifierFromFilteredRequest(request)); return getCasesToExcel(request.getQuery(), fieldsToExport, user, locale, request.getIsIntersection()); } @@ -100,6 +91,10 @@ protected List insertPredefinedFields(List fieldTo replaceInSet(fields, ExportedField.CREATION_DATE); } + if (!exportProperties.isExportAllImmediateFields()) { + return new ArrayList<>(fields); + } + if (processIdentifier == null || processIdentifier.isBlank()) { return new ArrayList<>(fields); } @@ -121,15 +116,15 @@ protected void replaceInSet(Set fields, T field) { } private File getCasesToExcel(List requests, List fields, LoggedUser user, Locale locale, Boolean isIntersection) throws Exception { - log.info("Exporting cases to xlsx file. Query: {}", requests); + log.info("Exporting cases to xlsx file. Query: {}", requests.stream().map(request -> request.query).collect(Collectors.joining(", "))); long caseCount = elasticCaseService.count(requests, user, locale, isIntersection); boolean isResultTrimmed = false; if (exportProperties.getMaxRows() > 0) { if (caseCount > exportProperties.getMaxRows()) { - log.warn("Requested case export could resulted in {} rows. Trimming result to {} row as configured in sse.export.max-rows", caseCount, exportProperties.getMaxRows()); + log.warn("Requested case export could resulted in {} rows. Trimming result to {} row as configured in nae.xls.export.max-rows", caseCount, exportProperties.getMaxRows()); isResultTrimmed = true; + caseCount = exportProperties.getMaxRows(); } - caseCount = Math.min(caseCount, exportProperties.getMaxRows()); } long numberOfPagesNeeded = caseCount % exportProperties.getPageSize() == 0 ? (caseCount / exportProperties.getPageSize()) : (caseCount / exportProperties.getPageSize()) + 1; @@ -137,8 +132,8 @@ private File getCasesToExcel(List requests, List scrollIdsToClear = new ArrayList<>(); NativeSearchQuery query = elasticCaseService.buildQuery(requests, user, PageRequest.of(0, exportProperties.getPageSize()), Locale.ENGLISH, true); SearchScrollHits scroll = elasticIndexService.scrollFirst(query, ElasticCase.class); @@ -181,7 +176,7 @@ private void insertHeader(List fields, Sheet sheet) { }); } - private long processPage(Page page, Sheet sheet, List fieldsToExport, long numberOfProcessedItems) { + private int processPage(Page page, Sheet sheet, List fieldsToExport, int numberOfProcessedItems) { int rowIndex = Math.toIntExact(numberOfProcessedItems == 0 ? ((long) page.getNumber() * exportProperties.getPageSize()) + 1 : numberOfProcessedItems); for (Case caze : page.getContent()) { Row row = processCase(caze, sheet, rowIndex, fieldsToExport); diff --git a/src/main/java/com/netgrif/application/engine/export/utils/XlsExportDateUtils.java b/src/main/java/com/netgrif/application/engine/export/utils/XlsExportDateUtils.java index b2e331e1c6..aaff27ae27 100644 --- a/src/main/java/com/netgrif/application/engine/export/utils/XlsExportDateUtils.java +++ b/src/main/java/com/netgrif/application/engine/export/utils/XlsExportDateUtils.java @@ -42,7 +42,7 @@ public static LocalDate parseLocalDate(String date, List patterns) { } } - log.info("Date " + date + " could not be parsed using patterns " + patterns); + log.error("Date {} could not be parsed using patterns: {}.", date, patterns); return null; } @@ -65,7 +65,7 @@ public static LocalDateTime parseLocalDateTime(String date, List pattern } } - log.info("Date " + date + " could not be parsed using patterns " + patterns); + log.error("Date {} could not be parsed using patterns: {}.", date, patterns); return null; } diff --git a/src/main/java/com/netgrif/application/engine/export/web/ExportController.java b/src/main/java/com/netgrif/application/engine/export/web/ExportController.java index fd6e0f4eb9..27d7a4151a 100644 --- a/src/main/java/com/netgrif/application/engine/export/web/ExportController.java +++ b/src/main/java/com/netgrif/application/engine/export/web/ExportController.java @@ -28,7 +28,7 @@ @Slf4j @RestController @RequiredArgsConstructor -@RequestMapping("/api/v2/export") +@RequestMapping("/api/export") public class ExportController { private final IXlsExportService exportService; diff --git a/src/test/groovy/com/netgrif/application/engine/export/service/XlsExportServiceTest.java b/src/test/groovy/com/netgrif/application/engine/export/service/XlsExportServiceTest.java index dde329d905..392d9b1f06 100644 --- a/src/test/groovy/com/netgrif/application/engine/export/service/XlsExportServiceTest.java +++ b/src/test/groovy/com/netgrif/application/engine/export/service/XlsExportServiceTest.java @@ -5,6 +5,8 @@ import com.netgrif.application.engine.auth.service.interfaces.IUserService; import com.netgrif.application.engine.elastic.web.requestbodies.CaseSearchRequest; import com.netgrif.application.engine.export.service.interfaces.IXlsExportService; +import com.netgrif.application.engine.startup.FilterRunner; +import com.netgrif.application.engine.startup.SuperCreator; import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService; import com.netgrif.application.engine.export.web.requestbodies.FilteredCasesRequest; import org.junit.jupiter.api.Test; @@ -34,16 +36,16 @@ public class XlsExportServiceTest { private IWorkflowService workflowService; @Autowired - private IUserService userService; + private SuperCreator superCreator; @Test void shouldCreateXlsxFile() throws Exception { - LoggedUser user = getSuperUser(); + LoggedUser superUser = superCreator.getSuperUser().transformToLoggedUser(); - IntStream.range(0,5).forEach(idx -> workflowService.createCaseByIdentifier("preference_item", "Test case", "", user)); + IntStream.range(0,5).forEach(idx -> workflowService.createCaseByIdentifier(FilterRunner.PREFERRED_ITEM_NET_IDENTIFIER, "Test case", "", superUser)); FilteredCasesRequest request = getTestRequest(); - File excel = xlsExportService.getExportFilteredCasesFile(request, getSuperUser(), Locale.ENGLISH); + File excel = xlsExportService.getExportFilteredCasesFile(request, superUser, Locale.ENGLISH); assertNotNull(excel); assertTrue(excel.getName().endsWith(".xlsx")); assertTrue(excel.length() > 0); @@ -53,16 +55,11 @@ void shouldCreateXlsxFile() throws Exception { assertTrue(deleted); } - LoggedUser getSuperUser() { - IUser user = userService.findByEmail("super@netgrif.com", true); - return user.transformToLoggedUser(); - } - FilteredCasesRequest getTestRequest() { FilteredCasesRequest request = new FilteredCasesRequest(); request.setQuery(List.of( CaseSearchRequest.builder() - .query("processIdentifier:preference_item") + .query("processIdentifier:" + FilterRunner.PREFERRED_ITEM_NET_IDENTIFIER) .build())); request.setSelectedDataFieldNames(List.of("Menu Item Identifier", "Item URI", "Menu icon identifier", "Name of the item", "Tab icon identifier", "Name of the item")); request.setSelectedDataFieldIds(List.of("menu_item_identifier", "nodePath", "menu_icon", "menu_name", "tab_icon", "tab_name")); From e16faa902f8d8bcb2bba4d350dcdebec957e656f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Ma=C5=BE=C3=A1ri?= Date: Fri, 16 May 2025 15:35:26 +0200 Subject: [PATCH 10/92] [NAE-2101] - Release 6.4.2 CE - update version - add changelog --- CHANGELOG.md | 7 ++++++- pom.xml | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a36e2dd5c9..cfda405bad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -Full Changelog: [https://github.com/netgrif/application-engine/commits/v6.4.1](https://github.com/netgrif/application-engine/commits/v6.4.1) +Full Changelog: [https://github.com/netgrif/application-engine/commits/v6.4.2](https://github.com/netgrif/application-engine/commits/v6.4.2) + +## [6.4.2](https://github.com/netgrif/application-engine/releases/tag/v6.4.2) (2025-05-16) + +### Added +- [NAE-2100] Case view export button as NAE feature ## [6.4.1](https://github.com/netgrif/application-engine/releases/tag/v6.4.1) (2025-03-19) diff --git a/pom.xml b/pom.xml index 78c23ddbb0..9083075911 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ com.netgrif application-engine - 6.4.1 + 6.4.2-SNAPSHOT jar NETGRIF Application Engine From 1f729cc02dc52986be21b7bcf191bb5252734d0b Mon Sep 17 00:00:00 2001 From: Machac Date: Thu, 26 Jun 2025 11:25:37 +0200 Subject: [PATCH 11/92] Update publishing configuration to use Sonatype Central --- .github/workflows/release-build.yml | 2 +- pom.xml | 18 ++++++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 2485befacb..d76764a729 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -142,7 +142,7 @@ jobs: with: java-version: 11 distribution: 'adopt' - server-id: ossrh + server-id: central server-username: MAVEN_USERNAME server-password: MAVEN_PASSWORD diff --git a/pom.xml b/pom.xml index cd46f8d11a..19f9f1921f 100644 --- a/pom.xml +++ b/pom.xml @@ -860,14 +860,14 @@ ossrh-publish - ossrh + central Central Repository OSSRH - https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ + https://central.sonatype.com/ - ossrh + central Central Repository OSSRG Snapshots - https://s01.oss.sonatype.org/content/repositories/snapshots/ + https://central.sonatype.com/repository/maven-snapshots/ @@ -907,6 +907,16 @@ + + org.sonatype.central + central-publishing-maven-plugin + 0.8.0 + true + + central + required + + From 20c28971f4d1a4f24b3f0e2c6ce28564db7913a2 Mon Sep 17 00:00:00 2001 From: Machac Date: Thu, 26 Jun 2025 12:47:45 +0200 Subject: [PATCH 12/92] release 6.4.2-rc.1 --- pom.xml | 52 ++++++++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/pom.xml b/pom.xml index 19f9f1921f..1ff7da9f49 100644 --- a/pom.xml +++ b/pom.xml @@ -66,31 +66,31 @@ https://sonarcloud.io - - - oss.snapshots - OSSRH SNAPSHOT - https://s01.oss.sonatype.org/content/repositories/snapshots - - false - - - true - - - - mvnrepository1 - https://maven.imagej.net/content/repositories/public/ - - - mvnrepository2 - https://repo.spring.io/plugins-release/ - - - mulesoft - https://repository.mulesoft.org/nexus/content/repositories/public/ - - + + + + + + + + + + + + + + + + + + + + + + + + + @@ -435,7 +435,7 @@ com.netgrif quartz-mongodb-connector - 1.0.0-SNAPSHOT + 1.0.0 From a473c0b5cb5b89e9ed581185e66762c1475e932f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Voz=C3=A1r?= Date: Tue, 15 Jul 2025 21:56:34 +0200 Subject: [PATCH 13/92] NAE-2136 - Pridana logika pre Reindex case-ov a taskov cez controller a scheduler --- pom.xml | 7 + .../engine/elastic/domain/CaseField.java | 31 ++++ .../engine/elastic/domain/ElasticCase.java | 9 +- .../LocalDateTimeJsonDeserializer.java | 25 +++ .../LocalDateTimeJsonSerializer.java | 18 +++ .../engine/elastic/service/BulkService.java | 149 ++++++++++++++++++ .../service/ElasticCaseMappingService.java | 7 + .../service/ElasticSearchJsonpMapper.java | 28 ++++ .../elastic/service/ElasticsearchConfig.java | 27 ++++ .../elastic/service/ReindexingTask.java | 84 +++++++--- .../service/interfaces/IBulkService.java | 12 ++ .../engine/elastic/web/ElasticController.java | 14 ++ .../domain/repositories/TaskRepository.java | 2 + src/main/resources/application.properties | 2 +- 14 files changed, 387 insertions(+), 28 deletions(-) create mode 100644 src/main/java/com/netgrif/application/engine/elastic/domain/CaseField.java create mode 100644 src/main/java/com/netgrif/application/engine/elastic/serializer/LocalDateTimeJsonDeserializer.java create mode 100644 src/main/java/com/netgrif/application/engine/elastic/serializer/LocalDateTimeJsonSerializer.java create mode 100644 src/main/java/com/netgrif/application/engine/elastic/service/BulkService.java create mode 100644 src/main/java/com/netgrif/application/engine/elastic/service/ElasticSearchJsonpMapper.java create mode 100644 src/main/java/com/netgrif/application/engine/elastic/service/ElasticsearchConfig.java create mode 100644 src/main/java/com/netgrif/application/engine/elastic/service/interfaces/IBulkService.java diff --git a/pom.xml b/pom.xml index 1ff7da9f49..60d3899457 100644 --- a/pom.xml +++ b/pom.xml @@ -366,6 +366,13 @@ spring-boot-starter-data-elasticsearch + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + com.google.protobuf diff --git a/src/main/java/com/netgrif/application/engine/elastic/domain/CaseField.java b/src/main/java/com/netgrif/application/engine/elastic/domain/CaseField.java new file mode 100644 index 0000000000..2e35d67265 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/elastic/domain/CaseField.java @@ -0,0 +1,31 @@ +package com.netgrif.application.engine.elastic.domain; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.springframework.data.elasticsearch.annotations.Field; + +import java.util.List; + +import static org.springframework.data.elasticsearch.annotations.FieldType.*; + +@Data +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class CaseField extends DataField { + + @Field(type = Text) + public List caseValue; + + public CaseField(List value) { + super(value.toString()); + this.caseValue = value; + } + + @AllArgsConstructor + private static class FileNameAndExtension { + public String name; + public String extension; + } +} diff --git a/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticCase.java b/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticCase.java index 1a13ad9348..89dda11f55 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticCase.java +++ b/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticCase.java @@ -1,9 +1,5 @@ package com.netgrif.application.engine.elastic.domain; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; -import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import com.netgrif.application.engine.workflow.domain.Case; import com.netgrif.application.engine.workflow.domain.TaskPair; import lombok.AllArgsConstructor; @@ -18,6 +14,7 @@ import java.sql.Timestamp; import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -56,8 +53,6 @@ public class ElasticCase { private String title; - @JsonSerialize(using = LocalDateTimeSerializer.class) - @JsonDeserialize(using = LocalDateTimeDeserializer.class) @Field(type = FieldType.Date, format = DateFormat.date_hour_minute_second_millis) private LocalDateTime creationDate; @@ -121,7 +116,7 @@ public ElasticCase(Case useCase) { processId = useCase.getPetriNetId(); visualId = useCase.getVisualId(); title = useCase.getTitle(); - creationDate = useCase.getCreationDate(); + creationDate = useCase.getCreationDate().truncatedTo(ChronoUnit.MILLIS); creationDateSortable = Timestamp.valueOf(useCase.getCreationDate()).getTime(); author = useCase.getAuthor().getId(); authorName = useCase.getAuthor().getFullName(); diff --git a/src/main/java/com/netgrif/application/engine/elastic/serializer/LocalDateTimeJsonDeserializer.java b/src/main/java/com/netgrif/application/engine/elastic/serializer/LocalDateTimeJsonDeserializer.java new file mode 100644 index 0000000000..c0ae2d1114 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/elastic/serializer/LocalDateTimeJsonDeserializer.java @@ -0,0 +1,25 @@ +package com.netgrif.application.engine.elastic.serializer; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.temporal.ChronoField; + +public class LocalDateTimeJsonDeserializer extends JsonDeserializer { + private static final DateTimeFormatter FORMATTER = new DateTimeFormatterBuilder() + .appendPattern("yyyy-MM-dd'T'HH:mm:ss") + .optionalStart() + .appendFraction(ChronoField.MILLI_OF_SECOND, 1, 3, true) + .optionalEnd() + .toFormatter(); + + @Override + public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + return LocalDateTime.parse(p.getValueAsString(), FORMATTER); + } +} \ No newline at end of file diff --git a/src/main/java/com/netgrif/application/engine/elastic/serializer/LocalDateTimeJsonSerializer.java b/src/main/java/com/netgrif/application/engine/elastic/serializer/LocalDateTimeJsonSerializer.java new file mode 100644 index 0000000000..a500adb526 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/elastic/serializer/LocalDateTimeJsonSerializer.java @@ -0,0 +1,18 @@ +package com.netgrif.application.engine.elastic.serializer; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +public class LocalDateTimeJsonSerializer extends JsonSerializer { + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS"); + + @Override + public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeString(FORMATTER.format(value)); + } +} \ No newline at end of file diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/BulkService.java b/src/main/java/com/netgrif/application/engine/elastic/service/BulkService.java new file mode 100644 index 0000000000..af3ba84818 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/elastic/service/BulkService.java @@ -0,0 +1,149 @@ +package com.netgrif.application.engine.elastic.service; + +import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.elasticsearch.core.BulkRequest; +import co.elastic.clients.elasticsearch.core.BulkResponse; +import com.netgrif.application.engine.elastic.domain.ElasticCase; +import com.netgrif.application.engine.elastic.domain.ElasticTask; +import com.netgrif.application.engine.elastic.service.interfaces.*; +import com.netgrif.application.engine.workflow.domain.Case; +import com.netgrif.application.engine.workflow.domain.Task; +import lombok.extern.slf4j.Slf4j; +import org.elasticsearch.ElasticsearchException; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Service responsible for bulk indexing of {@link Case} and {@link Task} entities into Elasticsearch. + * Uses transformation services to map domain objects to their corresponding Elastic representations. + * + * Indexing is performed using upsert operations. + */ +@Service +@Slf4j +public class BulkService implements IBulkService { + @Value("${spring.data.elasticsearch.index.task}") + private String taskIndex; + + @Value("${spring.data.elasticsearch.index.case}") + private String caseIndex; + + private final ElasticsearchClient esClient; + + private final IElasticCaseMappingService elasticCaseMappingService; + + private final IElasticTaskMappingService elasticTaskMappingService; + + + BulkService (@Qualifier("elasticsearchClient") ElasticsearchClient elasticsearchClient, + IElasticCaseMappingService elasticCaseMappingService, + IElasticTaskMappingService elasticTaskMappingService) { + this.esClient = elasticsearchClient; + this.elasticCaseMappingService = elasticCaseMappingService; + this.elasticTaskMappingService = elasticTaskMappingService; + } + + /** + * Performs bulk indexing of a list of {@link Case} objects into the Elasticsearch case index. + * Uses upsert semantics — if a document exists, it is updated; otherwise, it is created. + * + * @param cases the list of case entities to be indexed + */ + @Override + public void bulkIndexCases(List cases) { + BulkRequest.Builder builder = new BulkRequest.Builder(); + + for (Case c : cases) { + try { + if (c.getLastModified() == null) + c.setLastModified(LocalDateTime.now()); + + ElasticCase doc = elasticCaseMappingService.transform(c); + + builder.operations(op -> op + .update(u -> u + .index(caseIndex) + .id(doc.getStringId()) + .action(a -> a + .doc(doc) + .docAsUpsert(true) + ) + ) + ); + } catch (Exception e) { + log.error("Failed to prepare bulk operation for case [{}]: {}", c.getStringId(), e.getMessage()); + } + } + + executeAndValidate(builder.build()); + } + + /** + * Performs bulk indexing of a list of {@link Task} objects into the Elasticsearch task index. + * Uses upsert semantics — if a document exists, it is updated; otherwise, it is created. + * + * @param tasks the list of task entities to be indexed + */ + @Override + public void bulkIndexTasks(List tasks) { + if (tasks == null || tasks.isEmpty()) return; + + log.info("Indexing {} tasks", tasks.size()); + + BulkRequest.Builder requestBuilder = new BulkRequest.Builder(); + + for (Task task : tasks) { + try { + ElasticTask elasticTask = elasticTaskMappingService.transform(task); + + requestBuilder.operations(op -> op + .update(u -> u + .index(taskIndex) + .id(elasticTask.getStringId()) + .action(a -> a + .doc(elasticTask) + .docAsUpsert(true) + ) + ) + ); + } catch (Exception e) { + log.error("Failed to create upsert request for task [{}]: {}", task.getStringId(), e.getMessage()); + } + } + + executeAndValidate(requestBuilder.build()); + } + + private void executeAndValidate(BulkRequest request) { + try { + BulkResponse response = esClient.bulk(request); + checkForBulkUpdateFailure(response); + } catch (Exception e) { + log.error("Failed to index bulk " + e.getMessage(), e); + } + } + + private void checkForBulkUpdateFailure(BulkResponse response) { + Map failedDocuments = new HashMap<>(); + + response.items().forEach(item -> { + if (item.error() != null) { + failedDocuments.put(item.id(), item.error().reason()); + } + }); + + if (!failedDocuments.isEmpty()) { + throw new ElasticsearchException( + "Bulk indexing has failures. Use ElasticsearchException.getFailedDocuments() for details [" + + failedDocuments + "]", + failedDocuments + ); + } + } +} diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseMappingService.java b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseMappingService.java index 18666dfb5f..ee50866e3a 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseMappingService.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseMappingService.java @@ -3,6 +3,7 @@ import com.netgrif.application.engine.elastic.domain.BooleanField; import com.netgrif.application.engine.elastic.domain.ButtonField; +import com.netgrif.application.engine.elastic.domain.CaseField; import com.netgrif.application.engine.elastic.domain.DateField; import com.netgrif.application.engine.elastic.domain.FileField; import com.netgrif.application.engine.elastic.domain.I18nField; @@ -77,6 +78,8 @@ protected Optional transformDataField(String fieldId, Case useCase) { return this.transformFileListField(caseField); } else if (netField instanceof com.netgrif.application.engine.petrinet.domain.dataset.UserListField) { return this.transformUserListField(caseField); + } else if (netField instanceof com.netgrif.application.engine.petrinet.domain.dataset.CaseField) { + return this.transformCaseField(caseField); } else if (netField instanceof com.netgrif.application.engine.petrinet.domain.dataset.I18nField) { return this.transformI18nField(caseField, (com.netgrif.application.engine.petrinet.domain.dataset.I18nField) netField); } else { @@ -283,6 +286,10 @@ protected Optional transformFileListField(com.netgrif.application.eng return Optional.of(new FileField(((FileListFieldValue) fileListField.getValue()).getNamesPaths().toArray(new FileFieldValue[0]))); } + protected Optional transformCaseField(com.netgrif.application.engine.workflow.domain.DataField caseField) { + return Optional.of(new CaseField((List) caseField.getValue())); + } + protected Optional transformOtherFields(com.netgrif.application.engine.workflow.domain.DataField otherField, Field netField) { log.warn("Field of type " + netField.getClass().getCanonicalName() + " is not supported for indexation by default. Indexing the toString() representation of its value..."); return Optional.of(new TextField(otherField.getValue().toString())); diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticSearchJsonpMapper.java b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticSearchJsonpMapper.java new file mode 100644 index 0000000000..aa88227a75 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticSearchJsonpMapper.java @@ -0,0 +1,28 @@ +package com.netgrif.application.engine.elastic.service; + +import co.elastic.clients.json.jackson.JacksonJsonpMapper; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.netgrif.application.engine.elastic.serializer.LocalDateTimeJsonDeserializer; +import com.netgrif.application.engine.elastic.serializer.LocalDateTimeJsonSerializer; + +import java.time.LocalDateTime; + +public class ElasticSearchJsonpMapper extends JacksonJsonpMapper { + public ElasticSearchJsonpMapper() { + super(configureMapper()); + } + + private static ObjectMapper configureMapper() { + ObjectMapper mapper = new ObjectMapper(); + JavaTimeModule javaTimeModule = new JavaTimeModule(); + + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeJsonSerializer()); + javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeJsonDeserializer()); + mapper.registerModule(javaTimeModule); + + return mapper; + } +} diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticsearchConfig.java b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticsearchConfig.java new file mode 100644 index 0000000000..2287d0e031 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticsearchConfig.java @@ -0,0 +1,27 @@ +package com.netgrif.application.engine.elastic.service; + +import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.transport.ElasticsearchTransport; +import co.elastic.clients.transport.rest_client.RestClientTransport; +import com.netgrif.application.engine.configuration.properties.ElasticsearchProperties; +import org.apache.http.HttpHost; +import org.elasticsearch.client.RestClient; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ElasticsearchConfig { + private final ElasticsearchProperties elasticsearchProperties; + + ElasticsearchConfig(ElasticsearchProperties elasticsearchProperties) { + this.elasticsearchProperties = elasticsearchProperties; + } + + @Bean + public ElasticsearchClient elasticsearchClient() { + RestClient restClient = RestClient.builder(new HttpHost(elasticsearchProperties.getUrl(), elasticsearchProperties.getSearchPort())).build(); + ElasticsearchTransport transport = new RestClientTransport(restClient, new ElasticSearchJsonpMapper()); + return new ElasticsearchClient(transport); + + } +} diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java b/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java index ae21422527..d509467a0e 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java @@ -1,10 +1,8 @@ package com.netgrif.application.engine.elastic.service; import com.netgrif.application.engine.elastic.domain.ElasticCaseRepository; -import com.netgrif.application.engine.elastic.service.interfaces.IElasticCaseMappingService; -import com.netgrif.application.engine.elastic.service.interfaces.IElasticCaseService; -import com.netgrif.application.engine.elastic.service.interfaces.IElasticTaskMappingService; -import com.netgrif.application.engine.elastic.service.interfaces.IElasticTaskService; +import com.netgrif.application.engine.elastic.service.interfaces.*; +import com.netgrif.application.engine.petrinet.service.PetriNetService; import com.netgrif.application.engine.workflow.domain.Case; import com.netgrif.application.engine.workflow.domain.QCase; import com.netgrif.application.engine.workflow.domain.Task; @@ -13,6 +11,7 @@ import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService; import com.querydsl.core.types.Predicate; import com.querydsl.core.types.dsl.BooleanExpression; +import org.bson.types.ObjectId; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -21,6 +20,9 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @@ -28,6 +30,7 @@ import java.time.Duration; import java.time.LocalDateTime; import java.util.List; +import java.util.stream.Collectors; @Component @ConditionalOnExpression("'${spring.data.elasticsearch.reindex}'!= 'null'") @@ -35,17 +38,19 @@ public class ReindexingTask { private static final Logger log = LoggerFactory.getLogger(ReindexingTask.class); - private int pageSize; - private CaseRepository caseRepository; - private TaskRepository taskRepository; - private ElasticCaseRepository elasticCaseRepository; - private IElasticCaseService elasticCaseService; - private IElasticTaskService elasticTaskService; - private IElasticCaseMappingService caseMappingService; - private IElasticTaskMappingService taskMappingService; - private IWorkflowService workflowService; - + private final int pageSize; + private final CaseRepository caseRepository; + private final TaskRepository taskRepository; + private final ElasticCaseRepository elasticCaseRepository; + private final IElasticCaseService elasticCaseService; + private final IElasticTaskService elasticTaskService; + private final IElasticCaseMappingService caseMappingService; + private final IElasticTaskMappingService taskMappingService; + private final IWorkflowService workflowService; + private final MongoTemplate mongoTemplate; + private final PetriNetService petriNetService; private LocalDateTime lastRun; + private final IBulkService bulkService; @Autowired public ReindexingTask( @@ -59,6 +64,9 @@ public ReindexingTask( IElasticCaseMappingService caseMappingService, IElasticTaskMappingService taskMappingService, IWorkflowService workflowService, + IBulkService bulkService, + MongoTemplate mongoTemplate, + PetriNetService petriNetService, @Value("${spring.data.elasticsearch.reindexExecutor.size:20}") int pageSize, @Value("${spring.data.elasticsearch.reindex-from:#{null}}") Duration from) { this.caseRepository = caseRepository; @@ -69,7 +77,10 @@ public ReindexingTask( this.caseMappingService = caseMappingService; this.taskMappingService = taskMappingService; this.workflowService = workflowService; + this.mongoTemplate = mongoTemplate; + this.petriNetService = petriNetService; this.pageSize = pageSize; + this.bulkService = bulkService; lastRun = LocalDateTime.now(); if (from != null) { @@ -81,23 +92,56 @@ public ReindexingTask( public void reindex() { log.info("Reindexing stale cases: started reindexing after " + lastRun); - BooleanExpression predicate = QCase.case$.lastModified.before(LocalDateTime.now()).and(QCase.case$.lastModified.after(lastRun.minusMinutes(2))); - + LocalDateTime now = LocalDateTime.now(); + BooleanExpression predicate = QCase.case$.lastModified.before(now).and(QCase.case$.lastModified.after(lastRun.minusMinutes(2))); + LocalDateTime lastRunOld = lastRun; lastRun = LocalDateTime.now(); + long count = caseRepository.count(predicate); if (count > 0) { - reindexAllPages(predicate, count); + reindexAllPages(count, now, lastRunOld); } log.info("Reindexing stale cases: end"); } - private void reindexAllPages(BooleanExpression predicate, long count) { + private void reindexAllPages(long count, LocalDateTime now, LocalDateTime lastRunOld) { long numOfPages = ((count / pageSize) + 1); log.info("Reindexing " + numOfPages + " pages"); + ObjectId lastId = null; + + long page = 0; + while (true) { + page++; + log.info("Reindexing " + page + " / " + numOfPages); + Query query = new Query(); + + if (lastId != null) { + query.addCriteria(Criteria.where("_id").gt(lastId)); + } + query.addCriteria(Criteria.where("lastModified").lt(now).gt(lastRunOld.minusMinutes(2))); + query.limit(pageSize); + + List cases = mongoTemplate.find(query, Case.class); + List casesToIndex = cases.stream().filter(it -> elasticCaseRepository.countByStringIdAndLastModified(it.getStringId(), Timestamp.valueOf(it.getLastModified()).getTime()) == 0).collect(Collectors.toList()); + if (casesToIndex.isEmpty()) { + break; + } + + casesToIndex.forEach(c -> { + if (c.getPetriNet() == null) { + c.setPetriNet(petriNetService.get(c.getPetriNetObjectId())); + } + }); + + bulkService.bulkIndexCases(casesToIndex); + + List caseIds = casesToIndex.stream().map(Case::getStringId).collect(Collectors.toList()); + List tasksToReindex = taskRepository.findAllByCaseIdIn(caseIds); + + bulkService.bulkIndexTasks(tasksToReindex); - for (int page = 0; page < numOfPages; page++) { - reindexPage(predicate, page, numOfPages, false); + lastId = cases.get(cases.size() - 1).get_id(); } } diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/interfaces/IBulkService.java b/src/main/java/com/netgrif/application/engine/elastic/service/interfaces/IBulkService.java new file mode 100644 index 0000000000..95c53fa6da --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/elastic/service/interfaces/IBulkService.java @@ -0,0 +1,12 @@ +package com.netgrif.application.engine.elastic.service.interfaces; + +import com.netgrif.application.engine.workflow.domain.Case; +import com.netgrif.application.engine.workflow.domain.Task; + +import java.util.List; + +public interface IBulkService { + void bulkIndexCases(List cases); + + void bulkIndexTasks(List tasks); +} \ No newline at end of file diff --git a/src/main/java/com/netgrif/application/engine/elastic/web/ElasticController.java b/src/main/java/com/netgrif/application/engine/elastic/web/ElasticController.java index 9a738c8ab5..fc3e4f3b23 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/web/ElasticController.java +++ b/src/main/java/com/netgrif/application/engine/elastic/web/ElasticController.java @@ -85,4 +85,18 @@ public MessageResource reindex(@RequestBody Map searchBody, Auth return MessageResource.errorMessage(e.getMessage()); } } + + @PreAuthorize("hasRole('ADMIN')") + @PostMapping(value = "/index/cursor", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public MessageResource cursorAllReindex() { + try { + + reindexingTask.reindex(); + return MessageResource.successMessage("Success"); + + } catch (Exception e) { + log.error("Could not index: ", e); + return MessageResource.errorMessage(e.getMessage()); + } + } } diff --git a/src/main/java/com/netgrif/application/engine/workflow/domain/repositories/TaskRepository.java b/src/main/java/com/netgrif/application/engine/workflow/domain/repositories/TaskRepository.java index 841b720f50..9ee7b41ab5 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/domain/repositories/TaskRepository.java +++ b/src/main/java/com/netgrif/application/engine/workflow/domain/repositories/TaskRepository.java @@ -16,6 +16,8 @@ public interface TaskRepository extends MongoRepository, QuerydslP List findAllByCaseId(String id); + List findAllByCaseIdIn(Collection ids); + Page findByCaseIdIn(Pageable pageable, Collection ids); Page findByTransitionIdIn(Pageable pageable, Collection ids); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index ea5b8cf5e6..29a957a3fd 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -41,7 +41,7 @@ spring.data.elasticsearch.drop=false spring.data.elasticsearch.executors.size=500 spring.data.elasticsearch.executors.timeout=5 spring.data.elasticsearch.reindex=0 0 * * * * -spring.data.elasticsearch.reindexExecutor.size=20 +spring.data.elasticsearch.reindexExecutor.size=150 spring.data.elasticsearch.reindexExecutor.timeout=60 # Mail Service From 2480a25eaeb6011f81b4bb142a4fa2315e5aae0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Voz=C3=A1r?= Date: Mon, 21 Jul 2025 09:47:34 +0200 Subject: [PATCH 14/92] NAE-2136 - cursor next approach --- .../elastic/service/ReindexingTask.java | 128 +++++++++++++++++- 1 file changed, 125 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java b/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java index d509467a0e..422d500d84 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java @@ -21,14 +21,15 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.mongodb.core.MongoTemplate; -import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.util.CloseableIterator; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.sql.Timestamp; import java.time.Duration; import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; @@ -93,7 +94,8 @@ public void reindex() { log.info("Reindexing stale cases: started reindexing after " + lastRun); LocalDateTime now = LocalDateTime.now(); - BooleanExpression predicate = QCase.case$.lastModified.before(now).and(QCase.case$.lastModified.after(lastRun.minusMinutes(2))); + //BooleanExpression predicate = QCase.case$.lastModified.before(now).and(QCase.case$.lastModified.after(lastRun.minusMinutes(2))); + BooleanExpression predicate = QCase.case$.lastModified.isNotNull(); LocalDateTime lastRunOld = lastRun; lastRun = LocalDateTime.now(); @@ -111,7 +113,106 @@ private void reindexAllPages(long count, LocalDateTime now, LocalDateTime lastRu ObjectId lastId = null; long page = 0; - while (true) { + long pageWhile = 0; + + /*MongoCursor cursor = mongoTemplate + .getCollection("case") + .find() + .iterator(); + + try { + while (cursor.hasNext()) { + page++; + log.info("Reindexing " + page + " / " + numOfPages); + *//*Query query = new Query(); + + if (lastId != null) { + query.addCriteria(Criteria.where("_id").gt(lastId)); + } + query.addCriteria(Criteria.where("lastModified").lt(now).gt(lastRunOld.minusMinutes(2))); + query.limit(pageSize);*//* + + MongoDatabase + + new ArrayList<>(cursor.next().values()); + List cases = mongoTemplate.find(query, Case.class); + List casesToIndex = cases.stream().filter(it -> elasticCaseRepository.countByStringIdAndLastModified(it.getStringId(), Timestamp.valueOf(it.getLastModified()).getTime()) == 0).collect(Collectors.toList()); + if (casesToIndex.isEmpty()) { + break; + } + + casesToIndex.forEach(c -> { + if (c.getPetriNet() == null) { + c.setPetriNet(petriNetService.get(c.getPetriNetObjectId())); + } + }); + + bulkService.bulkIndexCases(casesToIndex); + + List caseIds = casesToIndex.stream().map(Case::getStringId).collect(Collectors.toList()); + List tasksToReindex = taskRepository.findAllByCaseIdIn(caseIds); + + bulkService.bulkIndexTasks(tasksToReindex); + + lastId = cases.get(cases.size() - 1).get_id(); + } + } finally { + cursor.close(); + }*/ + + Query query = new Query(); + + query.cursorBatchSize(pageSize); + + //query.addCriteria(Criteria.where("lastModified").lt(now).gt(lastRunOld.minusMinutes(2))); + + List batch = new ArrayList<>(pageSize); + + try (CloseableIterator cursor = mongoTemplate.stream(query, Case.class)) { + while (cursor.hasNext()) { + /*pageWhile++; + log.info("Reindexing -> " + pageWhile);*/ + batch.add(cursor.next()); + + if (batch.size() == pageSize) { + + page++; + log.info("Reindexing " + page + " / " + numOfPages); + + + reindexCasesBatch(batch); + + + + + + batch.clear(); + } + } + + // posledný batch + if (!batch.isEmpty()) { + reindexCasesBatch(batch); + } + } + + + /*try (CloseableIterator cursor = mongoTemplate.stream(query, Case.class)) { + while (cursor.hasNext()) { + batch.add(cursor.next()); + + if (batch.size() == pageSize) { + batch.clear(); + } + } + if (!batch.isEmpty()) { + + } + }*/ + + + + /* while (true) { page++; log.info("Reindexing " + page + " / " + numOfPages); Query query = new Query(); @@ -142,7 +243,28 @@ private void reindexAllPages(long count, LocalDateTime now, LocalDateTime lastRu bulkService.bulkIndexTasks(tasksToReindex); lastId = cases.get(cases.size() - 1).get_id(); + }*/ + } + + private void reindexCasesBatch(List casesBatch) { + List casesToIndex = casesBatch.stream().filter(it -> elasticCaseRepository.countByStringIdAndLastModified(it.getStringId(), Timestamp.valueOf(it.getLastModified()).getTime()) == 0).collect(Collectors.toList()); + if (casesToIndex.isEmpty()) { + log.info("No cases to reindex"); + return; } + + casesToIndex.forEach(c -> { + if (c.getPetriNet() == null) { + c.setPetriNet(petriNetService.get(c.getPetriNetObjectId())); + } + }); + + bulkService.bulkIndexCases(casesToIndex); + + List caseIds = casesToIndex.stream().map(Case::getStringId).collect(Collectors.toList()); + List tasksToReindex = taskRepository.findAllByCaseIdIn(caseIds); + + bulkService.bulkIndexTasks(tasksToReindex); } public void forceReindexPage(Predicate predicate, int page, long numOfPages) { From 990fe7b573b36bc04ca60be25a55509896fa3a04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Voz=C3=A1r?= Date: Mon, 21 Jul 2025 13:49:14 +0200 Subject: [PATCH 15/92] NAE-2136 - cursor improved indexing and improved loop --- .../engine/elastic/service/BulkService.java | 95 +++++++---- .../elastic/service/ReindexingTask.java | 151 +----------------- 2 files changed, 70 insertions(+), 176 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/BulkService.java b/src/main/java/com/netgrif/application/engine/elastic/service/BulkService.java index af3ba84818..5f01ef53f9 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/BulkService.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/BulkService.java @@ -8,6 +8,7 @@ import com.netgrif.application.engine.elastic.service.interfaces.*; import com.netgrif.application.engine.workflow.domain.Case; import com.netgrif.application.engine.workflow.domain.Task; +import com.netgrif.application.engine.workflow.domain.repositories.TaskRepository; import lombok.extern.slf4j.Slf4j; import org.elasticsearch.ElasticsearchException; import org.springframework.beans.factory.annotation.Qualifier; @@ -15,9 +16,7 @@ import org.springframework.stereotype.Service; import java.time.LocalDateTime; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; /** * Service responsible for bulk indexing of {@link Case} and {@link Task} entities into Elasticsearch. @@ -34,54 +33,69 @@ public class BulkService implements IBulkService { @Value("${spring.data.elasticsearch.index.case}") private String caseIndex; + @Value("${spring.data.elasticsearch.reindexExecutor.size:20}") + private int batchSize; + private final ElasticsearchClient esClient; private final IElasticCaseMappingService elasticCaseMappingService; private final IElasticTaskMappingService elasticTaskMappingService; + private final TaskRepository taskRepository; + + private final List bulkCases = new ArrayList<>(); + private final List bulkCaseIds = new ArrayList<>(); + + private BulkRequest.Builder builder = new BulkRequest.Builder(); + BulkService (@Qualifier("elasticsearchClient") ElasticsearchClient elasticsearchClient, IElasticCaseMappingService elasticCaseMappingService, - IElasticTaskMappingService elasticTaskMappingService) { + IElasticTaskMappingService elasticTaskMappingService, + TaskRepository taskRepository) { this.esClient = elasticsearchClient; this.elasticCaseMappingService = elasticCaseMappingService; this.elasticTaskMappingService = elasticTaskMappingService; + this.taskRepository = taskRepository; } /** - * Performs bulk indexing of a list of {@link Case} objects into the Elasticsearch case index. - * Uses upsert semantics — if a document exists, it is updated; otherwise, it is created. + * Creates elastic upsert operation for given case — if a document exists, it is updated; otherwise, it is created. + * calls indexCases if size of case list in cache equals batch size * - * @param cases the list of case entities to be indexed + * @param aCase the case entities to be indexed */ @Override - public void bulkIndexCases(List cases) { - BulkRequest.Builder builder = new BulkRequest.Builder(); - - for (Case c : cases) { - try { - if (c.getLastModified() == null) - c.setLastModified(LocalDateTime.now()); + public void bulkIndexCase(Case aCase) { + if (aCase == null) return; - ElasticCase doc = elasticCaseMappingService.transform(c); + bulkCases.add(aCase); + bulkCaseIds.add(aCase.getStringId()); - builder.operations(op -> op - .update(u -> u - .index(caseIndex) - .id(doc.getStringId()) - .action(a -> a - .doc(doc) - .docAsUpsert(true) - ) - ) - ); - } catch (Exception e) { - log.error("Failed to prepare bulk operation for case [{}]: {}", c.getStringId(), e.getMessage()); - } + try { + if (aCase.getLastModified() == null) + aCase.setLastModified(LocalDateTime.now()); + + ElasticCase doc = elasticCaseMappingService.transform(aCase); + + builder.operations(op -> op + .update(u -> u + .index(caseIndex) + .id(doc.getStringId()) + .action(a -> a + .doc(doc) + .docAsUpsert(true) + ) + ) + ); + } catch (Exception e) { + log.error("Failed to prepare bulk operation for case [{}]: {}", aCase.getStringId(), e.getMessage()); } - executeAndValidate(builder.build()); + if (bulkCases.size() == batchSize) { + indexCases(); + } } /** @@ -94,8 +108,6 @@ public void bulkIndexCases(List cases) { public void bulkIndexTasks(List tasks) { if (tasks == null || tasks.isEmpty()) return; - log.info("Indexing {} tasks", tasks.size()); - BulkRequest.Builder requestBuilder = new BulkRequest.Builder(); for (Task task : tasks) { @@ -120,12 +132,31 @@ public void bulkIndexTasks(List tasks) { executeAndValidate(requestBuilder.build()); } + /** + * Performs bulk indexing of a list of {@link Case} objects from cache into the Elasticsearch case index. + * Clears cache lists and recreates {@link BulkRequest.Builder} + */ + @Override + public void indexCases() { + if (bulkCases.isEmpty()) { + return; + } + + executeAndValidate(builder.build()); + List tasksToReindex = taskRepository.findAllByCaseIdIn(bulkCaseIds); + bulkIndexTasks(tasksToReindex); + + bulkCases.clear(); + bulkCaseIds.clear(); + this.builder = new BulkRequest.Builder(); + } + private void executeAndValidate(BulkRequest request) { try { BulkResponse response = esClient.bulk(request); checkForBulkUpdateFailure(response); } catch (Exception e) { - log.error("Failed to index bulk " + e.getMessage(), e); + log.error("Failed to index bulk {}", e.getMessage(), e); } } diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java b/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java index 422d500d84..cb84b50dd6 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java @@ -11,7 +11,6 @@ import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService; import com.querydsl.core.types.Predicate; import com.querydsl.core.types.dsl.BooleanExpression; -import org.bson.types.ObjectId; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -29,9 +28,7 @@ import java.sql.Timestamp; import java.time.Duration; import java.time.LocalDateTime; -import java.util.ArrayList; import java.util.List; -import java.util.stream.Collectors; @Component @ConditionalOnExpression("'${spring.data.elasticsearch.reindex}'!= 'null'") @@ -110,161 +107,27 @@ public void reindex() { private void reindexAllPages(long count, LocalDateTime now, LocalDateTime lastRunOld) { long numOfPages = ((count / pageSize) + 1); log.info("Reindexing " + numOfPages + " pages"); - ObjectId lastId = null; - - long page = 0; - long pageWhile = 0; - - /*MongoCursor cursor = mongoTemplate - .getCollection("case") - .find() - .iterator(); - - try { - while (cursor.hasNext()) { - page++; - log.info("Reindexing " + page + " / " + numOfPages); - *//*Query query = new Query(); - - if (lastId != null) { - query.addCriteria(Criteria.where("_id").gt(lastId)); - } - query.addCriteria(Criteria.where("lastModified").lt(now).gt(lastRunOld.minusMinutes(2))); - query.limit(pageSize);*//* - - MongoDatabase - - new ArrayList<>(cursor.next().values()); - List cases = mongoTemplate.find(query, Case.class); - List casesToIndex = cases.stream().filter(it -> elasticCaseRepository.countByStringIdAndLastModified(it.getStringId(), Timestamp.valueOf(it.getLastModified()).getTime()) == 0).collect(Collectors.toList()); - if (casesToIndex.isEmpty()) { - break; - } - - casesToIndex.forEach(c -> { - if (c.getPetriNet() == null) { - c.setPetriNet(petriNetService.get(c.getPetriNetObjectId())); - } - }); - - bulkService.bulkIndexCases(casesToIndex); - - List caseIds = casesToIndex.stream().map(Case::getStringId).collect(Collectors.toList()); - List tasksToReindex = taskRepository.findAllByCaseIdIn(caseIds); - - bulkService.bulkIndexTasks(tasksToReindex); - - lastId = cases.get(cases.size() - 1).get_id(); - } - } finally { - cursor.close(); - }*/ - Query query = new Query(); query.cursorBatchSize(pageSize); //query.addCriteria(Criteria.where("lastModified").lt(now).gt(lastRunOld.minusMinutes(2))); - List batch = new ArrayList<>(pageSize); - try (CloseableIterator cursor = mongoTemplate.stream(query, Case.class)) { - while (cursor.hasNext()) { - /*pageWhile++; - log.info("Reindexing -> " + pageWhile);*/ - batch.add(cursor.next()); - - if (batch.size() == pageSize) { - - page++; - log.info("Reindexing " + page + " / " + numOfPages); - - - reindexCasesBatch(batch); - - - - - - batch.clear(); + cursor.stream().forEach(aCase -> { + if (elasticCaseRepository.countByStringIdAndLastModified(aCase.getStringId(), Timestamp.valueOf(aCase.getLastModified()).getTime()) == 0) { + return; } - } - // posledný batch - if (!batch.isEmpty()) { - reindexCasesBatch(batch); - } - } - - - /*try (CloseableIterator cursor = mongoTemplate.stream(query, Case.class)) { - while (cursor.hasNext()) { - batch.add(cursor.next()); - - if (batch.size() == pageSize) { - batch.clear(); + if (aCase.getPetriNet() == null) { + aCase.setPetriNet(petriNetService.get(aCase.getPetriNetObjectId())); } - } - if (!batch.isEmpty()) { - - } - }*/ - - - - /* while (true) { - page++; - log.info("Reindexing " + page + " / " + numOfPages); - Query query = new Query(); - - if (lastId != null) { - query.addCriteria(Criteria.where("_id").gt(lastId)); - } - query.addCriteria(Criteria.where("lastModified").lt(now).gt(lastRunOld.minusMinutes(2))); - query.limit(pageSize); - List cases = mongoTemplate.find(query, Case.class); - List casesToIndex = cases.stream().filter(it -> elasticCaseRepository.countByStringIdAndLastModified(it.getStringId(), Timestamp.valueOf(it.getLastModified()).getTime()) == 0).collect(Collectors.toList()); - if (casesToIndex.isEmpty()) { - break; - } - - casesToIndex.forEach(c -> { - if (c.getPetriNet() == null) { - c.setPetriNet(petriNetService.get(c.getPetriNetObjectId())); - } + bulkService.bulkIndexCase(aCase); }); - - bulkService.bulkIndexCases(casesToIndex); - - List caseIds = casesToIndex.stream().map(Case::getStringId).collect(Collectors.toList()); - List tasksToReindex = taskRepository.findAllByCaseIdIn(caseIds); - - bulkService.bulkIndexTasks(tasksToReindex); - - lastId = cases.get(cases.size() - 1).get_id(); - }*/ - } - - private void reindexCasesBatch(List casesBatch) { - List casesToIndex = casesBatch.stream().filter(it -> elasticCaseRepository.countByStringIdAndLastModified(it.getStringId(), Timestamp.valueOf(it.getLastModified()).getTime()) == 0).collect(Collectors.toList()); - if (casesToIndex.isEmpty()) { - log.info("No cases to reindex"); - return; } - casesToIndex.forEach(c -> { - if (c.getPetriNet() == null) { - c.setPetriNet(petriNetService.get(c.getPetriNetObjectId())); - } - }); - - bulkService.bulkIndexCases(casesToIndex); - - List caseIds = casesToIndex.stream().map(Case::getStringId).collect(Collectors.toList()); - List tasksToReindex = taskRepository.findAllByCaseIdIn(caseIds); - - bulkService.bulkIndexTasks(tasksToReindex); + bulkService.indexCases(); } public void forceReindexPage(Predicate predicate, int page, long numOfPages) { From dfc51175ceb581cd0defa1bd1c5b36c0e0275f0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Voz=C3=A1r?= Date: Tue, 22 Jul 2025 12:26:01 +0200 Subject: [PATCH 16/92] NAE-2136 - fix pr logging, indexing algorithm, fix configuration, null checks --- pom.xml | 6 ++ .../ElasticsearchConfiguration.java | 13 +++ .../engine/elastic/domain/CaseField.java | 2 +- .../LocalDateTimeJsonDeserializer.java | 6 +- .../engine/elastic/service/BulkService.java | 81 +++++++++++++------ .../elastic/service/ElasticsearchConfig.java | 27 ------- .../elastic/service/ReindexingTask.java | 19 +++-- .../service/interfaces/IBulkService.java | 5 +- .../engine/elastic/web/ElasticController.java | 11 +-- 9 files changed, 101 insertions(+), 69 deletions(-) delete mode 100644 src/main/java/com/netgrif/application/engine/elastic/service/ElasticsearchConfig.java diff --git a/pom.xml b/pom.xml index 60d3899457..bdc8ce6246 100644 --- a/pom.xml +++ b/pom.xml @@ -372,6 +372,12 @@ jackson-datatype-jsr310 + + org.glassfish + jakarta.json + 2.0.1 + + diff --git a/src/main/java/com/netgrif/application/engine/configuration/ElasticsearchConfiguration.java b/src/main/java/com/netgrif/application/engine/configuration/ElasticsearchConfiguration.java index dbd923e5a5..9444649e31 100644 --- a/src/main/java/com/netgrif/application/engine/configuration/ElasticsearchConfiguration.java +++ b/src/main/java/com/netgrif/application/engine/configuration/ElasticsearchConfiguration.java @@ -1,6 +1,11 @@ package com.netgrif.application.engine.configuration; +import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.transport.ElasticsearchTransport; +import co.elastic.clients.transport.rest_client.RestClientTransport; +import com.netgrif.application.engine.configuration.properties.ElasticsearchProperties; import com.netgrif.application.engine.configuration.properties.UriProperties; +import com.netgrif.application.engine.elastic.service.ElasticSearchJsonpMapper; import com.netgrif.application.engine.workflow.service.CaseEventHandler; import org.apache.http.HttpHost; import org.elasticsearch.client.RestClient; @@ -79,4 +84,12 @@ public ElasticsearchOperations elasticsearchTemplate() { public CaseEventHandler caseEventHandler() { return new CaseEventHandler(); } + + @Bean + public ElasticsearchClient elasticsearchClient() { + RestClient restClient = RestClient.builder(new HttpHost(url, port)).build(); + ElasticsearchTransport transport = new RestClientTransport(restClient, new ElasticSearchJsonpMapper()); + return new ElasticsearchClient(transport); + + } } diff --git a/src/main/java/com/netgrif/application/engine/elastic/domain/CaseField.java b/src/main/java/com/netgrif/application/engine/elastic/domain/CaseField.java index 2e35d67265..de13806117 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/domain/CaseField.java +++ b/src/main/java/com/netgrif/application/engine/elastic/domain/CaseField.java @@ -16,7 +16,7 @@ public class CaseField extends DataField { @Field(type = Text) - public List caseValue; + private List caseValue; public CaseField(List value) { super(value.toString()); diff --git a/src/main/java/com/netgrif/application/engine/elastic/serializer/LocalDateTimeJsonDeserializer.java b/src/main/java/com/netgrif/application/engine/elastic/serializer/LocalDateTimeJsonDeserializer.java index c0ae2d1114..1d7b2e8c33 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/serializer/LocalDateTimeJsonDeserializer.java +++ b/src/main/java/com/netgrif/application/engine/elastic/serializer/LocalDateTimeJsonDeserializer.java @@ -20,6 +20,10 @@ public class LocalDateTimeJsonDeserializer extends JsonDeserializer bulkCases = new ArrayList<>(); private final List bulkCaseIds = new ArrayList<>(); + private List bulkTasks = new ArrayList<>(); private BulkRequest.Builder builder = new BulkRequest.Builder(); @@ -93,11 +97,21 @@ public void bulkIndexCase(Case aCase) { log.error("Failed to prepare bulk operation for case [{}]: {}", aCase.getStringId(), e.getMessage()); } - if (bulkCases.size() == batchSize) { + if (bulkCases.size() == caseBatchSize) { indexCases(); } } + /** + * Calls bulkIndexTasks with empty list. + * + */ + @Override + public void bulkIndexTasks() { + bulkIndexTasks(List.of()); + bulkTasks.clear(); + } + /** * Performs bulk indexing of a list of {@link Task} objects into the Elasticsearch task index. * Uses upsert semantics — if a document exists, it is updated; otherwise, it is created. @@ -108,28 +122,20 @@ public void bulkIndexCase(Case aCase) { public void bulkIndexTasks(List tasks) { if (tasks == null || tasks.isEmpty()) return; - BulkRequest.Builder requestBuilder = new BulkRequest.Builder(); + tasks.addAll(0, bulkTasks); + int totalSize = tasks.size(); - for (Task task : tasks) { - try { - ElasticTask elasticTask = elasticTaskMappingService.transform(task); + for (int i = 0; i < totalSize; i += taskBatchSize) { + int end = Math.min(i + taskBatchSize, totalSize); + List batch = tasks.subList(i, end); - requestBuilder.operations(op -> op - .update(u -> u - .index(taskIndex) - .id(elasticTask.getStringId()) - .action(a -> a - .doc(elasticTask) - .docAsUpsert(true) - ) - ) - ); - } catch (Exception e) { - log.error("Failed to create upsert request for task [{}]: {}", task.getStringId(), e.getMessage()); + if (batch.size() < taskBatchSize && !tasks.isEmpty()) { + bulkTasks = batch; + break; } - } - executeAndValidate(requestBuilder.build()); + indexTaskBatch(batch); + } } /** @@ -154,6 +160,7 @@ public void indexCases() { private void executeAndValidate(BulkRequest request) { try { BulkResponse response = esClient.bulk(request); + checkForBulkUpdateFailure(response); } catch (Exception e) { log.error("Failed to index bulk {}", e.getMessage(), e); @@ -163,6 +170,7 @@ private void executeAndValidate(BulkRequest request) { private void checkForBulkUpdateFailure(BulkResponse response) { Map failedDocuments = new HashMap<>(); + response.items().forEach(item -> { if (item.error() != null) { failedDocuments.put(item.id(), item.error().reason()); @@ -170,11 +178,32 @@ private void checkForBulkUpdateFailure(BulkResponse response) { }); if (!failedDocuments.isEmpty()) { - throw new ElasticsearchException( - "Bulk indexing has failures. Use ElasticsearchException.getFailedDocuments() for details [" + - failedDocuments + "]", - failedDocuments - ); + throw new ElasticsearchException("Bulk indexing has failures. Use ElasticsearchException.getFailedDocuments() for details [{}]", failedDocuments); + } + } + + private void indexTaskBatch(List tasks) { + BulkRequest.Builder requestBuilder = new BulkRequest.Builder(); + + for (Task task : tasks) { + try { + ElasticTask elasticTask = elasticTaskMappingService.transform(task); + + requestBuilder.operations(op -> op + .update(u -> u + .index(taskIndex) + .id(elasticTask.getStringId()) + .action(a -> a + .doc(elasticTask) + .docAsUpsert(true) + ) + ) + ); + } catch (Exception e) { + log.error("Failed to create upsert request for task [{}]: {}", task.getStringId(), e.getMessage()); + } } + + executeAndValidate(requestBuilder.build()); } } diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticsearchConfig.java b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticsearchConfig.java deleted file mode 100644 index 2287d0e031..0000000000 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticsearchConfig.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.netgrif.application.engine.elastic.service; - -import co.elastic.clients.elasticsearch.ElasticsearchClient; -import co.elastic.clients.transport.ElasticsearchTransport; -import co.elastic.clients.transport.rest_client.RestClientTransport; -import com.netgrif.application.engine.configuration.properties.ElasticsearchProperties; -import org.apache.http.HttpHost; -import org.elasticsearch.client.RestClient; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class ElasticsearchConfig { - private final ElasticsearchProperties elasticsearchProperties; - - ElasticsearchConfig(ElasticsearchProperties elasticsearchProperties) { - this.elasticsearchProperties = elasticsearchProperties; - } - - @Bean - public ElasticsearchClient elasticsearchClient() { - RestClient restClient = RestClient.builder(new HttpHost(elasticsearchProperties.getUrl(), elasticsearchProperties.getSearchPort())).build(); - ElasticsearchTransport transport = new RestClientTransport(restClient, new ElasticSearchJsonpMapper()); - return new ElasticsearchClient(transport); - - } -} diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java b/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java index cb84b50dd6..e5bd482fbe 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java @@ -50,6 +50,8 @@ public class ReindexingTask { private LocalDateTime lastRun; private final IBulkService bulkService; + private final IElasticCaseMappingService elasticCaseMappingService; + @Autowired public ReindexingTask( CaseRepository caseRepository, @@ -65,8 +67,9 @@ public ReindexingTask( IBulkService bulkService, MongoTemplate mongoTemplate, PetriNetService petriNetService, - @Value("${spring.data.elasticsearch.reindexExecutor.size:20}") int pageSize, - @Value("${spring.data.elasticsearch.reindex-from:#{null}}") Duration from) { + @Value("${spring.data.elasticsearch.reindexExecutor.caseSize:20}") int pageSize, + @Value("${spring.data.elasticsearch.reindex-from:#{null}}") Duration from, + IElasticCaseMappingService elasticCaseMappingService) { this.caseRepository = caseRepository; this.taskRepository = taskRepository; this.elasticCaseRepository = elasticCaseRepository; @@ -79,6 +82,7 @@ public ReindexingTask( this.petriNetService = petriNetService; this.pageSize = pageSize; this.bulkService = bulkService; + this.elasticCaseMappingService = elasticCaseMappingService; lastRun = LocalDateTime.now(); if (from != null) { @@ -88,7 +92,7 @@ public ReindexingTask( @Scheduled(cron = "#{springElasticsearchReindex}") public void reindex() { - log.info("Reindexing stale cases: started reindexing after " + lastRun); + log.info("Reindexing stale cases: started reindexing after {}", lastRun); LocalDateTime now = LocalDateTime.now(); //BooleanExpression predicate = QCase.case$.lastModified.before(now).and(QCase.case$.lastModified.after(lastRun.minusMinutes(2))); @@ -106,7 +110,7 @@ public void reindex() { private void reindexAllPages(long count, LocalDateTime now, LocalDateTime lastRunOld) { long numOfPages = ((count / pageSize) + 1); - log.info("Reindexing " + numOfPages + " pages"); + log.info("Reindexing {} pages", numOfPages); Query query = new Query(); query.cursorBatchSize(pageSize); @@ -115,9 +119,9 @@ private void reindexAllPages(long count, LocalDateTime now, LocalDateTime lastRu try (CloseableIterator cursor = mongoTemplate.stream(query, Case.class)) { cursor.stream().forEach(aCase -> { - if (elasticCaseRepository.countByStringIdAndLastModified(aCase.getStringId(), Timestamp.valueOf(aCase.getLastModified()).getTime()) == 0) { + /*if (elasticCaseRepository.countByStringIdAndLastModified(aCase.getStringId(), Timestamp.valueOf(aCase.getLastModified()).getTime()) == 0) { return; - } + }*/ if (aCase.getPetriNet() == null) { aCase.setPetriNet(petriNetService.get(aCase.getPetriNetObjectId())); @@ -128,6 +132,7 @@ private void reindexAllPages(long count, LocalDateTime now, LocalDateTime lastRu } bulkService.indexCases(); + bulkService.bulkIndexTasks(); } public void forceReindexPage(Predicate predicate, int page, long numOfPages) { @@ -135,7 +140,7 @@ public void forceReindexPage(Predicate predicate, int page, long numOfPages) { } private void reindexPage(Predicate predicate, int page, long numOfPages, boolean forced) { - log.info("Reindexing " + (page + 1) + " / " + numOfPages); + log.info("Reindexing {} / {}", (page + 1), numOfPages); Page cases = this.workflowService.search(predicate, PageRequest.of(page, pageSize)); for (Case aCase : cases) { diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/interfaces/IBulkService.java b/src/main/java/com/netgrif/application/engine/elastic/service/interfaces/IBulkService.java index 95c53fa6da..8aae214e5f 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/interfaces/IBulkService.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/interfaces/IBulkService.java @@ -6,7 +6,8 @@ import java.util.List; public interface IBulkService { - void bulkIndexCases(List cases); - + void bulkIndexCase(Case cases); + void bulkIndexTasks(); void bulkIndexTasks(List tasks); + void indexCases(); } \ No newline at end of file diff --git a/src/main/java/com/netgrif/application/engine/elastic/web/ElasticController.java b/src/main/java/com/netgrif/application/engine/elastic/web/ElasticController.java index fc3e4f3b23..436469bb0e 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/web/ElasticController.java +++ b/src/main/java/com/netgrif/application/engine/elastic/web/ElasticController.java @@ -69,11 +69,11 @@ public MessageResource reindex(@RequestBody Map searchBody, Auth if (count == 0) { log.info("No cases to reindex"); } else { - long numOfPages = (long) ((count / pageSize) + 1); - log.info("Reindexing cases: " + numOfPages + " pages"); + long numOfPages = (count / pageSize) + 1; + log.info("Reindexing cases: {} pages", numOfPages); for (int page = 0; page < numOfPages; page++) { - log.info("Indexing page " + (page + 1)); + log.info("Indexing page {}", (page + 1)); Predicate predicate = searchService.buildQuery(searchBody, user, locale); reindexingTask.forceReindexPage(predicate, page, numOfPages); } @@ -86,8 +86,9 @@ public MessageResource reindex(@RequestBody Map searchBody, Auth } } - @PreAuthorize("hasRole('ADMIN')") - @PostMapping(value = "/index/cursor", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + //@PreAuthorize("hasRole('ADMIN')") + @PreAuthorize("@authorizationService.hasAuthority('ADMIN')") + @PostMapping(value = "/index/cursor", produces = MediaType.APPLICATION_JSON_VALUE) public MessageResource cursorAllReindex() { try { From 9eef8e0d1eb65c433abb1e06cc574628de389937 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Voz=C3=A1r?= Date: Tue, 22 Jul 2025 12:28:14 +0200 Subject: [PATCH 17/92] NAE-2136 - restrict indexing only for past till now - 2 minutes --- .../engine/elastic/service/ReindexingTask.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java b/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java index e5bd482fbe..ea9ac47f20 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java @@ -20,6 +20,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.util.CloseableIterator; import org.springframework.scheduling.annotation.Scheduled; @@ -95,8 +96,7 @@ public void reindex() { log.info("Reindexing stale cases: started reindexing after {}", lastRun); LocalDateTime now = LocalDateTime.now(); - //BooleanExpression predicate = QCase.case$.lastModified.before(now).and(QCase.case$.lastModified.after(lastRun.minusMinutes(2))); - BooleanExpression predicate = QCase.case$.lastModified.isNotNull(); + BooleanExpression predicate = QCase.case$.lastModified.before(now).and(QCase.case$.lastModified.after(lastRun.minusMinutes(2))); LocalDateTime lastRunOld = lastRun; lastRun = LocalDateTime.now(); @@ -115,13 +115,13 @@ private void reindexAllPages(long count, LocalDateTime now, LocalDateTime lastRu query.cursorBatchSize(pageSize); - //query.addCriteria(Criteria.where("lastModified").lt(now).gt(lastRunOld.minusMinutes(2))); + query.addCriteria(Criteria.where("lastModified").lt(now).gt(lastRunOld.minusMinutes(2))); try (CloseableIterator cursor = mongoTemplate.stream(query, Case.class)) { cursor.stream().forEach(aCase -> { - /*if (elasticCaseRepository.countByStringIdAndLastModified(aCase.getStringId(), Timestamp.valueOf(aCase.getLastModified()).getTime()) == 0) { + if (elasticCaseRepository.countByStringIdAndLastModified(aCase.getStringId(), Timestamp.valueOf(aCase.getLastModified()).getTime()) == 0) { return; - }*/ + } if (aCase.getPetriNet() == null) { aCase.setPetriNet(petriNetService.get(aCase.getPetriNetObjectId())); From f286be955931f3e719564b552ca2398e284676ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Voz=C3=A1r?= Date: Tue, 22 Jul 2025 14:32:56 +0200 Subject: [PATCH 18/92] NAE-2136 - revert usage of unsecure global lists and new numbers of of bulk indexes set --- .../engine/elastic/service/BulkService.java | 99 +++++-------------- .../elastic/service/ReindexingTask.java | 48 ++++++--- .../service/interfaces/IBulkService.java | 4 +- src/main/resources/application.properties | 3 +- 4 files changed, 63 insertions(+), 91 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/BulkService.java b/src/main/java/com/netgrif/application/engine/elastic/service/BulkService.java index 77aeae87c4..a33a0e286c 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/BulkService.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/BulkService.java @@ -8,7 +8,6 @@ import com.netgrif.application.engine.elastic.service.interfaces.*; import com.netgrif.application.engine.workflow.domain.Case; import com.netgrif.application.engine.workflow.domain.Task; -import com.netgrif.application.engine.workflow.domain.repositories.TaskRepository; import lombok.extern.slf4j.Slf4j; import org.elasticsearch.ElasticsearchException; import org.springframework.beans.factory.annotation.Qualifier; @@ -45,71 +44,48 @@ public class BulkService implements IBulkService { private final IElasticTaskMappingService elasticTaskMappingService; - private final TaskRepository taskRepository; - - private final List bulkCases = new ArrayList<>(); - private final List bulkCaseIds = new ArrayList<>(); - private List bulkTasks = new ArrayList<>(); - - private BulkRequest.Builder builder = new BulkRequest.Builder(); - BulkService (@Qualifier("elasticsearchClient") ElasticsearchClient elasticsearchClient, IElasticCaseMappingService elasticCaseMappingService, - IElasticTaskMappingService elasticTaskMappingService, - TaskRepository taskRepository) { + IElasticTaskMappingService elasticTaskMappingService) { this.esClient = elasticsearchClient; this.elasticCaseMappingService = elasticCaseMappingService; this.elasticTaskMappingService = elasticTaskMappingService; - this.taskRepository = taskRepository; } /** - * Creates elastic upsert operation for given case — if a document exists, it is updated; otherwise, it is created. - * calls indexCases if size of case list in cache equals batch size + * Performs bulk indexing of a list of {@link Case} objects into the Elasticsearch case index. + * Uses upsert semantics — if a document exists, it is updated; otherwise, it is created. * - * @param aCase the case entities to be indexed + * @param cases the list of case entities to be indexed */ @Override - public void bulkIndexCase(Case aCase) { - if (aCase == null) return; + public void bulkIndexCases(List cases) { + BulkRequest.Builder builder = new BulkRequest.Builder(); - bulkCases.add(aCase); - bulkCaseIds.add(aCase.getStringId()); + for (Case c : cases) { + try { + if (c.getLastModified() == null) + c.setLastModified(LocalDateTime.now()); - try { - if (aCase.getLastModified() == null) - aCase.setLastModified(LocalDateTime.now()); - - ElasticCase doc = elasticCaseMappingService.transform(aCase); - - builder.operations(op -> op - .update(u -> u - .index(caseIndex) - .id(doc.getStringId()) - .action(a -> a - .doc(doc) - .docAsUpsert(true) - ) - ) - ); - } catch (Exception e) { - log.error("Failed to prepare bulk operation for case [{}]: {}", aCase.getStringId(), e.getMessage()); - } + ElasticCase doc = elasticCaseMappingService.transform(c); - if (bulkCases.size() == caseBatchSize) { - indexCases(); + builder.operations(op -> op + .update(u -> u + .index(caseIndex) + .id(doc.getStringId()) + .action(a -> a + .doc(doc) + .docAsUpsert(true) + ) + ) + ); + } catch (Exception e) { + log.error("Failed to prepare bulk operation for case [{}]: {}", c.getStringId(), e.getMessage()); + } } - } - /** - * Calls bulkIndexTasks with empty list. - * - */ - @Override - public void bulkIndexTasks() { - bulkIndexTasks(List.of()); - bulkTasks.clear(); + executeAndValidate(builder.build()); } /** @@ -122,41 +98,18 @@ public void bulkIndexTasks() { public void bulkIndexTasks(List tasks) { if (tasks == null || tasks.isEmpty()) return; - tasks.addAll(0, bulkTasks); int totalSize = tasks.size(); for (int i = 0; i < totalSize; i += taskBatchSize) { int end = Math.min(i + taskBatchSize, totalSize); List batch = tasks.subList(i, end); - if (batch.size() < taskBatchSize && !tasks.isEmpty()) { - bulkTasks = batch; - break; - } + log.info("Reindexing task page {} / {}", i / taskBatchSize, totalSize / taskBatchSize); indexTaskBatch(batch); } } - /** - * Performs bulk indexing of a list of {@link Case} objects from cache into the Elasticsearch case index. - * Clears cache lists and recreates {@link BulkRequest.Builder} - */ - @Override - public void indexCases() { - if (bulkCases.isEmpty()) { - return; - } - - executeAndValidate(builder.build()); - List tasksToReindex = taskRepository.findAllByCaseIdIn(bulkCaseIds); - bulkIndexTasks(tasksToReindex); - - bulkCases.clear(); - bulkCaseIds.clear(); - this.builder = new BulkRequest.Builder(); - } - private void executeAndValidate(BulkRequest request) { try { BulkResponse response = esClient.bulk(request); diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java b/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java index ea9ac47f20..dd3ef8b52f 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java @@ -29,7 +29,10 @@ import java.sql.Timestamp; import java.time.Duration; import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; @Component @ConditionalOnExpression("'${spring.data.elasticsearch.reindex}'!= 'null'") @@ -51,8 +54,6 @@ public class ReindexingTask { private LocalDateTime lastRun; private final IBulkService bulkService; - private final IElasticCaseMappingService elasticCaseMappingService; - @Autowired public ReindexingTask( CaseRepository caseRepository, @@ -69,8 +70,7 @@ public ReindexingTask( MongoTemplate mongoTemplate, PetriNetService petriNetService, @Value("${spring.data.elasticsearch.reindexExecutor.caseSize:20}") int pageSize, - @Value("${spring.data.elasticsearch.reindex-from:#{null}}") Duration from, - IElasticCaseMappingService elasticCaseMappingService) { + @Value("${spring.data.elasticsearch.reindex-from:#{null}}") Duration from) { this.caseRepository = caseRepository; this.taskRepository = taskRepository; this.elasticCaseRepository = elasticCaseRepository; @@ -83,7 +83,6 @@ public ReindexingTask( this.petriNetService = petriNetService; this.pageSize = pageSize; this.bulkService = bulkService; - this.elasticCaseMappingService = elasticCaseMappingService; lastRun = LocalDateTime.now(); if (from != null) { @@ -112,33 +111,54 @@ private void reindexAllPages(long count, LocalDateTime now, LocalDateTime lastRu long numOfPages = ((count / pageSize) + 1); log.info("Reindexing {} pages", numOfPages); Query query = new Query(); + AtomicInteger page = new AtomicInteger(); query.cursorBatchSize(pageSize); query.addCriteria(Criteria.where("lastModified").lt(now).gt(lastRunOld.minusMinutes(2))); + List batch = new ArrayList<>(pageSize); try (CloseableIterator cursor = mongoTemplate.stream(query, Case.class)) { cursor.stream().forEach(aCase -> { - if (elasticCaseRepository.countByStringIdAndLastModified(aCase.getStringId(), Timestamp.valueOf(aCase.getLastModified()).getTime()) == 0) { - return; - } + batch.add(aCase); + + if (batch.size() == pageSize) { + page.getAndIncrement(); + log.info("Reindexing {} / {}", page, numOfPages); - if (aCase.getPetriNet() == null) { - aCase.setPetriNet(petriNetService.get(aCase.getPetriNetObjectId())); + reindexCasesBatch(batch); + batch.clear(); } - bulkService.bulkIndexCase(aCase); }); } - - bulkService.indexCases(); - bulkService.bulkIndexTasks(); } public void forceReindexPage(Predicate predicate, int page, long numOfPages) { reindexPage(predicate, page, numOfPages, true); } + private void reindexCasesBatch(List casesBatch) { + List casesToIndex = casesBatch.stream().filter(it -> elasticCaseRepository.countByStringIdAndLastModified(it.getStringId(), Timestamp.valueOf(it.getLastModified()).getTime()) == 0).collect(Collectors.toList()); + if (casesToIndex.isEmpty()) { + log.info("No cases to reindex"); + return; + } + + casesToIndex.forEach(c -> { + if (c.getPetriNet() == null) { + c.setPetriNet(petriNetService.get(c.getPetriNetObjectId())); + } + }); + + bulkService.bulkIndexCases(casesToIndex); + + List caseIds = casesToIndex.stream().map(Case::getStringId).collect(Collectors.toList()); + List tasksToReindex = taskRepository.findAllByCaseIdIn(caseIds); + + bulkService.bulkIndexTasks(tasksToReindex); + } + private void reindexPage(Predicate predicate, int page, long numOfPages, boolean forced) { log.info("Reindexing {} / {}", (page + 1), numOfPages); Page cases = this.workflowService.search(predicate, PageRequest.of(page, pageSize)); diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/interfaces/IBulkService.java b/src/main/java/com/netgrif/application/engine/elastic/service/interfaces/IBulkService.java index 8aae214e5f..3dfaf6fd5d 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/interfaces/IBulkService.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/interfaces/IBulkService.java @@ -6,8 +6,6 @@ import java.util.List; public interface IBulkService { - void bulkIndexCase(Case cases); - void bulkIndexTasks(); + void bulkIndexCases(List cases); void bulkIndexTasks(List tasks); - void indexCases(); } \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 29a957a3fd..8d996df2fc 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -41,7 +41,8 @@ spring.data.elasticsearch.drop=false spring.data.elasticsearch.executors.size=500 spring.data.elasticsearch.executors.timeout=5 spring.data.elasticsearch.reindex=0 0 * * * * -spring.data.elasticsearch.reindexExecutor.size=150 +spring.data.elasticsearch.reindexExecutor.caseSize=5100 +spring.data.elasticsearch.reindexExecutor.taskSize=20000 spring.data.elasticsearch.reindexExecutor.timeout=60 # Mail Service From 714790d309292d8dd115d1bdcf03dd154bbca8a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Voz=C3=A1r?= Date: Wed, 23 Jul 2025 11:40:26 +0200 Subject: [PATCH 19/92] NAE-2136 - modified solution based on dividing the number of operations in bulk requests --- .../engine/elastic/service/BulkService.java | 48 ++++++++++++++----- .../elastic/service/ReindexingTask.java | 24 +++++----- 2 files changed, 48 insertions(+), 24 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/BulkService.java b/src/main/java/com/netgrif/application/engine/elastic/service/BulkService.java index a33a0e286c..d19e239be3 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/BulkService.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/BulkService.java @@ -3,6 +3,7 @@ import co.elastic.clients.elasticsearch.ElasticsearchClient; import co.elastic.clients.elasticsearch.core.BulkRequest; import co.elastic.clients.elasticsearch.core.BulkResponse; +import co.elastic.clients.elasticsearch.core.bulk.BulkOperation; import com.netgrif.application.engine.elastic.domain.ElasticCase; import com.netgrif.application.engine.elastic.domain.ElasticTask; import com.netgrif.application.engine.elastic.service.interfaces.*; @@ -61,7 +62,7 @@ public class BulkService implements IBulkService { */ @Override public void bulkIndexCases(List cases) { - BulkRequest.Builder builder = new BulkRequest.Builder(); + List operations = new ArrayList<>(); for (Case c : cases) { try { @@ -70,7 +71,7 @@ public void bulkIndexCases(List cases) { ElasticCase doc = elasticCaseMappingService.transform(c); - builder.operations(op -> op + operations.add(BulkOperation.of(op -> op .update(u -> u .index(caseIndex) .id(doc.getStringId()) @@ -78,14 +79,13 @@ public void bulkIndexCases(List cases) { .doc(doc) .docAsUpsert(true) ) - ) - ); + ))); } catch (Exception e) { log.error("Failed to prepare bulk operation for case [{}]: {}", c.getStringId(), e.getMessage()); } } - executeAndValidate(builder.build()); + executeAndValidate(operations); } /** @@ -110,13 +110,34 @@ public void bulkIndexTasks(List tasks) { } } - private void executeAndValidate(BulkRequest request) { - try { - BulkResponse response = esClient.bulk(request); + private void executeAndValidate(List operations) { + if (operations.isEmpty()) { + return; + } + BulkRequest.Builder builder = new BulkRequest.Builder(); + builder.operations(operations); + + try { + BulkResponse response = esClient.bulk(builder.build()); checkForBulkUpdateFailure(response); + log.info("Batch indexed successfully with {} ops", operations.size()); } catch (Exception e) { - log.error("Failed to index bulk {}", e.getMessage(), e); + log.warn("Failed for {} ops to index bulk {}", operations.size(), e.getMessage(), e); + + if (operations.size() == 1) { + log.error("Single operation failed. Skipping. {}", operations.get(0), e); + return; + } + + log.warn("Dividing the requirement."); + + int mid = operations.size() / 2; + List left = operations.subList(0, mid); + List right = operations.subList(mid, operations.size()); + + executeAndValidate(left); + executeAndValidate(right); } } @@ -136,13 +157,14 @@ private void checkForBulkUpdateFailure(BulkResponse response) { } private void indexTaskBatch(List tasks) { - BulkRequest.Builder requestBuilder = new BulkRequest.Builder(); + + List operations = new ArrayList<>(); for (Task task : tasks) { try { ElasticTask elasticTask = elasticTaskMappingService.transform(task); - requestBuilder.operations(op -> op + operations.add(BulkOperation.of(op -> op .update(u -> u .index(taskIndex) .id(elasticTask.getStringId()) @@ -150,13 +172,13 @@ private void indexTaskBatch(List tasks) { .doc(elasticTask) .docAsUpsert(true) ) - ) + )) ); } catch (Exception e) { log.error("Failed to create upsert request for task [{}]: {}", task.getStringId(), e.getMessage()); } } - executeAndValidate(requestBuilder.build()); + executeAndValidate(operations); } } diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java b/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java index dd3ef8b52f..ffcb0489fd 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java @@ -40,7 +40,7 @@ public class ReindexingTask { private static final Logger log = LoggerFactory.getLogger(ReindexingTask.class); - private final int pageSize; + private final int caseBatchSize; private final CaseRepository caseRepository; private final TaskRepository taskRepository; private final ElasticCaseRepository elasticCaseRepository; @@ -69,7 +69,7 @@ public ReindexingTask( IBulkService bulkService, MongoTemplate mongoTemplate, PetriNetService petriNetService, - @Value("${spring.data.elasticsearch.reindexExecutor.caseSize:20}") int pageSize, + @Value("${spring.data.elasticsearch.reindexExecutor.caseSize:20}") int caseBatchSize, @Value("${spring.data.elasticsearch.reindex-from:#{null}}") Duration from) { this.caseRepository = caseRepository; this.taskRepository = taskRepository; @@ -81,7 +81,7 @@ public ReindexingTask( this.workflowService = workflowService; this.mongoTemplate = mongoTemplate; this.petriNetService = petriNetService; - this.pageSize = pageSize; + this.caseBatchSize = caseBatchSize; this.bulkService = bulkService; lastRun = LocalDateTime.now(); @@ -95,7 +95,8 @@ public void reindex() { log.info("Reindexing stale cases: started reindexing after {}", lastRun); LocalDateTime now = LocalDateTime.now(); - BooleanExpression predicate = QCase.case$.lastModified.before(now).and(QCase.case$.lastModified.after(lastRun.minusMinutes(2))); + //BooleanExpression predicate = QCase.case$.lastModified.before(now).and(QCase.case$.lastModified.after(lastRun.minusMinutes(2))); + BooleanExpression predicate = QCase.case$.creationDate.isNotNull(); LocalDateTime lastRunOld = lastRun; lastRun = LocalDateTime.now(); @@ -108,21 +109,21 @@ public void reindex() { } private void reindexAllPages(long count, LocalDateTime now, LocalDateTime lastRunOld) { - long numOfPages = ((count / pageSize) + 1); + long numOfPages = ((count / caseBatchSize) + 1); log.info("Reindexing {} pages", numOfPages); Query query = new Query(); AtomicInteger page = new AtomicInteger(); - query.cursorBatchSize(pageSize); + query.cursorBatchSize(caseBatchSize); - query.addCriteria(Criteria.where("lastModified").lt(now).gt(lastRunOld.minusMinutes(2))); + //query.addCriteria(Criteria.where("lastModified").lt(now).gt(lastRunOld.minusMinutes(2))); - List batch = new ArrayList<>(pageSize); + List batch = new ArrayList<>(caseBatchSize); try (CloseableIterator cursor = mongoTemplate.stream(query, Case.class)) { cursor.stream().forEach(aCase -> { batch.add(aCase); - if (batch.size() == pageSize) { + if (batch.size() == caseBatchSize) { page.getAndIncrement(); log.info("Reindexing {} / {}", page, numOfPages); @@ -139,7 +140,8 @@ public void forceReindexPage(Predicate predicate, int page, long numOfPages) { } private void reindexCasesBatch(List casesBatch) { - List casesToIndex = casesBatch.stream().filter(it -> elasticCaseRepository.countByStringIdAndLastModified(it.getStringId(), Timestamp.valueOf(it.getLastModified()).getTime()) == 0).collect(Collectors.toList()); + //List casesToIndex = casesBatch.stream().filter(it -> elasticCaseRepository.countByStringIdAndLastModified(it.getStringId(), Timestamp.valueOf(it.getLastModified()).getTime()) == 0).collect(Collectors.toList()); + List casesToIndex = casesBatch; if (casesToIndex.isEmpty()) { log.info("No cases to reindex"); return; @@ -161,7 +163,7 @@ private void reindexCasesBatch(List casesBatch) { private void reindexPage(Predicate predicate, int page, long numOfPages, boolean forced) { log.info("Reindexing {} / {}", (page + 1), numOfPages); - Page cases = this.workflowService.search(predicate, PageRequest.of(page, pageSize)); + Page cases = this.workflowService.search(predicate, PageRequest.of(page, caseBatchSize)); for (Case aCase : cases) { if (forced || elasticCaseRepository.countByStringIdAndLastModified(aCase.getStringId(), Timestamp.valueOf(aCase.getLastModified()).getTime()) == 0) { From 01bea6037e8a5380fd0618b339cb24ad09c96dbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Voz=C3=A1r?= Date: Wed, 23 Jul 2025 11:43:56 +0200 Subject: [PATCH 20/92] NAE-2136 - ConcurrentModificationException prevention --- .../application/engine/elastic/service/BulkService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/BulkService.java b/src/main/java/com/netgrif/application/engine/elastic/service/BulkService.java index d19e239be3..e7742a559f 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/BulkService.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/BulkService.java @@ -136,8 +136,8 @@ private void executeAndValidate(List operations) { List left = operations.subList(0, mid); List right = operations.subList(mid, operations.size()); - executeAndValidate(left); - executeAndValidate(right); + executeAndValidate(new ArrayList<>(left)); + executeAndValidate(new ArrayList<>(right)); } } From 6ea44cc6c817b23a9f0482b7ec76793949f178e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Voz=C3=A1r?= Date: Wed, 23 Jul 2025 12:10:50 +0200 Subject: [PATCH 21/92] NAE-2136 - Remove unused inner class --- .../application/engine/elastic/domain/CaseField.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/elastic/domain/CaseField.java b/src/main/java/com/netgrif/application/engine/elastic/domain/CaseField.java index de13806117..e661a947d8 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/domain/CaseField.java +++ b/src/main/java/com/netgrif/application/engine/elastic/domain/CaseField.java @@ -1,6 +1,5 @@ package com.netgrif.application.engine.elastic.domain; -import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; @@ -22,10 +21,4 @@ public CaseField(List value) { super(value.toString()); this.caseValue = value; } - - @AllArgsConstructor - private static class FileNameAndExtension { - public String name; - public String extension; - } } From eaec9dc3047dffa2f69ee55b94844d499156635a Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Wed, 13 Aug 2025 09:52:38 +0200 Subject: [PATCH 22/92] **Enhance reindexing capabilities with bulk indexing improvements** - **Add bulk reindexing support**: - Introduced the `bulkIndex` method in `IElasticIndexService`, enabling reindexing of all or stale cases and tasks. - Created a dedicated `IndexParams` class to encapsulate parameters for the reindexing process (e.g., `indexAll`, `caseBatchSize`, `taskBatchSize`). - Optimized the index batching process for cases and tasks by leveraging configurable batch sizes. - **Remove deprecated methods**: - Removed the old `bulkIndex` method from `IElasticIndexService`. - Eliminated the obsolete `IBulkService` interface. - **Update ElasticController**: - Changed endpoint, `/reindex/bulk`, for initiating bulk reindexing with configurable parameters. - Updated controller logic to leverage the new `bulkIndex` method for improved performance and scalability. - **Configuration enhancements**: - Extended `ElasticsearchProperties` to include `IndexProperties`, allowing batch sizes for cases and tasks to be configured via properties. - **Code cleanup**: - Removed unused imports and annotated services with `@RequiredArgsConstructor`. - Streamlined reindexing tasks by integrating advanced filtering and improved logging. This commit enhances the maintainability and scalability of Elasticsearch reindexing while introducing configurability and improved documentation for reindexing processes. --- .../properties/ElasticsearchProperties.java | 14 + .../engine/elastic/service/BulkService.java | 184 ------------- .../elastic/service/ElasticIndexService.java | 244 ++++++++++++++++-- .../elastic/service/ReindexingTask.java | 108 ++------ .../service/interfaces/IBulkService.java | 11 - .../interfaces/IElasticIndexService.java | 6 +- .../engine/elastic/web/ElasticController.java | 26 +- .../web/requestbodies/IndexParams.java | 10 + .../engine/workflow/service/TaskService.java | 1 - 9 files changed, 277 insertions(+), 327 deletions(-) delete mode 100644 src/main/java/com/netgrif/application/engine/elastic/service/BulkService.java delete mode 100644 src/main/java/com/netgrif/application/engine/elastic/service/interfaces/IBulkService.java create mode 100644 src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/IndexParams.java diff --git a/src/main/java/com/netgrif/application/engine/configuration/properties/ElasticsearchProperties.java b/src/main/java/com/netgrif/application/engine/configuration/properties/ElasticsearchProperties.java index 4f77f278cb..6475cfbab0 100644 --- a/src/main/java/com/netgrif/application/engine/configuration/properties/ElasticsearchProperties.java +++ b/src/main/java/com/netgrif/application/engine/configuration/properties/ElasticsearchProperties.java @@ -1,6 +1,7 @@ package com.netgrif.application.engine.configuration.properties; import lombok.Data; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.core.io.Resource; import org.springframework.stereotype.Component; @@ -53,6 +54,8 @@ public class ElasticsearchProperties { private List defaultSearchFilters = new ArrayList<>(); + private IndexProperties indexProperties = new IndexProperties(); + @PostConstruct public void init() { indexSettings.putIfAbsent("max_result_window", 10000000); @@ -72,4 +75,15 @@ public void init() { public Map getClassSpecificSettings(String className) { return classSpecificIndexSettings.getOrDefault(className, new HashMap<>()); } + + @Data + public static class IndexProperties { + private String taskIndex; + + private String caseIndex; + + private int caseBatchSize; + + private int taskBatchSize; + } } diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/BulkService.java b/src/main/java/com/netgrif/application/engine/elastic/service/BulkService.java deleted file mode 100644 index e7742a559f..0000000000 --- a/src/main/java/com/netgrif/application/engine/elastic/service/BulkService.java +++ /dev/null @@ -1,184 +0,0 @@ -package com.netgrif.application.engine.elastic.service; - -import co.elastic.clients.elasticsearch.ElasticsearchClient; -import co.elastic.clients.elasticsearch.core.BulkRequest; -import co.elastic.clients.elasticsearch.core.BulkResponse; -import co.elastic.clients.elasticsearch.core.bulk.BulkOperation; -import com.netgrif.application.engine.elastic.domain.ElasticCase; -import com.netgrif.application.engine.elastic.domain.ElasticTask; -import com.netgrif.application.engine.elastic.service.interfaces.*; -import com.netgrif.application.engine.workflow.domain.Case; -import com.netgrif.application.engine.workflow.domain.Task; -import lombok.extern.slf4j.Slf4j; -import org.elasticsearch.ElasticsearchException; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; - -import java.time.LocalDateTime; -import java.util.*; - -/** - * Service responsible for bulk indexing of {@link Case} and {@link Task} entities into Elasticsearch. - * Uses transformation services to map domain objects to their corresponding Elastic representations. - * - * Indexing is performed using upsert operations. - */ -@Service -@Slf4j -public class BulkService implements IBulkService { - @Value("${spring.data.elasticsearch.index.task}") - private String taskIndex; - - @Value("${spring.data.elasticsearch.index.case}") - private String caseIndex; - - @Value("${spring.data.elasticsearch.reindexExecutor.caseSize:20}") - private int caseBatchSize; - - @Value("${spring.data.elasticsearch.reindexExecutor.taskSize:20}") - private int taskBatchSize; - - private final ElasticsearchClient esClient; - - private final IElasticCaseMappingService elasticCaseMappingService; - - private final IElasticTaskMappingService elasticTaskMappingService; - - - BulkService (@Qualifier("elasticsearchClient") ElasticsearchClient elasticsearchClient, - IElasticCaseMappingService elasticCaseMappingService, - IElasticTaskMappingService elasticTaskMappingService) { - this.esClient = elasticsearchClient; - this.elasticCaseMappingService = elasticCaseMappingService; - this.elasticTaskMappingService = elasticTaskMappingService; - } - - /** - * Performs bulk indexing of a list of {@link Case} objects into the Elasticsearch case index. - * Uses upsert semantics — if a document exists, it is updated; otherwise, it is created. - * - * @param cases the list of case entities to be indexed - */ - @Override - public void bulkIndexCases(List cases) { - List operations = new ArrayList<>(); - - for (Case c : cases) { - try { - if (c.getLastModified() == null) - c.setLastModified(LocalDateTime.now()); - - ElasticCase doc = elasticCaseMappingService.transform(c); - - operations.add(BulkOperation.of(op -> op - .update(u -> u - .index(caseIndex) - .id(doc.getStringId()) - .action(a -> a - .doc(doc) - .docAsUpsert(true) - ) - ))); - } catch (Exception e) { - log.error("Failed to prepare bulk operation for case [{}]: {}", c.getStringId(), e.getMessage()); - } - } - - executeAndValidate(operations); - } - - /** - * Performs bulk indexing of a list of {@link Task} objects into the Elasticsearch task index. - * Uses upsert semantics — if a document exists, it is updated; otherwise, it is created. - * - * @param tasks the list of task entities to be indexed - */ - @Override - public void bulkIndexTasks(List tasks) { - if (tasks == null || tasks.isEmpty()) return; - - int totalSize = tasks.size(); - - for (int i = 0; i < totalSize; i += taskBatchSize) { - int end = Math.min(i + taskBatchSize, totalSize); - List batch = tasks.subList(i, end); - - log.info("Reindexing task page {} / {}", i / taskBatchSize, totalSize / taskBatchSize); - - indexTaskBatch(batch); - } - } - - private void executeAndValidate(List operations) { - if (operations.isEmpty()) { - return; - } - - BulkRequest.Builder builder = new BulkRequest.Builder(); - builder.operations(operations); - - try { - BulkResponse response = esClient.bulk(builder.build()); - checkForBulkUpdateFailure(response); - log.info("Batch indexed successfully with {} ops", operations.size()); - } catch (Exception e) { - log.warn("Failed for {} ops to index bulk {}", operations.size(), e.getMessage(), e); - - if (operations.size() == 1) { - log.error("Single operation failed. Skipping. {}", operations.get(0), e); - return; - } - - log.warn("Dividing the requirement."); - - int mid = operations.size() / 2; - List left = operations.subList(0, mid); - List right = operations.subList(mid, operations.size()); - - executeAndValidate(new ArrayList<>(left)); - executeAndValidate(new ArrayList<>(right)); - } - } - - private void checkForBulkUpdateFailure(BulkResponse response) { - Map failedDocuments = new HashMap<>(); - - - response.items().forEach(item -> { - if (item.error() != null) { - failedDocuments.put(item.id(), item.error().reason()); - } - }); - - if (!failedDocuments.isEmpty()) { - throw new ElasticsearchException("Bulk indexing has failures. Use ElasticsearchException.getFailedDocuments() for details [{}]", failedDocuments); - } - } - - private void indexTaskBatch(List tasks) { - - List operations = new ArrayList<>(); - - for (Task task : tasks) { - try { - ElasticTask elasticTask = elasticTaskMappingService.transform(task); - - operations.add(BulkOperation.of(op -> op - .update(u -> u - .index(taskIndex) - .id(elasticTask.getStringId()) - .action(a -> a - .doc(elasticTask) - .docAsUpsert(true) - ) - )) - ); - } catch (Exception e) { - log.error("Failed to create upsert request for task [{}]: {}", task.getStringId(), e.getMessage()); - } - } - - executeAndValidate(operations); - } -} diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java index e4ff155a89..0b33a299f7 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java @@ -1,10 +1,24 @@ package com.netgrif.application.engine.elastic.service; +import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.elasticsearch.core.BulkRequest; +import co.elastic.clients.elasticsearch.core.BulkResponse; +import co.elastic.clients.elasticsearch.core.bulk.BulkOperation; import com.fasterxml.jackson.databind.ObjectMapper; import com.netgrif.application.engine.configuration.properties.ElasticsearchProperties; +import com.netgrif.application.engine.elastic.domain.ElasticCase; +import com.netgrif.application.engine.elastic.domain.ElasticTask; import com.netgrif.application.engine.elastic.service.interfaces.IElasticIndexService; +import com.netgrif.application.engine.petrinet.service.PetriNetService; +import com.netgrif.application.engine.workflow.domain.Case; +import com.netgrif.application.engine.workflow.domain.QCase; +import com.netgrif.application.engine.workflow.domain.Task; +import com.netgrif.application.engine.workflow.domain.repositories.CaseRepository; +import com.querydsl.core.types.Predicate; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.admin.indices.open.OpenIndexRequest; import org.elasticsearch.action.admin.indices.open.OpenIndexResponse; import org.elasticsearch.action.support.master.AcknowledgedResponse; @@ -13,7 +27,6 @@ import org.elasticsearch.client.indices.CloseIndexResponse; import org.elasticsearch.client.indices.PutIndexTemplateRequest; import org.elasticsearch.xcontent.XContentType; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.core.io.Resource; import org.springframework.data.annotation.Id; @@ -24,33 +37,46 @@ import org.springframework.data.elasticsearch.core.SearchScrollHits; import org.springframework.data.elasticsearch.core.document.Document; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; -import org.springframework.data.elasticsearch.core.query.IndexQuery; import org.springframework.data.elasticsearch.core.query.IndexQueryBuilder; import org.springframework.data.elasticsearch.core.query.Query; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.util.CloseableIterator; import org.springframework.stereotype.Service; import org.springframework.util.Assert; import java.io.InputStream; import java.lang.reflect.Field; +import java.time.LocalDateTime; import java.util.*; @Slf4j @Service +@RequiredArgsConstructor public class ElasticIndexService implements IElasticIndexService { private static final String PLACEHOLDERS = "petriNetIndex, caseIndex, taskIndex"; - @Autowired - private ApplicationContext context; + private final ApplicationContext context; - @Autowired - private ElasticsearchRestTemplate elasticsearchTemplate; + private final ElasticsearchRestTemplate elasticsearchTemplate; - @Autowired - private ElasticsearchOperations operations; + private final ElasticsearchClient elasticsearchClient; + + private final ElasticsearchOperations operations; + + private final ElasticsearchProperties elasticsearchProperties; + + private final CaseRepository caseRepository; + + private final PetriNetService petriNetService; + + private final MongoTemplate mongoTemplate; + + private final ElasticCaseMappingService caseMappingService; + + private final ElasticTaskMappingService taskMappingService; - @Autowired - private ElasticsearchProperties elasticsearchProperties; @Override public boolean indexExists(String indexName) { @@ -69,24 +95,6 @@ public String index(Class clazz, T source, String... placeholders) { .withObject(source).build(), IndexCoordinates.of(indexName)); } - - @Override - public boolean bulkIndex(List list, Class clazz, String... placeholders) { - String indexName = getIndexName(clazz, placeholders); - try { - if (list != null && !list.isEmpty()) { - List indexQueries = new ArrayList<>(); - list.forEach(source -> - indexQueries.add(new IndexQueryBuilder().withId(getIdFromSource(source)).withObject(source).build())); - elasticsearchTemplate.bulkIndex(indexQueries, IndexCoordinates.of(indexName)); - } - } catch (Exception e) { - log.error("bulkIndex:", e); - return false; - } - return true; - } - @Override public boolean createIndex(Class clazz, String... placeholders) { try { @@ -304,6 +312,186 @@ public void clearScrollHits(List scrollIds) { } } + @Override + public void bulkIndex(boolean indexAll, LocalDateTime after, Integer caseBatchSize, Integer taskBatchSize) { + log.info("Reindexing stale cases: started reindexing after {}", after); + LocalDateTime now = LocalDateTime.now(); + + if (caseBatchSize == null) { + caseBatchSize = elasticsearchProperties.getIndexProperties().getCaseBatchSize(); + } + if (taskBatchSize == null) { + taskBatchSize = elasticsearchProperties.getIndexProperties().getTaskBatchSize(); + } + + Predicate predicate; + if (indexAll || after == null) { + predicate = QCase.case$.lastModified.before(now); + log.info("Reindexing stale cases: force all"); + } else { + predicate = QCase.case$.lastModified.before(now).and(QCase.case$.lastModified.after(after.minusMinutes(2))); + } + + long count = caseRepository.count(predicate); + if (count > 0) { + reindexQueried(count, now, after, indexAll, caseBatchSize, taskBatchSize); + } + log.info("Reindexing stale cases: end"); + } + + private void reindexQueried(long count, LocalDateTime now, LocalDateTime after, boolean indexAll, int caseBatchSize, int taskBatchSize) { + long numOfPages = ((count / caseBatchSize) + 1); + log.info("Reindexing {} pages", numOfPages); + + org.springframework.data.mongodb.core.query.Query query; + if (indexAll) { + query = org.springframework.data.mongodb.core.query.Query.query(Criteria.where("lastModified").lt(now)); + } else { + query = org.springframework.data.mongodb.core.query.Query.query(Criteria.where("lastModified").lt(now).gt(after.minusMinutes(2))); + } + query.cursorBatchSize(caseBatchSize); + + long page = 1, currentBatchSize = 0; + List caseOperations = new ArrayList<>(); + List caseIds = new ArrayList<>(); + + try (CloseableIterator cursor = mongoTemplate.stream(query, Case.class)) { + while (cursor.hasNext()) { + Case aCase = cursor.next(); + prepareCase(aCase); + ElasticCase doc = caseMappingService.transform(aCase); + prepareCaseBulkOperation(doc, caseOperations); + caseIds.add(aCase.getStringId()); + + if (++currentBatchSize == caseBatchSize || !cursor.hasNext()) { + log.info("Reindexing case page {} / {}", page, numOfPages); + executeAndValidate(caseOperations); + bulkIndexTasks(caseIds, taskBatchSize); + caseOperations.clear(); + caseIds.clear(); + currentBatchSize = 0; + page++; + } + } + } + } + + private void bulkIndexTasks(List caseIds, int taskBatchSize) { + if (caseIds == null || caseIds.isEmpty()) { + return; + } + org.springframework.data.mongodb.core.query.Query query = org.springframework.data.mongodb.core.query.Query.query(Criteria.where("caseId").in(caseIds)).cursorBatchSize(taskBatchSize); + long totalSize = mongoTemplate.count(query, Task.class); + long numOfPages = ((totalSize / taskBatchSize) + 1); + + long page = 1, currentBatchSize = 0; + List taskOperations = new ArrayList<>(); + + try (CloseableIterator cursor = mongoTemplate.stream(query, Task.class)) { + while (cursor.hasNext()) { + Task task = cursor.next(); + ElasticTask elasticTask = taskMappingService.transform(task); + prepareTaskBulkOperation(elasticTask, taskOperations); + + if (++currentBatchSize == taskBatchSize || !cursor.hasNext()) { + log.info("Reindexing task page {} / {}", page, numOfPages); + executeAndValidate(taskOperations); + taskOperations.clear(); + currentBatchSize = 0; + page++; + } + } + } + } + + private void prepareCase(Case useCase) { + if (useCase.getPetriNet() == null) { + useCase.setPetriNet(petriNetService.get(useCase.getPetriNetObjectId())); + } + if (useCase.getLastModified() == null) { + useCase.setLastModified(LocalDateTime.now()); + } + } + + private void prepareCaseBulkOperation(ElasticCase doc, List operations) { + try { + operations.add(BulkOperation.of(op -> op + .update(u -> u + .index(elasticsearchProperties.getIndexProperties().getCaseIndex()) + .id(doc.getStringId()) + .action(a -> a + .doc(doc) + .docAsUpsert(true) + ) + ))); + } catch (Exception e) { + log.error("Failed to prepare bulk operation for case [{}]: {}", doc.getStringId(), e.getMessage()); + } + } + + private void prepareTaskBulkOperation(ElasticTask doc, List operations) { + try { + operations.add(BulkOperation.of(op -> op + .update(u -> u + .index(elasticsearchProperties.getIndexProperties().getTaskIndex()) + .id(doc.getStringId()) + .action(a -> a + .doc(doc) + .docAsUpsert(true) + ) + )) + ); + } catch (Exception e) { + log.error("Failed to prepare bulk operation for task [{}]: {}", doc.getStringId(), e.getMessage()); + } + } + + private void executeAndValidate(List operations) { + if (operations.isEmpty()) { + return; + } + + BulkRequest.Builder builder = new BulkRequest.Builder(); + builder.operations(operations); + + try { + BulkResponse response = elasticsearchClient.bulk(builder.build()); + checkForBulkUpdateFailure(response); + log.info("Batch indexed successfully with {} ops", operations.size()); + } catch (ElasticsearchException e) { + log.warn("Failed for {} ops to index bulk {}", operations.size(), e.getMessage(), e); + + if (operations.size() == 1) { + log.error("Single operation failed. Skipping. {}", operations.get(0), e); + return; + } + + log.warn("Dividing the requirement."); + + int mid = operations.size() / 2; + List left = operations.subList(0, mid); + List right = operations.subList(mid, operations.size()); + + executeAndValidate(new ArrayList<>(left)); + executeAndValidate(new ArrayList<>(right)); + } catch (Exception e) { + log.error("Failed to index bulk: {}", e.getMessage(), e); + } + } + + private void checkForBulkUpdateFailure(BulkResponse response) { + Map failedDocuments = new HashMap<>(); + response.items().forEach(item -> { + if (item.error() != null) { + failedDocuments.put(item.id(), item.error().reason()); + } + }); + + if (!failedDocuments.isEmpty()) { + throw new ElasticsearchException("Bulk indexing has failures. Use ElasticsearchException.getFailedDocuments() for details [{}]", failedDocuments); + } + } + private String getIdFromSource(Object source) { if (source == null) { return null; diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java b/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java index ffcb0489fd..5a54538164 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java @@ -2,7 +2,6 @@ import com.netgrif.application.engine.elastic.domain.ElasticCaseRepository; import com.netgrif.application.engine.elastic.service.interfaces.*; -import com.netgrif.application.engine.petrinet.service.PetriNetService; import com.netgrif.application.engine.workflow.domain.Case; import com.netgrif.application.engine.workflow.domain.QCase; import com.netgrif.application.engine.workflow.domain.Task; @@ -19,20 +18,13 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; -import org.springframework.data.mongodb.core.MongoTemplate; -import org.springframework.data.mongodb.core.query.Criteria; -import org.springframework.data.mongodb.core.query.Query; -import org.springframework.data.util.CloseableIterator; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.sql.Timestamp; import java.time.Duration; import java.time.LocalDateTime; -import java.util.ArrayList; import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Collectors; @Component @ConditionalOnExpression("'${spring.data.elasticsearch.reindex}'!= 'null'") @@ -40,19 +32,18 @@ public class ReindexingTask { private static final Logger log = LoggerFactory.getLogger(ReindexingTask.class); - private final int caseBatchSize; - private final CaseRepository caseRepository; - private final TaskRepository taskRepository; - private final ElasticCaseRepository elasticCaseRepository; - private final IElasticCaseService elasticCaseService; - private final IElasticTaskService elasticTaskService; - private final IElasticCaseMappingService caseMappingService; - private final IElasticTaskMappingService taskMappingService; - private final IWorkflowService workflowService; - private final MongoTemplate mongoTemplate; - private final PetriNetService petriNetService; + private int pageSize; + private CaseRepository caseRepository; + private TaskRepository taskRepository; + private ElasticCaseRepository elasticCaseRepository; + private IElasticCaseService elasticCaseService; + private IElasticTaskService elasticTaskService; + private IElasticCaseMappingService caseMappingService; + private IElasticTaskMappingService taskMappingService; + private IWorkflowService workflowService; + private IElasticIndexService elasticIndexService; + private LocalDateTime lastRun; - private final IBulkService bulkService; @Autowired public ReindexingTask( @@ -66,10 +57,7 @@ public ReindexingTask( IElasticCaseMappingService caseMappingService, IElasticTaskMappingService taskMappingService, IWorkflowService workflowService, - IBulkService bulkService, - MongoTemplate mongoTemplate, - PetriNetService petriNetService, - @Value("${spring.data.elasticsearch.reindexExecutor.caseSize:20}") int caseBatchSize, + @Value("${spring.data.elasticsearch.reindexExecutor.size:20}") int pageSize, @Value("${spring.data.elasticsearch.reindex-from:#{null}}") Duration from) { this.caseRepository = caseRepository; this.taskRepository = taskRepository; @@ -79,10 +67,7 @@ public ReindexingTask( this.caseMappingService = caseMappingService; this.taskMappingService = taskMappingService; this.workflowService = workflowService; - this.mongoTemplate = mongoTemplate; - this.petriNetService = petriNetService; - this.caseBatchSize = caseBatchSize; - this.bulkService = bulkService; + this.pageSize = pageSize; lastRun = LocalDateTime.now(); if (from != null) { @@ -92,78 +77,19 @@ public ReindexingTask( @Scheduled(cron = "#{springElasticsearchReindex}") public void reindex() { - log.info("Reindexing stale cases: started reindexing after {}", lastRun); - - LocalDateTime now = LocalDateTime.now(); - //BooleanExpression predicate = QCase.case$.lastModified.before(now).and(QCase.case$.lastModified.after(lastRun.minusMinutes(2))); - BooleanExpression predicate = QCase.case$.creationDate.isNotNull(); - LocalDateTime lastRunOld = lastRun; + log.info("Reindexing stale cases: started reindexing after " + lastRun); + elasticIndexService.bulkIndex(false, lastRun, null, null); lastRun = LocalDateTime.now(); - - long count = caseRepository.count(predicate); - if (count > 0) { - reindexAllPages(count, now, lastRunOld); - } - log.info("Reindexing stale cases: end"); } - private void reindexAllPages(long count, LocalDateTime now, LocalDateTime lastRunOld) { - long numOfPages = ((count / caseBatchSize) + 1); - log.info("Reindexing {} pages", numOfPages); - Query query = new Query(); - AtomicInteger page = new AtomicInteger(); - - query.cursorBatchSize(caseBatchSize); - - //query.addCriteria(Criteria.where("lastModified").lt(now).gt(lastRunOld.minusMinutes(2))); - - List batch = new ArrayList<>(caseBatchSize); - try (CloseableIterator cursor = mongoTemplate.stream(query, Case.class)) { - cursor.stream().forEach(aCase -> { - batch.add(aCase); - - if (batch.size() == caseBatchSize) { - page.getAndIncrement(); - log.info("Reindexing {} / {}", page, numOfPages); - - reindexCasesBatch(batch); - batch.clear(); - } - - }); - } - } - public void forceReindexPage(Predicate predicate, int page, long numOfPages) { reindexPage(predicate, page, numOfPages, true); } - private void reindexCasesBatch(List casesBatch) { - //List casesToIndex = casesBatch.stream().filter(it -> elasticCaseRepository.countByStringIdAndLastModified(it.getStringId(), Timestamp.valueOf(it.getLastModified()).getTime()) == 0).collect(Collectors.toList()); - List casesToIndex = casesBatch; - if (casesToIndex.isEmpty()) { - log.info("No cases to reindex"); - return; - } - - casesToIndex.forEach(c -> { - if (c.getPetriNet() == null) { - c.setPetriNet(petriNetService.get(c.getPetriNetObjectId())); - } - }); - - bulkService.bulkIndexCases(casesToIndex); - - List caseIds = casesToIndex.stream().map(Case::getStringId).collect(Collectors.toList()); - List tasksToReindex = taskRepository.findAllByCaseIdIn(caseIds); - - bulkService.bulkIndexTasks(tasksToReindex); - } - private void reindexPage(Predicate predicate, int page, long numOfPages, boolean forced) { - log.info("Reindexing {} / {}", (page + 1), numOfPages); - Page cases = this.workflowService.search(predicate, PageRequest.of(page, caseBatchSize)); + log.info("Reindexing " + (page + 1) + " / " + numOfPages); + Page cases = this.workflowService.search(predicate, PageRequest.of(page, pageSize)); for (Case aCase : cases) { if (forced || elasticCaseRepository.countByStringIdAndLastModified(aCase.getStringId(), Timestamp.valueOf(aCase.getLastModified()).getTime()) == 0) { diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/interfaces/IBulkService.java b/src/main/java/com/netgrif/application/engine/elastic/service/interfaces/IBulkService.java deleted file mode 100644 index 3dfaf6fd5d..0000000000 --- a/src/main/java/com/netgrif/application/engine/elastic/service/interfaces/IBulkService.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.netgrif.application.engine.elastic.service.interfaces; - -import com.netgrif.application.engine.workflow.domain.Case; -import com.netgrif.application.engine.workflow.domain.Task; - -import java.util.List; - -public interface IBulkService { - void bulkIndexCases(List cases); - void bulkIndexTasks(List tasks); -} \ No newline at end of file diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/interfaces/IElasticIndexService.java b/src/main/java/com/netgrif/application/engine/elastic/service/interfaces/IElasticIndexService.java index 5660a6db47..f23036457b 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/interfaces/IElasticIndexService.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/interfaces/IElasticIndexService.java @@ -1,10 +1,12 @@ package com.netgrif.application.engine.elastic.service.interfaces; +import com.querydsl.core.types.Predicate; import org.springframework.data.elasticsearch.core.SearchHits; import org.springframework.data.elasticsearch.core.SearchScrollHits; import org.springframework.data.elasticsearch.core.document.Document; import org.springframework.data.elasticsearch.core.query.Query; +import java.time.LocalDateTime; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -31,8 +33,6 @@ public interface IElasticIndexService { String index(Class clazz, T source, String... placeholders); - boolean bulkIndex(List list, Class clazz, String... placeholders); - SearchScrollHits scrollFirst(Query query, Class clazz, String... placeholders); SearchScrollHits scroll(String scrollId, Class clazz, String... placeholders); @@ -42,4 +42,6 @@ public interface IElasticIndexService { void applySettings(HashMap settingMap, Class clazz); void clearScrollHits(List scrollIds); + + void bulkIndex(boolean indexAll, LocalDateTime lastRun, Integer caseBatchSize, Integer taskBatchSize); } diff --git a/src/main/java/com/netgrif/application/engine/elastic/web/ElasticController.java b/src/main/java/com/netgrif/application/engine/elastic/web/ElasticController.java index 436469bb0e..96c0318466 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/web/ElasticController.java +++ b/src/main/java/com/netgrif/application/engine/elastic/web/ElasticController.java @@ -2,6 +2,8 @@ import com.netgrif.application.engine.auth.domain.LoggedUser; import com.netgrif.application.engine.elastic.service.ReindexingTask; +import com.netgrif.application.engine.elastic.service.interfaces.IElasticIndexService; +import com.netgrif.application.engine.elastic.web.requestbodies.IndexParams; import com.netgrif.application.engine.workflow.service.CaseSearchService; import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService; import com.netgrif.application.engine.workflow.web.responsebodies.MessageResource; @@ -20,10 +22,7 @@ import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.Authentication; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import java.util.Locale; import java.util.Map; @@ -49,6 +48,9 @@ public class ElasticController { @Autowired private ReindexingTask reindexingTask; + @Autowired + private IElasticIndexService indexService; + @Value("${spring.data.elasticsearch.reindexExecutor.size:20}") private int pageSize; @@ -86,15 +88,19 @@ public MessageResource reindex(@RequestBody Map searchBody, Auth } } - //@PreAuthorize("hasRole('ADMIN')") @PreAuthorize("@authorizationService.hasAuthority('ADMIN')") - @PostMapping(value = "/index/cursor", produces = MediaType.APPLICATION_JSON_VALUE) - public MessageResource cursorAllReindex() { + @Operation(summary = "Reindex all or stale cases with bulk index", + description = "Reindex all or stale cases (specified by IndexParams.indexAll param) with bulk index. Caller must have the ADMIN role", + security = {@SecurityRequirement(name = "BasicAuth")}) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "OK"), + @ApiResponse(responseCode = "403", description = "Caller doesn't fulfill the authorisation requirements"), + }) + @PostMapping(value = "/reindex/bulk", produces = MediaType.APPLICATION_JSON_VALUE) + public MessageResource bulkIndex(@RequestParam(required = false) IndexParams indexParams) { try { - - reindexingTask.reindex(); + indexService.bulkIndex(indexParams.isIndexAll(), null, indexParams.getCaseBatchSize(), indexParams.getTaskBatchSize()); return MessageResource.successMessage("Success"); - } catch (Exception e) { log.error("Could not index: ", e); return MessageResource.errorMessage(e.getMessage()); diff --git a/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/IndexParams.java b/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/IndexParams.java new file mode 100644 index 0000000000..d63e62ae06 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/IndexParams.java @@ -0,0 +1,10 @@ +package com.netgrif.application.engine.elastic.web.requestbodies; + +import lombok.Data; + +@Data +public class IndexParams { + private boolean indexAll = false; + private Integer caseBatchSize = 5000; + private Integer taskBatchSize = 20000; +} diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/TaskService.java b/src/main/java/com/netgrif/application/engine/workflow/service/TaskService.java index 2a045ffad9..bde3ec1fad 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/TaskService.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/TaskService.java @@ -36,7 +36,6 @@ import com.netgrif.application.engine.workflow.domain.eventoutcomes.dataoutcomes.SetDataEventOutcome; import com.netgrif.application.engine.workflow.domain.eventoutcomes.taskoutcomes.*; import com.netgrif.application.engine.workflow.domain.repositories.TaskRepository; -import com.netgrif.application.engine.workflow.domain.triggers.AutoTrigger; import com.netgrif.application.engine.workflow.domain.triggers.TimeTrigger; import com.netgrif.application.engine.workflow.domain.triggers.Trigger; import com.netgrif.application.engine.workflow.service.interfaces.IDataService; From f8817945a5a2cf04e23bffc5808e3304a1d74a74 Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Wed, 13 Aug 2025 10:16:26 +0200 Subject: [PATCH 23/92] - updated index resolution - updated request param --- .../properties/ElasticsearchProperties.java | 9 ++------- .../engine/elastic/service/ElasticIndexService.java | 12 ++++++------ .../engine/elastic/web/ElasticController.java | 2 +- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/configuration/properties/ElasticsearchProperties.java b/src/main/java/com/netgrif/application/engine/configuration/properties/ElasticsearchProperties.java index 6475cfbab0..4e4b213134 100644 --- a/src/main/java/com/netgrif/application/engine/configuration/properties/ElasticsearchProperties.java +++ b/src/main/java/com/netgrif/application/engine/configuration/properties/ElasticsearchProperties.java @@ -1,7 +1,6 @@ package com.netgrif.application.engine.configuration.properties; import lombok.Data; -import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.core.io.Resource; import org.springframework.stereotype.Component; @@ -54,7 +53,7 @@ public class ElasticsearchProperties { private List defaultSearchFilters = new ArrayList<>(); - private IndexProperties indexProperties = new IndexProperties(); + private BatchProperties batch = new BatchProperties(); @PostConstruct public void init() { @@ -77,11 +76,7 @@ public Map getClassSpecificSettings(String className) { } @Data - public static class IndexProperties { - private String taskIndex; - - private String caseIndex; - + public static class BatchProperties { private int caseBatchSize; private int taskBatchSize; diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java index 0b33a299f7..687a65f837 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java @@ -15,7 +15,7 @@ import com.netgrif.application.engine.workflow.domain.QCase; import com.netgrif.application.engine.workflow.domain.Task; import com.netgrif.application.engine.workflow.domain.repositories.CaseRepository; -import com.querydsl.core.types.Predicate; +import com.querydsl.core.types.dsl.BooleanExpression; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.elasticsearch.ElasticsearchException; @@ -318,13 +318,13 @@ public void bulkIndex(boolean indexAll, LocalDateTime after, Integer caseBatchSi LocalDateTime now = LocalDateTime.now(); if (caseBatchSize == null) { - caseBatchSize = elasticsearchProperties.getIndexProperties().getCaseBatchSize(); + caseBatchSize = elasticsearchProperties.getBatch().getCaseBatchSize(); } if (taskBatchSize == null) { - taskBatchSize = elasticsearchProperties.getIndexProperties().getTaskBatchSize(); + taskBatchSize = elasticsearchProperties.getBatch().getTaskBatchSize(); } - Predicate predicate; + BooleanExpression predicate; if (indexAll || after == null) { predicate = QCase.case$.lastModified.before(now); log.info("Reindexing stale cases: force all"); @@ -417,7 +417,7 @@ private void prepareCaseBulkOperation(ElasticCase doc, List opera try { operations.add(BulkOperation.of(op -> op .update(u -> u - .index(elasticsearchProperties.getIndexProperties().getCaseIndex()) + .index(elasticsearchProperties.getIndex().get("case")) .id(doc.getStringId()) .action(a -> a .doc(doc) @@ -433,7 +433,7 @@ private void prepareTaskBulkOperation(ElasticTask doc, List opera try { operations.add(BulkOperation.of(op -> op .update(u -> u - .index(elasticsearchProperties.getIndexProperties().getTaskIndex()) + .index(elasticsearchProperties.getIndex().get("task")) .id(doc.getStringId()) .action(a -> a .doc(doc) diff --git a/src/main/java/com/netgrif/application/engine/elastic/web/ElasticController.java b/src/main/java/com/netgrif/application/engine/elastic/web/ElasticController.java index 96c0318466..f9049f190a 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/web/ElasticController.java +++ b/src/main/java/com/netgrif/application/engine/elastic/web/ElasticController.java @@ -97,7 +97,7 @@ public MessageResource reindex(@RequestBody Map searchBody, Auth @ApiResponse(responseCode = "403", description = "Caller doesn't fulfill the authorisation requirements"), }) @PostMapping(value = "/reindex/bulk", produces = MediaType.APPLICATION_JSON_VALUE) - public MessageResource bulkIndex(@RequestParam(required = false) IndexParams indexParams) { + public MessageResource bulkIndex(IndexParams indexParams) { try { indexService.bulkIndex(indexParams.isIndexAll(), null, indexParams.getCaseBatchSize(), indexParams.getTaskBatchSize()); return MessageResource.successMessage("Success"); From 2e124b191d5308ebe506bc4f17d114c38fac3506 Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Wed, 13 Aug 2025 10:25:22 +0200 Subject: [PATCH 24/92] Add Javadoc comments to Elasticsearch indexing methods Added detailed Javadoc comments to improve the clarity of key methods and fields in `ElasticIndexService` and `IndexParams`. This documentation provides insights into parameter usage, default values, and functionalities, facilitating better understanding and maintenance. --- .../elastic/service/ElasticIndexService.java | 53 +++++++++++++++++++ .../web/requestbodies/IndexParams.java | 18 +++++++ 2 files changed, 71 insertions(+) diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java index 687a65f837..7352638202 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java @@ -312,6 +312,15 @@ public void clearScrollHits(List scrollIds) { } } + + /** + * Performs bulk indexing of cases and tasks into Elasticsearch. + * + * @param indexAll if true, indexes all cases and tasks, regardless of modification time + * @param after the time after which cases and tasks should be considered for reindexing + * @param caseBatchSize number of cases to process per batch. If null, defaults from Elasticsearch properties + * @param taskBatchSize number of tasks to process per batch. If null, defaults from Elasticsearch properties + */ @Override public void bulkIndex(boolean indexAll, LocalDateTime after, Integer caseBatchSize, Integer taskBatchSize) { log.info("Reindexing stale cases: started reindexing after {}", after); @@ -339,6 +348,16 @@ public void bulkIndex(boolean indexAll, LocalDateTime after, Integer caseBatchSi log.info("Reindexing stale cases: end"); } + /** + * Reindexes queried cases and tasks into Elasticsearch in batches. + * + * @param count total number of cases to reindex + * @param now current timestamp for filtering cases + * @param after reindexing cases modified after this time + * @param indexAll when true, reindexes all cases + * @param caseBatchSize batch size for cases + * @param taskBatchSize batch size for tasks + */ private void reindexQueried(long count, LocalDateTime now, LocalDateTime after, boolean indexAll, int caseBatchSize, int taskBatchSize) { long numOfPages = ((count / caseBatchSize) + 1); log.info("Reindexing {} pages", numOfPages); @@ -376,6 +395,12 @@ private void reindexQueried(long count, LocalDateTime now, LocalDateTime after, } } + /** + * Reindexes tasks into Elasticsearch in batches corresponding to the provided case IDs. + * + * @param caseIds list of case IDs whose tasks need to be reindexed + * @param taskBatchSize size of the batch for tasks + */ private void bulkIndexTasks(List caseIds, int taskBatchSize) { if (caseIds == null || caseIds.isEmpty()) { return; @@ -404,6 +429,11 @@ private void bulkIndexTasks(List caseIds, int taskBatchSize) { } } + /** + * Prepares the case object by ensuring necessary dependencies and last modified timestamp are set. + * + * @param useCase case object to prepare + */ private void prepareCase(Case useCase) { if (useCase.getPetriNet() == null) { useCase.setPetriNet(petriNetService.get(useCase.getPetriNetObjectId())); @@ -413,6 +443,12 @@ private void prepareCase(Case useCase) { } } + /** + * Prepares a bulk operation for indexing or updating a case in Elasticsearch. + * + * @param doc transformed ElasticCase object + * @param operations collection of BulkOperations to add this operation to + */ private void prepareCaseBulkOperation(ElasticCase doc, List operations) { try { operations.add(BulkOperation.of(op -> op @@ -429,6 +465,12 @@ private void prepareCaseBulkOperation(ElasticCase doc, List opera } } + /** + * Prepares a bulk operation for indexing or updating a task in Elasticsearch. + * + * @param doc transformed ElasticTask object + * @param operations collection of BulkOperations to add this operation to + */ private void prepareTaskBulkOperation(ElasticTask doc, List operations) { try { operations.add(BulkOperation.of(op -> op @@ -446,6 +488,11 @@ private void prepareTaskBulkOperation(ElasticTask doc, List opera } } + /** + * Executes the bulk operations and validates the results, retrying on partial failures. + * + * @param operations list of bulk operations to execute + */ private void executeAndValidate(List operations) { if (operations.isEmpty()) { return; @@ -479,6 +526,12 @@ private void executeAndValidate(List operations) { } } + /** + * Checks the results of a bulk indexing operation for failures. + * + * @param response the BulkResponse from Elasticsearch + * @throws ElasticsearchException if there are failures in the bulk response + */ private void checkForBulkUpdateFailure(BulkResponse response) { Map failedDocuments = new HashMap<>(); response.items().forEach(item -> { diff --git a/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/IndexParams.java b/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/IndexParams.java index d63e62ae06..9e3adab68c 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/IndexParams.java +++ b/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/IndexParams.java @@ -2,9 +2,27 @@ import lombok.Data; + +/** + * Represents the parameters to configure the indexing operation. + * This class allows customization of batch sizes for cases and tasks, + * as well as the option to index all data. + */ @Data public class IndexParams { + + /** + * Determines whether to index all available data. Default is {@code false}. + */ private boolean indexAll = false; + + /** + * Specifies the batch size for cases during indexing. Default is {@code 5000}. + */ private Integer caseBatchSize = 5000; + + /** + * Specifies the batch size for tasks during indexing. Default is {@code 20000}. + */ private Integer taskBatchSize = 20000; } From 692610ab4ac6e17109a2a65968ed6b5eb87d362b Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Wed, 13 Aug 2025 11:28:25 +0200 Subject: [PATCH 25/92] Update repositories and QRGen dependency in pom.xml Enable and configure JitPack repository for dependency resolution. Update QRGen dependency to version 3.0.1 with updated groupId and artifactId for compatibility. --- pom.xml | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index bdc8ce6246..574550d8ac 100644 --- a/pom.xml +++ b/pom.xml @@ -66,7 +66,7 @@ https://sonarcloud.io - + @@ -90,7 +90,17 @@ - + + jitpack.io + https://jitpack.io + + true + + + false + + + @@ -355,9 +365,9 @@ - com.github.kenglxn.qrgen - javase - 2.6.0 + com.github.kenglxn + QRGen + 3.0.1 From 4762d94cf8e4b4b4dc94e1d26f5179a9acac226b Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Wed, 13 Aug 2025 11:39:02 +0200 Subject: [PATCH 26/92] Refactor reindex logic to use MongoDB queries. Updated the reindexing logic to replace BooleanExpression predicates with MongoDB query objects, improving clarity and alignment with MongoDB operations. Adjusted method signatures and internal calls to support the new query-based approach. Renamed `bulkIndex` to `bulkReindex` for consistency with functionality. --- .../elastic/service/ElasticIndexService.java | 19 ++++++------------- .../engine/elastic/web/ElasticController.java | 2 +- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java index 7352638202..9eef9ba552 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java @@ -333,17 +333,17 @@ public void bulkIndex(boolean indexAll, LocalDateTime after, Integer caseBatchSi taskBatchSize = elasticsearchProperties.getBatch().getTaskBatchSize(); } - BooleanExpression predicate; + org.springframework.data.mongodb.core.query.Query query; if (indexAll || after == null) { - predicate = QCase.case$.lastModified.before(now); + query = org.springframework.data.mongodb.core.query.Query.query(Criteria.where("lastModified").lt(now)); log.info("Reindexing stale cases: force all"); } else { - predicate = QCase.case$.lastModified.before(now).and(QCase.case$.lastModified.after(after.minusMinutes(2))); + query = org.springframework.data.mongodb.core.query.Query.query(Criteria.where("lastModified").lt(now).gt(after.minusMinutes(2))); } - long count = caseRepository.count(predicate); + long count = mongoTemplate.count(query, Case.class); if (count > 0) { - reindexQueried(count, now, after, indexAll, caseBatchSize, taskBatchSize); + reindexQueried(query, count, now, after, indexAll, caseBatchSize, taskBatchSize); } log.info("Reindexing stale cases: end"); } @@ -358,18 +358,11 @@ public void bulkIndex(boolean indexAll, LocalDateTime after, Integer caseBatchSi * @param caseBatchSize batch size for cases * @param taskBatchSize batch size for tasks */ - private void reindexQueried(long count, LocalDateTime now, LocalDateTime after, boolean indexAll, int caseBatchSize, int taskBatchSize) { + private void reindexQueried(org.springframework.data.mongodb.core.query.Query query, long count, LocalDateTime now, LocalDateTime after, boolean indexAll, int caseBatchSize, int taskBatchSize) { long numOfPages = ((count / caseBatchSize) + 1); log.info("Reindexing {} pages", numOfPages); - org.springframework.data.mongodb.core.query.Query query; - if (indexAll) { - query = org.springframework.data.mongodb.core.query.Query.query(Criteria.where("lastModified").lt(now)); - } else { - query = org.springframework.data.mongodb.core.query.Query.query(Criteria.where("lastModified").lt(now).gt(after.minusMinutes(2))); - } query.cursorBatchSize(caseBatchSize); - long page = 1, currentBatchSize = 0; List caseOperations = new ArrayList<>(); List caseIds = new ArrayList<>(); diff --git a/src/main/java/com/netgrif/application/engine/elastic/web/ElasticController.java b/src/main/java/com/netgrif/application/engine/elastic/web/ElasticController.java index f9049f190a..c50c37df03 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/web/ElasticController.java +++ b/src/main/java/com/netgrif/application/engine/elastic/web/ElasticController.java @@ -97,7 +97,7 @@ public MessageResource reindex(@RequestBody Map searchBody, Auth @ApiResponse(responseCode = "403", description = "Caller doesn't fulfill the authorisation requirements"), }) @PostMapping(value = "/reindex/bulk", produces = MediaType.APPLICATION_JSON_VALUE) - public MessageResource bulkIndex(IndexParams indexParams) { + public MessageResource bulkReindex(IndexParams indexParams) { try { indexService.bulkIndex(indexParams.isIndexAll(), null, indexParams.getCaseBatchSize(), indexParams.getTaskBatchSize()); return MessageResource.successMessage("Success"); From 12f50fc0ce60791e6058d2983bd07b2f1afac860 Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Wed, 13 Aug 2025 11:41:28 +0200 Subject: [PATCH 27/92] Refactor reindexQueried method to simplify parameters Removed unused parameters `now`, `after`, and `indexAll` from the `reindexQueried` method and its invocation. This streamlines the method signature and improves clarity by reducing unnecessary complexity. --- .../engine/elastic/service/ElasticIndexService.java | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java index 9eef9ba552..a3d039d0ec 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java @@ -12,10 +12,8 @@ import com.netgrif.application.engine.elastic.service.interfaces.IElasticIndexService; import com.netgrif.application.engine.petrinet.service.PetriNetService; import com.netgrif.application.engine.workflow.domain.Case; -import com.netgrif.application.engine.workflow.domain.QCase; import com.netgrif.application.engine.workflow.domain.Task; import com.netgrif.application.engine.workflow.domain.repositories.CaseRepository; -import com.querydsl.core.types.dsl.BooleanExpression; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.elasticsearch.ElasticsearchException; @@ -343,7 +341,7 @@ public void bulkIndex(boolean indexAll, LocalDateTime after, Integer caseBatchSi long count = mongoTemplate.count(query, Case.class); if (count > 0) { - reindexQueried(query, count, now, after, indexAll, caseBatchSize, taskBatchSize); + reindexQueried(query, count, caseBatchSize, taskBatchSize); } log.info("Reindexing stale cases: end"); } @@ -352,13 +350,10 @@ public void bulkIndex(boolean indexAll, LocalDateTime after, Integer caseBatchSi * Reindexes queried cases and tasks into Elasticsearch in batches. * * @param count total number of cases to reindex - * @param now current timestamp for filtering cases - * @param after reindexing cases modified after this time - * @param indexAll when true, reindexes all cases * @param caseBatchSize batch size for cases * @param taskBatchSize batch size for tasks */ - private void reindexQueried(org.springframework.data.mongodb.core.query.Query query, long count, LocalDateTime now, LocalDateTime after, boolean indexAll, int caseBatchSize, int taskBatchSize) { + private void reindexQueried(org.springframework.data.mongodb.core.query.Query query, long count, int caseBatchSize, int taskBatchSize) { long numOfPages = ((count / caseBatchSize) + 1); log.info("Reindexing {} pages", numOfPages); From 668dc94f61dfceaff44b5a2f519d975e0b82d7c1 Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Wed, 13 Aug 2025 12:13:57 +0200 Subject: [PATCH 28/92] Add @Min validation to batch size properties Introduce the @Min annotation to enforce a minimum value of 1 for `caseBatchSize` and `taskBatchSize` in `BatchProperties`. This ensures valid configurations and prevents potential errors caused by invalid or zero values. --- .../configuration/properties/ElasticsearchProperties.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/com/netgrif/application/engine/configuration/properties/ElasticsearchProperties.java b/src/main/java/com/netgrif/application/engine/configuration/properties/ElasticsearchProperties.java index 4e4b213134..5084cbf5b6 100644 --- a/src/main/java/com/netgrif/application/engine/configuration/properties/ElasticsearchProperties.java +++ b/src/main/java/com/netgrif/application/engine/configuration/properties/ElasticsearchProperties.java @@ -6,6 +6,7 @@ import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; +import javax.validation.constraints.Min; import java.time.Duration; import java.util.ArrayList; import java.util.HashMap; @@ -77,8 +78,10 @@ public Map getClassSpecificSettings(String className) { @Data public static class BatchProperties { + @Min(1) private int caseBatchSize; + @Min(1) private int taskBatchSize; } } From faa66e28300e548b1feab912ecea5b7461b23767 Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Wed, 13 Aug 2025 12:21:04 +0200 Subject: [PATCH 29/92] Remove unused QRGen dependency from pom.xml The QRGen library is no longer required and has been removed to improve project maintainability. This cleanup helps reduce redundancies and ensures only necessary dependencies are included. --- pom.xml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/pom.xml b/pom.xml index 574550d8ac..9fb08fb81b 100644 --- a/pom.xml +++ b/pom.xml @@ -362,7 +362,6 @@ ${querydsl.version} - com.github.kenglxn @@ -418,13 +417,6 @@ ${drools.version} - - - net.glxn.qrgen - core - 2.0 - - org.apache.commons commons-lang3 From 62ee1e5f04544c3d2a71152f4e565dc97ffd415b Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Wed, 13 Aug 2025 12:26:56 +0200 Subject: [PATCH 30/92] Use property for Jackson version management Centralized the Jackson version using the `` property to ensure consistency across dependencies. This improves maintainability by avoiding hardcoded version references. --- pom.xml | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/pom.xml b/pom.xml index 9fb08fb81b..06357d4018 100644 --- a/pom.xml +++ b/pom.xml @@ -64,6 +64,7 @@ 7.70.0.Final netgrif-oss https://sonarcloud.io + 2.15.0-rc1 @@ -379,6 +380,7 @@ com.fasterxml.jackson.datatype jackson-datatype-jsr310 + ${jackson.version} @@ -507,38 +509,38 @@ com.fasterxml.jackson jackson-base - 2.15.0-rc1 + ${jackson.version} pom com.fasterxml.jackson.core jackson-core - 2.15.0-rc1 + ${jackson.version} com.fasterxml.jackson.core jackson-databind - 2.15.0-rc1 + ${jackson.version} com.fasterxml.jackson.jaxrs jackson-jaxrs-json-provider - 2.15.0-rc1 + ${jackson.version} com.fasterxml.jackson.core jackson-annotations - 2.15.0-rc1 + ${jackson.version} com.fasterxml.jackson.dataformat jackson-dataformat-xml - 2.15.0-rc1 + ${jackson.version} com.fasterxml.jackson.module jackson-module-jsonSchema - 2.15.0-rc1 + ${jackson.version} io.minio From 6788ac26482e63568f6f6efac100c259ff7af60a Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Wed, 13 Aug 2025 12:53:17 +0200 Subject: [PATCH 31/92] Add ElasticIndexService dependency to ReindexingTask The ElasticIndexService was added as a new dependency to the ReindexingTask class. This enables better integration and ensures required services are available for indexing operations. --- .../application/engine/elastic/service/ReindexingTask.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java b/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java index 5a54538164..2150e9d6df 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java @@ -58,7 +58,8 @@ public ReindexingTask( IElasticTaskMappingService taskMappingService, IWorkflowService workflowService, @Value("${spring.data.elasticsearch.reindexExecutor.size:20}") int pageSize, - @Value("${spring.data.elasticsearch.reindex-from:#{null}}") Duration from) { + @Value("${spring.data.elasticsearch.reindex-from:#{null}}") Duration from, + ElasticIndexService elasticIndexService) { this.caseRepository = caseRepository; this.taskRepository = taskRepository; this.elasticCaseRepository = elasticCaseRepository; @@ -67,6 +68,7 @@ public ReindexingTask( this.caseMappingService = caseMappingService; this.taskMappingService = taskMappingService; this.workflowService = workflowService; + this.elasticIndexService = elasticIndexService; this.pageSize = pageSize; lastRun = LocalDateTime.now(); From 7edf68f559c764791ca50da3bc2f56a0a022ce03 Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Wed, 13 Aug 2025 12:53:43 +0200 Subject: [PATCH 32/92] Remove unused CaseRepository dependency from ReindexingTask. The CaseRepository field and constructor dependency were eliminated as they are not utilized in the class. This simplifies the code, reduces clutter, and improves maintainability. No functional changes have been introduced. --- .../application/engine/elastic/service/ReindexingTask.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java b/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java index 2150e9d6df..ef2b3a74dd 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java @@ -3,13 +3,11 @@ import com.netgrif.application.engine.elastic.domain.ElasticCaseRepository; import com.netgrif.application.engine.elastic.service.interfaces.*; import com.netgrif.application.engine.workflow.domain.Case; -import com.netgrif.application.engine.workflow.domain.QCase; import com.netgrif.application.engine.workflow.domain.Task; import com.netgrif.application.engine.workflow.domain.repositories.CaseRepository; import com.netgrif.application.engine.workflow.domain.repositories.TaskRepository; import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService; import com.querydsl.core.types.Predicate; -import com.querydsl.core.types.dsl.BooleanExpression; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -33,7 +31,6 @@ public class ReindexingTask { private static final Logger log = LoggerFactory.getLogger(ReindexingTask.class); private int pageSize; - private CaseRepository caseRepository; private TaskRepository taskRepository; private ElasticCaseRepository elasticCaseRepository; private IElasticCaseService elasticCaseService; @@ -47,7 +44,6 @@ public class ReindexingTask { @Autowired public ReindexingTask( - CaseRepository caseRepository, TaskRepository taskRepository, ElasticCaseRepository elasticCaseRepository, @Qualifier("reindexingTaskElasticCaseService") @@ -60,7 +56,6 @@ public ReindexingTask( @Value("${spring.data.elasticsearch.reindexExecutor.size:20}") int pageSize, @Value("${spring.data.elasticsearch.reindex-from:#{null}}") Duration from, ElasticIndexService elasticIndexService) { - this.caseRepository = caseRepository; this.taskRepository = taskRepository; this.elasticCaseRepository = elasticCaseRepository; this.elasticCaseService = elasticCaseService; From d0de75c0a677c261f699f8d24018fbc96fef398b Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Wed, 13 Aug 2025 15:46:07 +0200 Subject: [PATCH 33/92] Refactor Elasticsearch indexing and update entity handling. Standardize ID usage for ElasticTask and ElasticCase by setting IDs during transformation. Update indexing logic to handle existing cases and tasks via lookup and merge, ensuring up-to-date data in Elasticsearch. Adjust batch size defaults and improve validation for Elasticsearch properties. --- .../properties/ElasticsearchProperties.java | 6 ++-- .../engine/elastic/domain/ElasticCase.java | 1 + .../engine/elastic/domain/ElasticTask.java | 1 + .../elastic/service/ElasticIndexService.java | 29 ++++++++++++++----- .../elastic/service/ReindexingTask.java | 2 +- 5 files changed, 29 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/configuration/properties/ElasticsearchProperties.java b/src/main/java/com/netgrif/application/engine/configuration/properties/ElasticsearchProperties.java index 5084cbf5b6..99c2e1c180 100644 --- a/src/main/java/com/netgrif/application/engine/configuration/properties/ElasticsearchProperties.java +++ b/src/main/java/com/netgrif/application/engine/configuration/properties/ElasticsearchProperties.java @@ -6,6 +6,7 @@ import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; +import javax.validation.Valid; import javax.validation.constraints.Min; import java.time.Duration; import java.util.ArrayList; @@ -54,6 +55,7 @@ public class ElasticsearchProperties { private List defaultSearchFilters = new ArrayList<>(); + @Valid private BatchProperties batch = new BatchProperties(); @PostConstruct @@ -79,9 +81,9 @@ public Map getClassSpecificSettings(String className) { @Data public static class BatchProperties { @Min(1) - private int caseBatchSize; + private int caseBatchSize = 5000; @Min(1) - private int taskBatchSize; + private int taskBatchSize = 20000; } } diff --git a/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticCase.java b/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticCase.java index 89dda11f55..34c9c6f212 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticCase.java +++ b/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticCase.java @@ -108,6 +108,7 @@ public class ElasticCase { * @param useCase the data object that should be turned into elasticsearch data object */ public ElasticCase(Case useCase) { + id = useCase.getStringId(); stringId = useCase.getStringId(); uriNodeId = useCase.getUriNodeId(); mongoId = useCase.getStringId(); //TODO: Duplication diff --git a/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticTask.java b/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticTask.java index 9eac5ee86b..f3cea15a17 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticTask.java +++ b/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticTask.java @@ -105,6 +105,7 @@ public class ElasticTask { private Map tags; public ElasticTask(Task task) { + this.id = task.getStringId(); this.stringId = task.getStringId(); this.processId = task.getProcessId(); this.taskId = task.getStringId(); diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java index a3d039d0ec..81a4a7603a 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java @@ -8,12 +8,13 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.netgrif.application.engine.configuration.properties.ElasticsearchProperties; import com.netgrif.application.engine.elastic.domain.ElasticCase; +import com.netgrif.application.engine.elastic.domain.ElasticCaseRepository; import com.netgrif.application.engine.elastic.domain.ElasticTask; +import com.netgrif.application.engine.elastic.domain.ElasticTaskRepository; import com.netgrif.application.engine.elastic.service.interfaces.IElasticIndexService; import com.netgrif.application.engine.petrinet.service.PetriNetService; import com.netgrif.application.engine.workflow.domain.Case; import com.netgrif.application.engine.workflow.domain.Task; -import com.netgrif.application.engine.workflow.domain.repositories.CaseRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.elasticsearch.ElasticsearchException; @@ -65,7 +66,9 @@ public class ElasticIndexService implements IElasticIndexService { private final ElasticsearchProperties elasticsearchProperties; - private final CaseRepository caseRepository; + private final ElasticCaseRepository elasticCaseRepository; + + private final ElasticTaskRepository elasticTaskRepository; private final PetriNetService petriNetService; @@ -366,8 +369,14 @@ private void reindexQueried(org.springframework.data.mongodb.core.query.Query qu while (cursor.hasNext()) { Case aCase = cursor.next(); prepareCase(aCase); - ElasticCase doc = caseMappingService.transform(aCase); - prepareCaseBulkOperation(doc, caseOperations); + ElasticCase elasticCase = caseMappingService.transform(aCase); + ElasticCase savedCase = elasticCaseRepository.findByStringId(aCase.getStringId()); + if (savedCase == null) { + savedCase = elasticCase; + } else { + savedCase.update(elasticCase); + } + prepareCaseBulkOperation(savedCase, caseOperations); caseIds.add(aCase.getStringId()); if (++currentBatchSize == caseBatchSize || !cursor.hasNext()) { @@ -404,7 +413,13 @@ private void bulkIndexTasks(List caseIds, int taskBatchSize) { while (cursor.hasNext()) { Task task = cursor.next(); ElasticTask elasticTask = taskMappingService.transform(task); - prepareTaskBulkOperation(elasticTask, taskOperations); + ElasticTask savedTask = elasticTaskRepository.findByStringId(task.getStringId()); + if (savedTask == null) { + savedTask = elasticTask; + } else { + savedTask.update(elasticTask); + } + prepareTaskBulkOperation(savedTask, taskOperations); if (++currentBatchSize == taskBatchSize || !cursor.hasNext()) { log.info("Reindexing task page {} / {}", page, numOfPages); @@ -442,7 +457,7 @@ private void prepareCaseBulkOperation(ElasticCase doc, List opera operations.add(BulkOperation.of(op -> op .update(u -> u .index(elasticsearchProperties.getIndex().get("case")) - .id(doc.getStringId()) + .id(doc.getId()) .action(a -> a .doc(doc) .docAsUpsert(true) @@ -464,7 +479,7 @@ private void prepareTaskBulkOperation(ElasticTask doc, List opera operations.add(BulkOperation.of(op -> op .update(u -> u .index(elasticsearchProperties.getIndex().get("task")) - .id(doc.getStringId()) + .id(doc.getId()) .action(a -> a .doc(doc) .docAsUpsert(true) diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java b/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java index ef2b3a74dd..3b9426c74f 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java @@ -55,7 +55,7 @@ public ReindexingTask( IWorkflowService workflowService, @Value("${spring.data.elasticsearch.reindexExecutor.size:20}") int pageSize, @Value("${spring.data.elasticsearch.reindex-from:#{null}}") Duration from, - ElasticIndexService elasticIndexService) { + IElasticIndexService elasticIndexService) { this.taskRepository = taskRepository; this.elasticCaseRepository = elasticCaseRepository; this.elasticCaseService = elasticCaseService; From 523f1b0586f0e776e7a0f6f6f3af45c417567346 Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Thu, 14 Aug 2025 11:10:08 +0200 Subject: [PATCH 34/92] Handle duplicate records during ElasticSearch reindexing Added logic to handle and reindex duplicate cases and tasks by catching InvalidDataAccessApiUsageException and deleting duplicates. Removed redundant assignment of `id` fields in ElasticTask and ElasticCase constructors to avoid potential inconsistencies. --- .../engine/elastic/domain/ElasticCase.java | 1 - .../engine/elastic/domain/ElasticTask.java | 1 - .../elastic/service/ElasticIndexService.java | 33 +++++++++++++------ 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticCase.java b/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticCase.java index 34c9c6f212..89dda11f55 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticCase.java +++ b/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticCase.java @@ -108,7 +108,6 @@ public class ElasticCase { * @param useCase the data object that should be turned into elasticsearch data object */ public ElasticCase(Case useCase) { - id = useCase.getStringId(); stringId = useCase.getStringId(); uriNodeId = useCase.getUriNodeId(); mongoId = useCase.getStringId(); //TODO: Duplication diff --git a/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticTask.java b/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticTask.java index f3cea15a17..9eac5ee86b 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticTask.java +++ b/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticTask.java @@ -105,7 +105,6 @@ public class ElasticTask { private Map tags; public ElasticTask(Task task) { - this.id = task.getStringId(); this.stringId = task.getStringId(); this.processId = task.getProcessId(); this.taskId = task.getStringId(); diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java index 81a4a7603a..f208abe4fd 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java @@ -28,6 +28,7 @@ import org.elasticsearch.xcontent.XContentType; import org.springframework.context.ApplicationContext; import org.springframework.core.io.Resource; +import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.annotation.Id; import org.springframework.data.elasticsearch.annotations.Setting; import org.springframework.data.elasticsearch.core.ElasticsearchOperations; @@ -370,13 +371,19 @@ private void reindexQueried(org.springframework.data.mongodb.core.query.Query qu Case aCase = cursor.next(); prepareCase(aCase); ElasticCase elasticCase = caseMappingService.transform(aCase); - ElasticCase savedCase = elasticCaseRepository.findByStringId(aCase.getStringId()); - if (savedCase == null) { - savedCase = elasticCase; + ElasticCase existingCase = null; + try { + existingCase = elasticCaseRepository.findByStringId(aCase.getStringId()); + } catch (InvalidDataAccessApiUsageException ignored) { + log.debug("[{}]: Case \"{}\" has duplicates, will reindex.", aCase.getStringId(), aCase.getTitle()); + elasticCaseRepository.deleteAllByStringId(aCase.getStringId()); + } + if (existingCase == null) { + existingCase = elasticCase; } else { - savedCase.update(elasticCase); + existingCase.update(elasticCase); } - prepareCaseBulkOperation(savedCase, caseOperations); + prepareCaseBulkOperation(existingCase, caseOperations); caseIds.add(aCase.getStringId()); if (++currentBatchSize == caseBatchSize || !cursor.hasNext()) { @@ -413,13 +420,19 @@ private void bulkIndexTasks(List caseIds, int taskBatchSize) { while (cursor.hasNext()) { Task task = cursor.next(); ElasticTask elasticTask = taskMappingService.transform(task); - ElasticTask savedTask = elasticTaskRepository.findByStringId(task.getStringId()); - if (savedTask == null) { - savedTask = elasticTask; + ElasticTask existingTask = null; + try { + existingTask = elasticTaskRepository.findByStringId(task.getStringId()); + } catch (InvalidDataAccessApiUsageException ignored) { + log.debug("[{}]: Task \"{}\" has duplicates, will reindex.", task.getStringId(), task.getTitle()); + elasticCaseRepository.deleteAllByStringId(task.getStringId()); + } + if (existingTask == null) { + existingTask = elasticTask; } else { - savedTask.update(elasticTask); + existingTask.update(elasticTask); } - prepareTaskBulkOperation(savedTask, taskOperations); + prepareTaskBulkOperation(existingTask, taskOperations); if (++currentBatchSize == taskBatchSize || !cursor.hasNext()) { log.info("Reindexing task page {} / {}", page, numOfPages); From 1788209bfb7fefcbacbc119d29b1282124980977 Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Thu, 14 Aug 2025 12:00:29 +0200 Subject: [PATCH 35/92] Fix incorrect repository usage in task reindexing logic Replaced elasticCaseRepository with elasticTaskRepository when deleting tasks by stringId. This ensures the correct repository operation is invoked and resolves potential data consistency issues during reindexing. --- .../application/engine/elastic/service/ElasticIndexService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java index f208abe4fd..41142a1c6e 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java @@ -425,7 +425,7 @@ private void bulkIndexTasks(List caseIds, int taskBatchSize) { existingTask = elasticTaskRepository.findByStringId(task.getStringId()); } catch (InvalidDataAccessApiUsageException ignored) { log.debug("[{}]: Task \"{}\" has duplicates, will reindex.", task.getStringId(), task.getTitle()); - elasticCaseRepository.deleteAllByStringId(task.getStringId()); + elasticTaskRepository.deleteAllByStringId(task.getStringId()); } if (existingTask == null) { existingTask = elasticTask; From 37f2023206c39279cf3ac3bd5192e7e107ed8487 Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Thu, 14 Aug 2025 17:34:13 +0200 Subject: [PATCH 36/92] Refactor Elasticsearch client integration and authentication. Removed ElasticSearchJsonpMapper and transitioned to using RestHighLevelClient with configurable credentials support. Refactored bulk operation handling and mapped serializer setup to ObjectMapper bean. Adjusted LocalDateTime handling in date field transformations. --- .../ElasticsearchConfiguration.java | 65 +++++++----- .../properties/ElasticsearchProperties.java | 4 + .../service/ElasticCaseMappingService.java | 4 +- .../elastic/service/ElasticIndexService.java | 98 +++++++++++-------- .../service/ElasticSearchJsonpMapper.java | 28 ------ 5 files changed, 108 insertions(+), 91 deletions(-) delete mode 100644 src/main/java/com/netgrif/application/engine/elastic/service/ElasticSearchJsonpMapper.java diff --git a/src/main/java/com/netgrif/application/engine/configuration/ElasticsearchConfiguration.java b/src/main/java/com/netgrif/application/engine/configuration/ElasticsearchConfiguration.java index 9444649e31..9bbb202adf 100644 --- a/src/main/java/com/netgrif/application/engine/configuration/ElasticsearchConfiguration.java +++ b/src/main/java/com/netgrif/application/engine/configuration/ElasticsearchConfiguration.java @@ -1,14 +1,21 @@ package com.netgrif.application.engine.configuration; -import co.elastic.clients.elasticsearch.ElasticsearchClient; -import co.elastic.clients.transport.ElasticsearchTransport; -import co.elastic.clients.transport.rest_client.RestClientTransport; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.netgrif.application.engine.configuration.properties.ElasticsearchProperties; import com.netgrif.application.engine.configuration.properties.UriProperties; -import com.netgrif.application.engine.elastic.service.ElasticSearchJsonpMapper; +import com.netgrif.application.engine.elastic.serializer.LocalDateTimeJsonDeserializer; +import com.netgrif.application.engine.elastic.serializer.LocalDateTimeJsonSerializer; import com.netgrif.application.engine.workflow.service.CaseEventHandler; +import lombok.RequiredArgsConstructor; import org.apache.http.HttpHost; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.impl.client.BasicCredentialsProvider; import org.elasticsearch.client.RestClient; +import org.elasticsearch.client.RestClientBuilder; import org.elasticsearch.client.RestHighLevelClient; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; @@ -16,15 +23,12 @@ import org.springframework.data.elasticsearch.core.ElasticsearchOperations; import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate; +import java.time.LocalDateTime; + @Configuration +@RequiredArgsConstructor public class ElasticsearchConfiguration { - @Value("${spring.data.elasticsearch.url}") - private String url; - - @Value("${spring.data.elasticsearch.searchport}") - private int port; - @Value("${spring.data.elasticsearch.index.petriNet}") private String petriNetIndex; @@ -37,11 +41,9 @@ public class ElasticsearchConfiguration { @Value("${spring.data.elasticsearch.reindex}") private String cron; - private final UriProperties uriProperties; + private final ElasticsearchProperties elasticsearchProperties; - public ElasticsearchConfiguration(UriProperties uriProperties) { - this.uriProperties = uriProperties; - } + private final UriProperties uriProperties; @Bean public String springElasticsearchReindex() { @@ -70,9 +72,18 @@ public String elasticUriIndex() { @Bean public RestHighLevelClient client() { - - return new RestHighLevelClient( - RestClient.builder(new HttpHost(url, port, "http"))); + RestClientBuilder builder = RestClient.builder(new HttpHost(elasticsearchProperties.getUrl(), elasticsearchProperties.getSearchPort())); + if (hasCredentials()) { + CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); + credentialsProvider.setCredentials(AuthScope.ANY, + new UsernamePasswordCredentials( + elasticsearchProperties.getUsername(), + elasticsearchProperties.getPassword() + ) + ); + builder.setHttpClientConfigCallback(httpClientBuilder -> httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider)); + } + return new RestHighLevelClient(builder); } @Bean @@ -85,11 +96,21 @@ public CaseEventHandler caseEventHandler() { return new CaseEventHandler(); } - @Bean - public ElasticsearchClient elasticsearchClient() { - RestClient restClient = RestClient.builder(new HttpHost(url, port)).build(); - ElasticsearchTransport transport = new RestClientTransport(restClient, new ElasticSearchJsonpMapper()); - return new ElasticsearchClient(transport); + @Bean(name = "elasticCaseObjectMapper") + public ObjectMapper configureMapper() { + ObjectMapper mapper = new ObjectMapper(); + + JavaTimeModule javaTimeModule = new JavaTimeModule(); + javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeJsonSerializer()); + javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeJsonDeserializer()); + + mapper.registerModule(javaTimeModule); + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + return mapper; + } + private boolean hasCredentials() { + return elasticsearchProperties.getUsername() != null && !elasticsearchProperties.getUsername().isBlank() && + elasticsearchProperties.getPassword() != null && !elasticsearchProperties.getPassword().isBlank(); } } diff --git a/src/main/java/com/netgrif/application/engine/configuration/properties/ElasticsearchProperties.java b/src/main/java/com/netgrif/application/engine/configuration/properties/ElasticsearchProperties.java index 99c2e1c180..f3b1269b09 100644 --- a/src/main/java/com/netgrif/application/engine/configuration/properties/ElasticsearchProperties.java +++ b/src/main/java/com/netgrif/application/engine/configuration/properties/ElasticsearchProperties.java @@ -35,6 +35,10 @@ public class ElasticsearchProperties { private String url; + private String username; + + private String password; + private Map index; private boolean analyzerEnabled = false; diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseMappingService.java b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseMappingService.java index ee50866e3a..11286761da 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseMappingService.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseMappingService.java @@ -230,11 +230,11 @@ private StringBuilder buildFullName(String name, String surname) { protected Optional transformDateField(com.netgrif.application.engine.workflow.domain.DataField dateField, com.netgrif.application.engine.petrinet.domain.dataset.DateField netField) { if (dateField.getValue() instanceof LocalDate) { LocalDate date = (LocalDate) dateField.getValue(); - return formatDateField(LocalDateTime.of(date, LocalTime.NOON)); + return formatDateField(LocalDateTime.of(date, LocalTime.MIDNIGHT)); } else if (dateField.getValue() instanceof Date) { // log.warn(String.format("DateFields should have LocalDate values! DateField (%s) with Date value found! Value will be converted for indexation.", netField.getImportId())); LocalDateTime transformed = this.transformDateValueField(dateField); - return formatDateField(LocalDateTime.of(transformed.toLocalDate(), LocalTime.NOON)); + return formatDateField(LocalDateTime.of(transformed.toLocalDate(), LocalTime.MIDNIGHT)); } else { // TODO throw error? log.error(String.format("Unsupported DateField value type (%s)! Skipping indexation...", dateField.getValue().getClass().getCanonicalName())); diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java index 41142a1c6e..d4152cbd8b 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java @@ -1,10 +1,6 @@ package com.netgrif.application.engine.elastic.service; -import co.elastic.clients.elasticsearch.ElasticsearchClient; -import co.elastic.clients.elasticsearch.core.BulkRequest; -import co.elastic.clients.elasticsearch.core.BulkResponse; -import co.elastic.clients.elasticsearch.core.bulk.BulkOperation; import com.fasterxml.jackson.databind.ObjectMapper; import com.netgrif.application.engine.configuration.properties.ElasticsearchProperties; import com.netgrif.application.engine.elastic.domain.ElasticCase; @@ -15,17 +11,21 @@ import com.netgrif.application.engine.petrinet.service.PetriNetService; import com.netgrif.application.engine.workflow.domain.Case; import com.netgrif.application.engine.workflow.domain.Task; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.admin.indices.open.OpenIndexRequest; import org.elasticsearch.action.admin.indices.open.OpenIndexResponse; +import org.elasticsearch.action.bulk.BulkRequest; +import org.elasticsearch.action.bulk.BulkResponse; import org.elasticsearch.action.support.master.AcknowledgedResponse; +import org.elasticsearch.action.update.UpdateRequest; import org.elasticsearch.client.RequestOptions; +import org.elasticsearch.client.RestHighLevelClient; import org.elasticsearch.client.indices.CloseIndexRequest; import org.elasticsearch.client.indices.CloseIndexResponse; import org.elasticsearch.client.indices.PutIndexTemplateRequest; import org.elasticsearch.xcontent.XContentType; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.ApplicationContext; import org.springframework.core.io.Resource; import org.springframework.dao.InvalidDataAccessApiUsageException; @@ -52,7 +52,6 @@ @Slf4j @Service -@RequiredArgsConstructor public class ElasticIndexService implements IElasticIndexService { private static final String PLACEHOLDERS = "petriNetIndex, caseIndex, taskIndex"; @@ -61,7 +60,7 @@ public class ElasticIndexService implements IElasticIndexService { private final ElasticsearchRestTemplate elasticsearchTemplate; - private final ElasticsearchClient elasticsearchClient; + private final RestHighLevelClient elasticsearchClient; private final ElasticsearchOperations operations; @@ -79,6 +78,34 @@ public class ElasticIndexService implements IElasticIndexService { private final ElasticTaskMappingService taskMappingService; + private final ObjectMapper objectMapper; + + public ElasticIndexService(ApplicationContext context, + ElasticsearchRestTemplate elasticsearchTemplate, + RestHighLevelClient elasticsearchClient, + ElasticsearchOperations operations, + ElasticsearchProperties elasticsearchProperties, + ElasticCaseRepository elasticCaseRepository, + ElasticTaskRepository elasticTaskRepository, + PetriNetService petriNetService, + MongoTemplate mongoTemplate, + ElasticCaseMappingService caseMappingService, + ElasticTaskMappingService taskMappingService, + @Qualifier("elasticCaseObjectMapper") + ObjectMapper objectMapper) { + this.context = context; + this.elasticsearchTemplate = elasticsearchTemplate; + this.elasticsearchClient = elasticsearchClient; + this.operations = operations; + this.elasticsearchProperties = elasticsearchProperties; + this.elasticCaseRepository = elasticCaseRepository; + this.elasticTaskRepository = elasticTaskRepository; + this.petriNetService = petriNetService; + this.mongoTemplate = mongoTemplate; + this.caseMappingService = caseMappingService; + this.taskMappingService = taskMappingService; + this.objectMapper = objectMapper; + } @Override public boolean indexExists(String indexName) { @@ -363,7 +390,7 @@ private void reindexQueried(org.springframework.data.mongodb.core.query.Query qu query.cursorBatchSize(caseBatchSize); long page = 1, currentBatchSize = 0; - List caseOperations = new ArrayList<>(); + List caseOperations = new ArrayList<>(); List caseIds = new ArrayList<>(); try (CloseableIterator cursor = mongoTemplate.stream(query, Case.class)) { @@ -414,7 +441,7 @@ private void bulkIndexTasks(List caseIds, int taskBatchSize) { long numOfPages = ((totalSize / taskBatchSize) + 1); long page = 1, currentBatchSize = 0; - List taskOperations = new ArrayList<>(); + List taskOperations = new ArrayList<>(); try (CloseableIterator cursor = mongoTemplate.stream(query, Task.class)) { while (cursor.hasNext()) { @@ -465,17 +492,14 @@ private void prepareCase(Case useCase) { * @param doc transformed ElasticCase object * @param operations collection of BulkOperations to add this operation to */ - private void prepareCaseBulkOperation(ElasticCase doc, List operations) { + private void prepareCaseBulkOperation(ElasticCase doc, List operations) { try { - operations.add(BulkOperation.of(op -> op - .update(u -> u - .index(elasticsearchProperties.getIndex().get("case")) - .id(doc.getId()) - .action(a -> a - .doc(doc) - .docAsUpsert(true) - ) - ))); + UpdateRequest updateRequest = new UpdateRequest() + .id(doc.getId()) + .doc(objectMapper.writeValueAsString(doc), XContentType.JSON) + .upsert(objectMapper.writeValueAsString(doc), XContentType.JSON) + .index(elasticsearchProperties.getIndex().get("case")); + operations.add(updateRequest); } catch (Exception e) { log.error("Failed to prepare bulk operation for case [{}]: {}", doc.getStringId(), e.getMessage()); } @@ -487,18 +511,14 @@ private void prepareCaseBulkOperation(ElasticCase doc, List opera * @param doc transformed ElasticTask object * @param operations collection of BulkOperations to add this operation to */ - private void prepareTaskBulkOperation(ElasticTask doc, List operations) { + private void prepareTaskBulkOperation(ElasticTask doc, List operations) { try { - operations.add(BulkOperation.of(op -> op - .update(u -> u - .index(elasticsearchProperties.getIndex().get("task")) - .id(doc.getId()) - .action(a -> a - .doc(doc) - .docAsUpsert(true) - ) - )) - ); + UpdateRequest updateRequest = new UpdateRequest() + .id(doc.getId()) + .doc(objectMapper.writeValueAsString(doc), XContentType.JSON) + .upsert(objectMapper.writeValueAsString(doc), XContentType.JSON) + .index(elasticsearchProperties.getIndex().get("task")); + operations.add(updateRequest); } catch (Exception e) { log.error("Failed to prepare bulk operation for task [{}]: {}", doc.getStringId(), e.getMessage()); } @@ -509,16 +529,16 @@ private void prepareTaskBulkOperation(ElasticTask doc, List opera * * @param operations list of bulk operations to execute */ - private void executeAndValidate(List operations) { + private void executeAndValidate(List operations) { if (operations.isEmpty()) { return; } - BulkRequest.Builder builder = new BulkRequest.Builder(); - builder.operations(operations); + BulkRequest request = new BulkRequest(); + operations.forEach(request::add); try { - BulkResponse response = elasticsearchClient.bulk(builder.build()); + BulkResponse response = elasticsearchClient.bulk(request, RequestOptions.DEFAULT); checkForBulkUpdateFailure(response); log.info("Batch indexed successfully with {} ops", operations.size()); } catch (ElasticsearchException e) { @@ -532,8 +552,8 @@ private void executeAndValidate(List operations) { log.warn("Dividing the requirement."); int mid = operations.size() / 2; - List left = operations.subList(0, mid); - List right = operations.subList(mid, operations.size()); + List left = operations.subList(0, mid); + List right = operations.subList(mid, operations.size()); executeAndValidate(new ArrayList<>(left)); executeAndValidate(new ArrayList<>(right)); @@ -550,9 +570,9 @@ private void executeAndValidate(List operations) { */ private void checkForBulkUpdateFailure(BulkResponse response) { Map failedDocuments = new HashMap<>(); - response.items().forEach(item -> { - if (item.error() != null) { - failedDocuments.put(item.id(), item.error().reason()); + Arrays.stream(response.getItems()).forEach(item -> { + if (item.getFailure() != null) { + failedDocuments.put(item.getId(), item.getFailure().getMessage()); } }); diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticSearchJsonpMapper.java b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticSearchJsonpMapper.java deleted file mode 100644 index aa88227a75..0000000000 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticSearchJsonpMapper.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.netgrif.application.engine.elastic.service; - -import co.elastic.clients.json.jackson.JacksonJsonpMapper; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import com.netgrif.application.engine.elastic.serializer.LocalDateTimeJsonDeserializer; -import com.netgrif.application.engine.elastic.serializer.LocalDateTimeJsonSerializer; - -import java.time.LocalDateTime; - -public class ElasticSearchJsonpMapper extends JacksonJsonpMapper { - public ElasticSearchJsonpMapper() { - super(configureMapper()); - } - - private static ObjectMapper configureMapper() { - ObjectMapper mapper = new ObjectMapper(); - JavaTimeModule javaTimeModule = new JavaTimeModule(); - - mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); - javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeJsonSerializer()); - javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeJsonDeserializer()); - mapper.registerModule(javaTimeModule); - - return mapper; - } -} From d1600974369c2809dfd15a7a18b960ef5e122be1 Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Thu, 14 Aug 2025 17:45:34 +0200 Subject: [PATCH 37/92] Fix null ID handling in Elastic bulk operations Updated `prepareCaseBulkOperation` and `prepareTaskBulkOperation` methods to handle cases where `doc.getId()` is null by using `doc.getStringId()` as a fallback. This ensures robust ID assignment during bulk operations and prevents potential null pointer issues. --- .../engine/elastic/service/ElasticIndexService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java index d4152cbd8b..278b330df6 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java @@ -495,7 +495,7 @@ private void prepareCase(Case useCase) { private void prepareCaseBulkOperation(ElasticCase doc, List operations) { try { UpdateRequest updateRequest = new UpdateRequest() - .id(doc.getId()) + .id(doc.getId() == null ? doc.getStringId() : doc.getId()) .doc(objectMapper.writeValueAsString(doc), XContentType.JSON) .upsert(objectMapper.writeValueAsString(doc), XContentType.JSON) .index(elasticsearchProperties.getIndex().get("case")); @@ -514,7 +514,7 @@ private void prepareCaseBulkOperation(ElasticCase doc, List opera private void prepareTaskBulkOperation(ElasticTask doc, List operations) { try { UpdateRequest updateRequest = new UpdateRequest() - .id(doc.getId()) + .id(doc.getId() == null ? doc.getStringId() : doc.getId()) .doc(objectMapper.writeValueAsString(doc), XContentType.JSON) .upsert(objectMapper.writeValueAsString(doc), XContentType.JSON) .index(elasticsearchProperties.getIndex().get("task")); From b88fc8a2dcef01ce5f80cdfc81350f24ac13b36a Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Thu, 14 Aug 2025 17:46:45 +0200 Subject: [PATCH 38/92] Refactor bulk operation JSON handling in ElasticIndexService Extract the JSON serialization of documents into a variable to improve code readability and reduce redundancy. This change applies to both case and task bulk operations. --- .../engine/elastic/service/ElasticIndexService.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java index 278b330df6..ec41ec1f7f 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java @@ -494,10 +494,11 @@ private void prepareCase(Case useCase) { */ private void prepareCaseBulkOperation(ElasticCase doc, List operations) { try { + String json = objectMapper.writeValueAsString(doc); UpdateRequest updateRequest = new UpdateRequest() .id(doc.getId() == null ? doc.getStringId() : doc.getId()) - .doc(objectMapper.writeValueAsString(doc), XContentType.JSON) - .upsert(objectMapper.writeValueAsString(doc), XContentType.JSON) + .doc(json, XContentType.JSON) + .upsert(json, XContentType.JSON) .index(elasticsearchProperties.getIndex().get("case")); operations.add(updateRequest); } catch (Exception e) { @@ -513,10 +514,11 @@ private void prepareCaseBulkOperation(ElasticCase doc, List opera */ private void prepareTaskBulkOperation(ElasticTask doc, List operations) { try { + String json = objectMapper.writeValueAsString(doc); UpdateRequest updateRequest = new UpdateRequest() .id(doc.getId() == null ? doc.getStringId() : doc.getId()) - .doc(objectMapper.writeValueAsString(doc), XContentType.JSON) - .upsert(objectMapper.writeValueAsString(doc), XContentType.JSON) + .doc(json, XContentType.JSON) + .upsert(json, XContentType.JSON) .index(elasticsearchProperties.getIndex().get("task")); operations.add(updateRequest); } catch (Exception e) { From 6f102aab75c546565e57db5dbbfa4a8c0fbff019 Mon Sep 17 00:00:00 2001 From: Machac Date: Thu, 14 Aug 2025 18:25:34 +0200 Subject: [PATCH 39/92] [NAE-2136] Speed up Elasticsearch reindex Update `DataSearchRequestTest` to use `MIDNIGHT` instead of `NOON` for `LocalTime` in timestamp creation --- .../application/engine/elastic/DataSearchRequestTest.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/groovy/com/netgrif/application/engine/elastic/DataSearchRequestTest.groovy b/src/test/groovy/com/netgrif/application/engine/elastic/DataSearchRequestTest.groovy index 0801773097..56cd0c04d9 100644 --- a/src/test/groovy/com/netgrif/application/engine/elastic/DataSearchRequestTest.groovy +++ b/src/test/groovy/com/netgrif/application/engine/elastic/DataSearchRequestTest.groovy @@ -158,7 +158,7 @@ class DataSearchRequestTest { new AbstractMap.SimpleEntry("user.emailValue.keyword" as String, "${testUser1.email}" as String), new AbstractMap.SimpleEntry("user.fullNameValue.keyword" as String, "${testUser1.fullName}" as String), new AbstractMap.SimpleEntry("user.userIdValue" as String, "${testUser1.getId()}" as String), - new AbstractMap.SimpleEntry("date.timestampValue" as String, "${Timestamp.valueOf(LocalDateTime.of(date, LocalTime.NOON)).getTime()}" as String), + new AbstractMap.SimpleEntry("date.timestampValue" as String, "${Timestamp.valueOf(LocalDateTime.of(date, LocalTime.MIDNIGHT)).getTime()}" as String), new AbstractMap.SimpleEntry("datetime.timestampValue" as String, "${Timestamp.valueOf(date.atTime(13, 37)).getTime()}" as String), new AbstractMap.SimpleEntry("enumeration" as String, "Alice" as String), new AbstractMap.SimpleEntry("enumeration" as String, "Alica" as String), From 0a19875a395f162f12633d918fe8c515c0657ba3 Mon Sep 17 00:00:00 2001 From: Machac Date: Thu, 14 Aug 2025 19:16:00 +0200 Subject: [PATCH 40/92] [NAE-2136] Speed up Elasticsearch reindex docker-compose.yml --- docker-compose.yml | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index de15910fff..6ccd039ca1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -38,6 +38,7 @@ services: image: redis:6 ports: - "6379:6379" + minio: image: docker.io/bitnami/minio:2022 ports: @@ -51,13 +52,6 @@ services: - MINIO_ROOT_USER=root - MINIO_ROOT_PASSWORD=password - MINIO_DEFAULT_BUCKETS=default -networks: - minionetwork: - driver: bridge - -volumes: - minio_data: - driver: local # kibana: # image: docker.elastic.co/kibana/kibana:7.17.4 @@ -67,3 +61,12 @@ volumes: # - docker-elastic # ports: # - "5601:5601" + +networks: + minionetwork: + driver: bridge + +volumes: + minio_data: + driver: local + From cb8cd88ec0ea1c09909349107f4f783d82e18440 Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Tue, 19 Aug 2025 14:04:30 +0200 Subject: [PATCH 41/92] Refactor ObjectMapper configuration for Elasticsearch. Moved the ObjectMapper configuration from ElasticsearchConfiguration to ElasticIndexService. This improves cohesion by localizing the configuration closer to its usage. Removed unused bean, reducing complexity in the configuration class. --- .../ElasticsearchConfiguration.java | 16 +++------------- .../elastic/service/ElasticIndexService.java | 19 ++++++++++++++----- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/configuration/ElasticsearchConfiguration.java b/src/main/java/com/netgrif/application/engine/configuration/ElasticsearchConfiguration.java index 9bbb202adf..aab0fc7c8b 100644 --- a/src/main/java/com/netgrif/application/engine/configuration/ElasticsearchConfiguration.java +++ b/src/main/java/com/netgrif/application/engine/configuration/ElasticsearchConfiguration.java @@ -1,5 +1,6 @@ package com.netgrif.application.engine.configuration; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; @@ -17,9 +18,11 @@ import org.elasticsearch.client.RestClient; import org.elasticsearch.client.RestClientBuilder; import org.elasticsearch.client.RestHighLevelClient; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; import org.springframework.data.elasticsearch.core.ElasticsearchOperations; import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate; @@ -96,19 +99,6 @@ public CaseEventHandler caseEventHandler() { return new CaseEventHandler(); } - @Bean(name = "elasticCaseObjectMapper") - public ObjectMapper configureMapper() { - ObjectMapper mapper = new ObjectMapper(); - - JavaTimeModule javaTimeModule = new JavaTimeModule(); - javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeJsonSerializer()); - javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeJsonDeserializer()); - - mapper.registerModule(javaTimeModule); - mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); - return mapper; - } - private boolean hasCredentials() { return elasticsearchProperties.getUsername() != null && !elasticsearchProperties.getUsername().isBlank() && elasticsearchProperties.getPassword() != null && !elasticsearchProperties.getPassword().isBlank(); diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java index ec41ec1f7f..0b89b7da00 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java @@ -2,11 +2,15 @@ import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.netgrif.application.engine.configuration.properties.ElasticsearchProperties; import com.netgrif.application.engine.elastic.domain.ElasticCase; import com.netgrif.application.engine.elastic.domain.ElasticCaseRepository; import com.netgrif.application.engine.elastic.domain.ElasticTask; import com.netgrif.application.engine.elastic.domain.ElasticTaskRepository; +import com.netgrif.application.engine.elastic.serializer.LocalDateTimeJsonDeserializer; +import com.netgrif.application.engine.elastic.serializer.LocalDateTimeJsonSerializer; import com.netgrif.application.engine.elastic.service.interfaces.IElasticIndexService; import com.netgrif.application.engine.petrinet.service.PetriNetService; import com.netgrif.application.engine.workflow.domain.Case; @@ -25,7 +29,6 @@ import org.elasticsearch.client.indices.CloseIndexResponse; import org.elasticsearch.client.indices.PutIndexTemplateRequest; import org.elasticsearch.xcontent.XContentType; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.ApplicationContext; import org.springframework.core.io.Resource; import org.springframework.dao.InvalidDataAccessApiUsageException; @@ -90,9 +93,7 @@ public ElasticIndexService(ApplicationContext context, PetriNetService petriNetService, MongoTemplate mongoTemplate, ElasticCaseMappingService caseMappingService, - ElasticTaskMappingService taskMappingService, - @Qualifier("elasticCaseObjectMapper") - ObjectMapper objectMapper) { + ElasticTaskMappingService taskMappingService) { this.context = context; this.elasticsearchTemplate = elasticsearchTemplate; this.elasticsearchClient = elasticsearchClient; @@ -104,7 +105,8 @@ public ElasticIndexService(ApplicationContext context, this.mongoTemplate = mongoTemplate; this.caseMappingService = caseMappingService; this.taskMappingService = taskMappingService; - this.objectMapper = objectMapper; + this.objectMapper = new ObjectMapper(); + configureMapper(); } @Override @@ -656,4 +658,11 @@ private String getIndexName(Class clazz, String... placeholders) { return indexName; } + private void configureMapper() { + JavaTimeModule javaTimeModule = new JavaTimeModule(); + javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeJsonSerializer()); + javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeJsonDeserializer()); + objectMapper.registerModule(javaTimeModule); + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + } } From 43738ecf22ab05b4c49dab8e5f6f3b84a7f14357 Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Tue, 19 Aug 2025 14:11:03 +0200 Subject: [PATCH 42/92] Refactor ElasticsearchConfiguration to remove unused imports Removed unnecessary imports and unused dependencies from ElasticsearchConfiguration. This simplifies the code, improves readability, and ensures maintainability by decluttering the file. --- .../configuration/ElasticsearchConfiguration.java | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/configuration/ElasticsearchConfiguration.java b/src/main/java/com/netgrif/application/engine/configuration/ElasticsearchConfiguration.java index aab0fc7c8b..3c1e253c8c 100644 --- a/src/main/java/com/netgrif/application/engine/configuration/ElasticsearchConfiguration.java +++ b/src/main/java/com/netgrif/application/engine/configuration/ElasticsearchConfiguration.java @@ -1,13 +1,7 @@ package com.netgrif.application.engine.configuration; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.netgrif.application.engine.configuration.properties.ElasticsearchProperties; import com.netgrif.application.engine.configuration.properties.UriProperties; -import com.netgrif.application.engine.elastic.serializer.LocalDateTimeJsonDeserializer; -import com.netgrif.application.engine.elastic.serializer.LocalDateTimeJsonSerializer; import com.netgrif.application.engine.workflow.service.CaseEventHandler; import lombok.RequiredArgsConstructor; import org.apache.http.HttpHost; @@ -18,16 +12,12 @@ import org.elasticsearch.client.RestClient; import org.elasticsearch.client.RestClientBuilder; import org.elasticsearch.client.RestHighLevelClient; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.annotation.Order; import org.springframework.data.elasticsearch.core.ElasticsearchOperations; import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate; -import java.time.LocalDateTime; - @Configuration @RequiredArgsConstructor public class ElasticsearchConfiguration { From 867240bdc7067b84091752e9ae6cb22fea1544f4 Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Thu, 21 Aug 2025 14:44:45 +0200 Subject: [PATCH 43/92] Remove custom serializers for startDate in ElasticTask The custom JsonSerialize and JsonDeserialize annotations for startDate were removed. Elastic's FieldType.Date with the specified format handles the serialization/deserialization, making the custom serializers redundant. This simplifies the code and reduces unnecessary dependencies. --- .../netgrif/application/engine/elastic/domain/ElasticTask.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticTask.java b/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticTask.java index 9eac5ee86b..e0618f76e2 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticTask.java +++ b/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticTask.java @@ -64,8 +64,6 @@ public class ElasticTask { private String userId; - @JsonSerialize(using = LocalDateTimeSerializer.class) - @JsonDeserialize(using = LocalDateTimeDeserializer.class) @Field(type = FieldType.Date, format = DateFormat.date_hour_minute_second_millis) private LocalDateTime startDate; From dce6d0b82f469596eb402b721fe24925efb4dbaa Mon Sep 17 00:00:00 2001 From: chvostek Date: Thu, 9 Oct 2025 10:28:11 +0200 Subject: [PATCH 44/92] [NAE-2225] Not possible to set empty options using setData - fix setting empty options in DataService --- .../application/engine/workflow/service/DataService.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java b/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java index 70a6903f40..bdfbbbf90d 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java @@ -1050,16 +1050,13 @@ private Map parseOptions(JsonNode node) { if (optionsNode == null) { return null; } + ObjectMapper mapper = new ObjectMapper(); SimpleModule module = new SimpleModule(); module.addDeserializer(I18nString.class, new I18nStringDeserializer()); mapper.registerModule(module); - Map optionsMapped = mapper.convertValue(optionsNode, new TypeReference>() { - }); - if (optionsMapped.isEmpty()) { - return null; - } - return optionsMapped; + + return mapper.convertValue(optionsNode, new TypeReference<>() {}); } private void setDataFieldOptions(Map options, DataField dataField, ChangedField changedField, String fieldType) { From 240ee7da62dae42bd7cd11c91d11f73c39892260 Mon Sep 17 00:00:00 2001 From: chvostek Date: Tue, 14 Oct 2025 15:27:29 +0200 Subject: [PATCH 45/92] [NAE-2231] Unable to change behavior of taskRef on finish event without error message - implement FinishTaskEventOutcome flag, which signals if the finished task still exists --- .../taskoutcomes/FinishTaskEventOutcome.java | 11 +++++++++++ .../LocalisedFinishTaskEventOutcome.java | 7 +++++++ 2 files changed, 18 insertions(+) diff --git a/src/main/java/com/netgrif/application/engine/workflow/domain/eventoutcomes/taskoutcomes/FinishTaskEventOutcome.java b/src/main/java/com/netgrif/application/engine/workflow/domain/eventoutcomes/taskoutcomes/FinishTaskEventOutcome.java index 9fd4506560..92319a6737 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/domain/eventoutcomes/taskoutcomes/FinishTaskEventOutcome.java +++ b/src/main/java/com/netgrif/application/engine/workflow/domain/eventoutcomes/taskoutcomes/FinishTaskEventOutcome.java @@ -10,16 +10,27 @@ @Data public class FinishTaskEventOutcome extends TaskEventOutcome { + /** + * Outcome flag, which is true if the task is still executable after the finish task event + */ + protected boolean isTaskStillExecutable; + public FinishTaskEventOutcome() { super(); } public FinishTaskEventOutcome(Case useCase, Task task) { super(useCase, task); + this.isTaskStillExecutable = isTaskStillExecutable(useCase, task); } public FinishTaskEventOutcome(Case useCase, Task task, List outcomes) { this(useCase, task); this.setOutcomes(outcomes); } + + protected boolean isTaskStillExecutable(Case useCase, Task task) { + return useCase.getTasks().stream() + .anyMatch(taskPair -> taskPair.getTask().equals(task.getStringId())); + } } diff --git a/src/main/java/com/netgrif/application/engine/workflow/web/responsebodies/eventoutcomes/LocalisedFinishTaskEventOutcome.java b/src/main/java/com/netgrif/application/engine/workflow/web/responsebodies/eventoutcomes/LocalisedFinishTaskEventOutcome.java index efc93a6199..0112855158 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/web/responsebodies/eventoutcomes/LocalisedFinishTaskEventOutcome.java +++ b/src/main/java/com/netgrif/application/engine/workflow/web/responsebodies/eventoutcomes/LocalisedFinishTaskEventOutcome.java @@ -2,12 +2,19 @@ import com.netgrif.application.engine.workflow.domain.eventoutcomes.taskoutcomes.FinishTaskEventOutcome; import com.netgrif.application.engine.workflow.web.responsebodies.eventoutcomes.base.LocalisedTaskEventOutcome; +import lombok.Getter; import java.util.Locale; public class LocalisedFinishTaskEventOutcome extends LocalisedTaskEventOutcome { + @Getter + protected Boolean isTaskStillExecutable; + public LocalisedFinishTaskEventOutcome(FinishTaskEventOutcome outcome, Locale locale) { super(outcome, locale); + if (outcome != null) { + this.isTaskStillExecutable = outcome.isTaskStillExecutable() ; + } } } From 14e43031db10d4c35184ba18e64fb5c3e22e69b9 Mon Sep 17 00:00:00 2001 From: Milan Mladoniczky <6153201+tuplle@users.noreply.github.com> Date: Mon, 1 Dec 2025 16:45:35 +0100 Subject: [PATCH 46/92] [NAE-2231] Unable to change behavior of taskRef on finish event without error message - Bump Elasticsearch image version in docker-compose from 7.17.4 to 7.17.28. - Fix null-check and equality handling in task execution logic. - Remove unnecessary whitespace in LocalisedFinishTaskEventOutcome. --- docker-compose.yml | 2 +- .../eventoutcomes/taskoutcomes/FinishTaskEventOutcome.java | 3 ++- .../eventoutcomes/LocalisedFinishTaskEventOutcome.java | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 6ccd039ca1..5f576309bd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,7 +15,7 @@ services: memory: "512M" docker-elastic: - image: elasticsearch:7.17.4 + image: elasticsearch:7.17.28 environment: - cluster.name=elasticsearch - discovery.type=single-node diff --git a/src/main/java/com/netgrif/application/engine/workflow/domain/eventoutcomes/taskoutcomes/FinishTaskEventOutcome.java b/src/main/java/com/netgrif/application/engine/workflow/domain/eventoutcomes/taskoutcomes/FinishTaskEventOutcome.java index 92319a6737..2206527ead 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/domain/eventoutcomes/taskoutcomes/FinishTaskEventOutcome.java +++ b/src/main/java/com/netgrif/application/engine/workflow/domain/eventoutcomes/taskoutcomes/FinishTaskEventOutcome.java @@ -6,6 +6,7 @@ import lombok.Data; import java.util.List; +import java.util.Objects; @Data public class FinishTaskEventOutcome extends TaskEventOutcome { @@ -31,6 +32,6 @@ public FinishTaskEventOutcome(Case useCase, Task task, List outcom protected boolean isTaskStillExecutable(Case useCase, Task task) { return useCase.getTasks().stream() - .anyMatch(taskPair -> taskPair.getTask().equals(task.getStringId())); + .anyMatch(taskPair -> task != null && Objects.equals(taskPair.getTask(), task.getStringId())); } } diff --git a/src/main/java/com/netgrif/application/engine/workflow/web/responsebodies/eventoutcomes/LocalisedFinishTaskEventOutcome.java b/src/main/java/com/netgrif/application/engine/workflow/web/responsebodies/eventoutcomes/LocalisedFinishTaskEventOutcome.java index 0112855158..708e72ee35 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/web/responsebodies/eventoutcomes/LocalisedFinishTaskEventOutcome.java +++ b/src/main/java/com/netgrif/application/engine/workflow/web/responsebodies/eventoutcomes/LocalisedFinishTaskEventOutcome.java @@ -14,7 +14,7 @@ public class LocalisedFinishTaskEventOutcome extends LocalisedTaskEventOutcome { public LocalisedFinishTaskEventOutcome(FinishTaskEventOutcome outcome, Locale locale) { super(outcome, locale); if (outcome != null) { - this.isTaskStillExecutable = outcome.isTaskStillExecutable() ; + this.isTaskStillExecutable = outcome.isTaskStillExecutable(); } } } From f543ddc698767466c5867966b6cf17654ed8178d Mon Sep 17 00:00:00 2001 From: Milan Mladoniczky <6153201+tuplle@users.noreply.github.com> Date: Mon, 1 Dec 2025 18:26:03 +0100 Subject: [PATCH 47/92] Update CHANGELOG, dependencies, and Docker base images - Added fixed issues to the CHANGELOG for NAE-2225 and NAE-2231. - Upgraded central-publishing-maven-plugin to version 0.9.0 in pom.xml. - Updated Docker base images to use Eclipse Temurin for JDK. --- CHANGELOG.md | 4 ++++ Dockerfile | 4 ++-- pom.xml | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cfda405bad..0c250d2f1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ Full Changelog: [https://github.com/netgrif/application-engine/commits/v6.4.2](h ## [6.4.2](https://github.com/netgrif/application-engine/releases/tag/v6.4.2) (2025-05-16) +### Fixed +- [NAE-2225] Not possible to set empty options using setData +- [NAE-2231] Unable to change behavior of taskRef on finish event without error message + ### Added - [NAE-2100] Case view export button as NAE feature diff --git a/Dockerfile b/Dockerfile index c2fe13e8ea..2259d3100a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM maven:3-jdk-11 AS build +FROM maven:3.9-eclipse-temurin-11 AS build MAINTAINER Netgrif WORKDIR /app COPY src /app/src @@ -6,7 +6,7 @@ COPY pom.xml /app RUN mvn -P docker-build -DskipTests=true -f /app/pom.xml clean package install -FROM openjdk:11-jdk +FROM eclipse-temurin:11-jdk MAINTAINER Netgrif COPY --from=build app/target/app-exec.jar /app.jar COPY --from=build app/src/main/resources /src/main/resources diff --git a/pom.xml b/pom.xml index 06357d4018..e57436ea5c 100644 --- a/pom.xml +++ b/pom.xml @@ -927,7 +927,7 @@ org.sonatype.central central-publishing-maven-plugin - 0.8.0 + 0.9.0 true central From 5701851fd2600374d606d6ec4796ca55a4fa09b8 Mon Sep 17 00:00:00 2001 From: Milan Mladoniczky <6153201+tuplle@users.noreply.github.com> Date: Mon, 1 Dec 2025 18:53:27 +0100 Subject: [PATCH 48/92] Update schema URLs in test and documentation XML files - Replaced all occurrences of "https://petriflow.com/petriflow.schema.xsd" with "https://petriflow.org/petriflow.schema.xsd". - Updated multiple XML files in test resources and documentation to ensure consistency with the new schema URL. --- README.md | 4 ++-- docker-compose.yml | 2 +- docs/README.md | 6 +++--- docs/_media/roles/usersRef_net.xml | 2 +- .../views/Layout-Guide-compacting-example.xml | 2 +- .../_media/views/Layout-Guide-flow-example.xml | 2 +- .../_media/views/Layout-Guide-grid-example.xml | 2 +- .../views/Layout-Guide-legacy-example.xml | 2 +- docs/_navbar.md | 2 +- docs/events/case_event.md | 10 +++++----- docs/events/process_event.md | 4 ++-- docs/roles/permissions.md | 18 +++++++++--------- docs/roles/shared_roles.md | 4 ++-- src/main/resources/petriNets/all_data.xml | 2 +- src/main/resources/petriNets/datamap.xml | 4 ++-- .../petriNets/engine-processes/dashboard.xml | 4 ++-- .../engine-processes/dashboard_tile.xml | 4 ++-- .../engine-processes/export_filters.xml | 2 +- .../petriNets/engine-processes/filter.xml | 4 ++-- .../engine-processes/impersonation_config.xml | 4 ++-- .../impersonation_users_select.xml | 4 ++-- .../engine-processes/import_filters.xml | 2 +- .../petriNets/engine-processes/org_group.xml | 2 +- .../preference_filter_item.xml | 4 ++-- .../engine-processes/preference_item.xml | 4 ++-- .../petriNets/insurance_portal_demo.xml | 4 ++-- .../petriNets/insurance_role_test.xml | 4 ++-- src/main/resources/petriNets/leukemia.xml | 4 ++-- src/main/resources/petriNets/leukemia_en.xml | 2 +- .../resources/petriNets/mortgage/address.xml | 2 +- .../petriNets/mortgage/financial_data.xml | 4 ++-- .../petriNets/mortgage/financial_data_doc.xml | 4 ++-- .../petriNets/mortgage/financial_data_func.xml | 4 ++-- .../resources/petriNets/mortgage/mortgage.xml | 4 ++-- .../petriNets/mortgage/mortgage_modeler.xml | 2 +- .../mortgage/personal_information.xml | 4 ++-- .../resources/petriNets/petriflow_schema.xsd | 4 ++-- src/main/resources/petriNets/posudky.xml | 4 ++-- .../petriNets/taskRef-propagation/child.xml | 2 +- .../petriNets/taskRef-propagation/parent.xml | 2 +- .../petriNets/test_model_immediate_data.xml | 4 ++-- src/main/resources/petriNets/wizard.xml | 4 ++-- src/test/resources/actionref_test.xml | 4 ++-- src/test/resources/all_data.xml | 2 +- src/test/resources/all_data_pdf.xml | 2 +- src/test/resources/arc_order_test.xml | 4 ++-- .../resources/arc_reference_invalid_test.xml | 4 ++-- src/test/resources/arc_reference_test.xml | 4 ++-- src/test/resources/assignRoleMainNet_test_.xml | 4 ++-- .../resources/assignRoleSecondaryNet_test.xml | 4 ++-- .../assign_cancel_finish_with_Case_test.xml | 4 ++-- src/test/resources/autotrigger_taskref.xml | 2 +- src/test/resources/case_choices_test.xml | 4 ++-- src/test/resources/case_search_test.xml | 4 ++-- src/test/resources/caseref_test.xml | 4 ++-- .../change_allowed_nets_action_test.xml | 4 ++-- .../change_caseref_value_action_test.xml | 4 ++-- src/test/resources/constructor_destructor.xml | 2 +- src/test/resources/create_case_locale.xml | 4 ++-- src/test/resources/currency_test.xml | 4 ++-- src/test/resources/data_button_test.xml | 4 ++-- src/test/resources/data_map.xml | 4 ++-- src/test/resources/data_map_2.xml | 4 ++-- src/test/resources/data_service_referenced.xml | 2 +- src/test/resources/data_service_taskref.xml | 2 +- src/test/resources/data_test.xml | 4 ++-- src/test/resources/data_text_validation.xml | 4 ++-- src/test/resources/datagroup_test_layout.xml | 2 +- src/test/resources/enum_list.xml | 4 ++-- .../enumeration_multichoice_options.xml | 2 +- src/test/resources/event_test.xml | 4 ++-- src/test/resources/field_view.xml | 4 ++-- src/test/resources/file_test.xml | 4 ++-- src/test/resources/file_test_net.xml | 4 ++-- src/test/resources/flow.xml | 2 +- src/test/resources/initial_behavior.xml | 2 +- src/test/resources/ipc_bulk.xml | 4 ++-- src/test/resources/ipc_createCase.xml | 4 ++-- src/test/resources/ipc_data.xml | 4 ++-- src/test/resources/ipc_group.xml | 4 ++-- src/test/resources/ipc_set_data.xml | 4 ++-- src/test/resources/ipc_task_search.xml | 4 ++-- src/test/resources/ipc_transition_role.xml | 4 ++-- src/test/resources/ipc_where.xml | 4 ++-- src/test/resources/mapping_test.xml | 4 ++-- .../nae-1272_datagroup_divider_improvement.xml | 4 ++-- src/test/resources/net_clone.xml | 4 ++-- src/test/resources/net_import_1.xml | 4 ++-- src/test/resources/net_import_2.xml | 4 ++-- src/test/resources/org_group.xml | 4 ++-- src/test/resources/pdf_run_action.xml | 4 ++-- src/test/resources/pdf_test_1.xml | 2 +- src/test/resources/pdf_test_2.xml | 4 ++-- src/test/resources/pdf_test_3.xml | 2 +- ...NAE_1305_Loading_na_set_data_pre_button.xml | 4 ++-- .../petriNets/NAE_1382_first_trans_auto.xml | 2 +- .../petriNets/NAE_1382_first_trans_auto_2.xml | 2 +- .../action_delegate_concurrency_test.xml | 4 ++-- src/test/resources/petriNets/all_data_refs.xml | 2 +- .../petriNets/change_field_value_init.xml | 4 ++-- .../petriNets/changed_fields_allowed_nets.xml | 2 +- .../resources/petriNets/data_actions_test.xml | 4 ++-- .../petriNets/dynamic_case_name_test.xml | 4 ++-- .../resources/petriNets/dynamic_choices.xml | 4 ++-- src/test/resources/petriNets/dynamic_init.xml | 4 ++-- .../petriNets/dynamic_validations.xml | 4 ++-- .../dynamic_validations_performance_test.xml | 4 ++-- ...validations_performance_test_comparison.xml | 4 ++-- .../petriNets/function_overloading.xml | 4 ++-- .../petriNets/function_overloading_fail.xml | 4 ++-- .../petriNets/function_overloading_fail_v2.xml | 4 ++-- .../petriNets/function_overloading_v2.xml | 4 ++-- src/test/resources/petriNets/function_res.xml | 4 ++-- .../resources/petriNets/function_res_v2.xml | 4 ++-- src/test/resources/petriNets/function_test.xml | 4 ++-- .../resources/petriNets/function_test_v2.xml | 4 ++-- .../resources/petriNets/groovy_shell_test.xml | 2 +- .../resources/petriNets/impersonation_test.xml | 4 ++-- .../resources/petriNets/importer_upsert.xml | 2 +- src/test/resources/petriNets/mortgage_net.xml | 4 ++-- .../nae_1276_Init_value_as_choice.xml | 2 +- .../petriNets/role_assign_remove_test.xml | 2 +- src/test/resources/petriNets/role_test.xml | 2 +- .../petriNets/validation/valid_boolean.xml | 4 ++-- .../petriNets/validation/valid_date.xml | 4 ++-- .../petriNets/validation/valid_number.xml | 4 ++-- .../petriNets/validation/valid_regex.xml | 4 ++-- .../petriNets/validation/valid_text.xml | 4 ++-- ...ole_permissions_anonymous_role_combined.xml | 2 +- .../role_permissions_anonymous_role_custom.xml | 2 +- ...role_permissions_anonymous_role_defined.xml | 2 +- ...ole_permissions_anonymous_role_disabled.xml | 2 +- ...role_permissions_anonymous_role_missing.xml | 2 +- ...ole_permissions_anonymous_role_negative.xml | 2 +- ...ole_permissions_anonymous_role_reserved.xml | 2 +- ...ole_permissions_anonymous_role_shadowed.xml | 2 +- ...issions_anonymous_role_shadowed_userref.xml | 2 +- ...ssions_anonymous_role_shadowed_usersref.xml | 2 +- ...role_permissions_combined_roles_defined.xml | 2 +- ...le_permissions_combined_roles_undefined.xml | 2 +- .../role_permissions_default_role_combined.xml | 2 +- .../role_permissions_default_role_custom.xml | 2 +- .../role_permissions_default_role_defined.xml | 2 +- .../role_permissions_default_role_disabled.xml | 2 +- .../role_permissions_default_role_missing.xml | 2 +- .../role_permissions_default_role_negative.xml | 2 +- .../role_permissions_default_role_reserved.xml | 2 +- .../role_permissions_default_role_shadowed.xml | 2 +- ...rmissions_default_role_shadowed_userref.xml | 2 +- ...missions_default_role_shadowed_usersref.xml | 2 +- src/test/resources/priloha.xml | 2 +- src/test/resources/process_delete_test.xml | 2 +- src/test/resources/process_search_test.xml | 2 +- src/test/resources/remoteFileField.xml | 4 ++-- src/test/resources/remoteFileListField.xml | 4 ++-- src/test/resources/removeRole_test.xml | 4 ++-- src/test/resources/role_cancel_test.xml | 4 ++-- src/test/resources/rolref_view.xml | 4 ++-- src/test/resources/rule_engine_test.xml | 2 +- src/test/resources/simple_taskref.xml | 2 +- src/test/resources/taskRefLayoutTest.xml | 2 +- src/test/resources/taskRefLayoutTest2.xml | 2 +- src/test/resources/taskRefLayoutTest3.xml | 2 +- src/test/resources/taskRefLayoutTest4.xml | 2 +- .../taskRef_propagation_test_child.xml | 2 +- .../taskRef_propagation_test_parent.xml | 2 +- .../task_authentication_service_test.xml | 2 +- .../task_authorization_service_test.xml | 2 +- ...uthorization_service_test_with_userRefs.xml | 2 +- src/test/resources/task_cancel_net.xml | 4 ++-- src/test/resources/task_events.xml | 4 ++-- src/test/resources/task_reindex_test.xml | 4 ++-- src/test/resources/taskref_demo.xml | 2 +- src/test/resources/taskref_init.xml | 2 +- .../resources/test_autocomplete_dynamic.xml | 2 +- src/test/resources/test_icon_enum.xml | 2 +- .../test_inter_data_actions_dynamic.xml | 4 ++-- .../test_inter_data_actions_static.xml | 4 ++-- src/test/resources/test_setData.xml | 2 +- src/test/resources/this_kw_test.xml | 4 ++-- src/test/resources/user_list.xml | 2 +- src/test/resources/userrefs_test.xml | 2 +- src/test/resources/variable_arc_test.xml | 2 +- src/test/resources/view_permission_test.xml | 2 +- .../view_permission_with_userRefs_test.xml | 2 +- .../workflow_authorization_service_test.xml | 2 +- ...uthorization_service_test_with_userRefs.xml | 2 +- 187 files changed, 300 insertions(+), 300 deletions(-) diff --git a/README.md b/README.md index 11e1847f0c..b3420b67c0 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![License](https://img.shields.io/badge/license-NETGRIF%20Community%20License-green)](https://netgrif.com/license) [![Java](https://img.shields.io/badge/Java-11-red)](https://openjdk.java.net/projects/jdk/11/) -[![Petriflow 1.0.1](https://img.shields.io/badge/Petriflow-1.0.1-0aa8ff)](https://petriflow.com) +[![Petriflow 1.0.1](https://img.shields.io/badge/Petriflow-1.0.1-0aa8ff)](https://petriflow.org) [![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/netgrif/application-engine?sort=semver&display_name=tag)](https://github.com/netgrif/application-engine/releases) [![build](https://github.com/netgrif/application-engine/actions/workflows/master-build.yml/badge.svg)](https://github.com/netgrif/application-engine/actions/workflows/master-build.yml) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=netgrif_application-engine&metric=alert_status)](https://sonarcloud.io/dashboard?id=netgrif_application-engine) @@ -17,7 +17,7 @@ is based on Spring framework with fully complaint Petriflow language interpreter Machine. It can be embedded into Java 11 project or used as a standalone process server. On top of the process server, NAE provides additional components to make integration to your project/environment seamless. -* Petriflow low-code language: [http://petriflow.com](https://petriflow.com) +* Petriflow low-code language: [http://petriflow.org](https://petriflow.org) * Documentation: [https://engine.netgrif.com](https://engine.netgrif.com) * Issue Tracker: [GitHub issues](https://github.com/netgrif/application-engine/issues) diff --git a/docker-compose.yml b/docker-compose.yml index 5f576309bd..bc0f999de3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -40,7 +40,7 @@ services: - "6379:6379" minio: - image: docker.io/bitnami/minio:2022 + image: docker.io/bitnamilegacy/minio:2025.7.23 ports: - '9000:9000' - '9001:9001' diff --git a/docs/README.md b/docs/README.md index 1643abd37a..965a7b3d4d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,7 +2,7 @@ [![GitHub](https://img.shields.io/github/license/netgrif/application-engine)](https://netgrif.com/license) [![Java](https://img.shields.io/badge/Java-11-red)](https://openjdk.java.net/projects/jdk/11/) -[![Petriflow 1.0.1](https://img.shields.io/badge/Petriflow-1.0.1-0aa8ff)](https://petriflow.com) +[![Petriflow 1.0.1](https://img.shields.io/badge/Petriflow-1.0.1-0aa8ff)](https://petriflow.org) [![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/netgrif/application-engine?sort=semver&display_name=tag)](https://github.com/netgrif/application-engine/releases) [![build](https://github.com/netgrif/application-engine/actions/workflows/master-build.yml/badge.svg)](https://github.com/netgrif/application-engine/actions/workflows/master-build.yml) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=netgrif_application-engine&metric=alert_status)](https://sonarcloud.io/dashboard?id=netgrif_application-engine) @@ -17,7 +17,7 @@ is based on Spring framework with fully complaint Petriflow language interpreter Machine. It can be embedded into Java 11 project or used as a standalone process server. On top of the process server, NAE provides additional components to make integration to your project/environment seamless. -* Petriflow low-code language: [http://petriflow.com](https://petriflow.com) +* Petriflow low-code language: [http://petriflow.org](https://petriflow.org) * Documentation: [https://engine.netgrif.com](https://engine.netgrif.com) * Getting Started: [https://engine.netgrif.com/get_started](https://engine.netgrif.com/get_started) * Issue Tracker: [GitHub issues](https://github.com/netgrif/application-engine/issues) @@ -157,4 +157,4 @@ our [Contribution guide](https://github.com/netgrif/application-engine/blob/mast ## License The software is licensed under NETGRIF Community license. You may be found this license -at [the LICENSE file](https://github.com/netgrif/application-engine/blob/master/LICENSE) in the repository. \ No newline at end of file +at [the LICENSE file](https://github.com/netgrif/application-engine/blob/master/LICENSE) in the repository. diff --git a/docs/_media/roles/usersRef_net.xml b/docs/_media/roles/usersRef_net.xml index 39db84655a..29449f4fc3 100644 --- a/docs/_media/roles/usersRef_net.xml +++ b/docs/_media/roles/usersRef_net.xml @@ -1,5 +1,5 @@ - + testing_model TSM Testing Model diff --git a/docs/_media/views/Layout-Guide-compacting-example.xml b/docs/_media/views/Layout-Guide-compacting-example.xml index 69bb578acc..cb44471679 100644 --- a/docs/_media/views/Layout-Guide-compacting-example.xml +++ b/docs/_media/views/Layout-Guide-compacting-example.xml @@ -1,5 +1,5 @@ - + compacting_example EXP Compacting example diff --git a/docs/_media/views/Layout-Guide-flow-example.xml b/docs/_media/views/Layout-Guide-flow-example.xml index 4c7a882aca..d4f46a67d3 100644 --- a/docs/_media/views/Layout-Guide-flow-example.xml +++ b/docs/_media/views/Layout-Guide-flow-example.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> flowExample 1.0.0 FEX diff --git a/docs/_media/views/Layout-Guide-grid-example.xml b/docs/_media/views/Layout-Guide-grid-example.xml index 225fe69f4a..ddd7090e39 100644 --- a/docs/_media/views/Layout-Guide-grid-example.xml +++ b/docs/_media/views/Layout-Guide-grid-example.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> gridExample 1.0.0 GEX diff --git a/docs/_media/views/Layout-Guide-legacy-example.xml b/docs/_media/views/Layout-Guide-legacy-example.xml index db750f8e6d..dc5e885e0d 100644 --- a/docs/_media/views/Layout-Guide-legacy-example.xml +++ b/docs/_media/views/Layout-Guide-legacy-example.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> legacyExample 1.0.0 LEX diff --git a/docs/_navbar.md b/docs/_navbar.md index 73170c3718..68a50824ba 100644 --- a/docs/_navbar.md +++ b/docs/_navbar.md @@ -1,3 +1,3 @@ * [Try Engine](https://demo.netgrif.com/) * [Try Builder](https://builder.netgrif.com) -* [Petriflow](https://petriflow.com) \ No newline at end of file +* [Petriflow](https://petriflow.org) diff --git a/docs/events/case_event.md b/docs/events/case_event.md index 1762cbfbab..7fbcf4f8d2 100644 --- a/docs/events/case_event.md +++ b/docs/events/case_event.md @@ -15,7 +15,7 @@ case. ```xml + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> case_events Case Events example CEE @@ -53,7 +53,7 @@ User lists cannot be associated with the create event, since they don't exist be ```xml + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> case_events Case Events example CEE @@ -88,7 +88,7 @@ execution. ```xml + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> case_events Case Events example CEE @@ -126,7 +126,7 @@ User lists can be associated with the delete event ```xml + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> case_events Case Events example CEE @@ -144,4 +144,4 @@ User lists can be associated with the delete event ``` - \ No newline at end of file + diff --git a/docs/events/process_event.md b/docs/events/process_event.md index 1428ceed67..85b4f87df9 100644 --- a/docs/events/process_event.md +++ b/docs/events/process_event.md @@ -15,7 +15,7 @@ If you want to change data on another cases, you need to use the `setData` funct ```xml - + constructor_destructor Constructor and Destructor CAD @@ -37,4 +37,4 @@ If you want to change data on another cases, you need to use the `setData` funct ``` - \ No newline at end of file + diff --git a/docs/roles/permissions.md b/docs/roles/permissions.md index 27c408013b..b6976cf9f2 100644 --- a/docs/roles/permissions.md +++ b/docs/roles/permissions.md @@ -23,7 +23,7 @@ Since the developer may want to apply these roles to "all" transitions a shortha The default and the anonymous role can be applied independently of each other. ``` - + ... true true @@ -234,7 +234,7 @@ In the XML model of the process, you can define roles as child elements of the r element. The role is connected to the role reference via the role's ID. ``` - + ... process_role @@ -256,7 +256,7 @@ Permission documentation can be found [here](#Permissions). Roles can be referen - as a child element of the `document` tag for referencing roles on cases: ``` - + ... process_role @@ -272,7 +272,7 @@ Permission documentation can be found [here](#Permissions). Roles can be referen - as a child element of the `transition` tag for referencing roles on tasks: ``` - + ... ... @@ -296,7 +296,7 @@ given user list) to Petriflow objects and their actions. User list can be define defined, as child element of the root **document** element: ``` - + ... user_list_1 @@ -316,7 +316,7 @@ Permission documentation can be found [here](#Permissions). User list can be ref - as a child element of the `document` tag for referencing user list on cases: ``` - + ... user_list_1 @@ -332,7 +332,7 @@ Permission documentation can be found [here](#Permissions). User list can be ref - as a child element of the `transition` tag for referencing user list on tasks: ``` - + ... ... @@ -381,7 +381,7 @@ Each reference element has a child element called **caseLogic**, which can be us permissions for case created from process as follows: ``` - + ... process_role @@ -429,7 +429,7 @@ transition** element. Each reference element has a child element called **logic* permissions for task created from transition as follows: ``` - + ... ... diff --git a/docs/roles/shared_roles.md b/docs/roles/shared_roles.md index 3ef64b026a..880c98bb7f 100644 --- a/docs/roles/shared_roles.md +++ b/docs/roles/shared_roles.md @@ -4,7 +4,7 @@ To use a shared role in Petri nets first we must declare it. We can declare it a attribute set to ``true``: ```xml + xsi:noNamespaceSchemaLocation='https://petriflow.org/petriflow.schema.xsd'> nae_1927 ... @@ -33,4 +33,4 @@ Then we can reference it as usual: ... ``` When importing a Petri net, the importer checks, whether the global role has already existed. -If not, the importer creates one. If there has been already one, the importer passes it to a the newly created net. \ No newline at end of file +If not, the importer creates one. If there has been already one, the importer passes it to a the newly created net. diff --git a/src/main/resources/petriNets/all_data.xml b/src/main/resources/petriNets/all_data.xml index f65ed4ae65..fa77752807 100644 --- a/src/main/resources/petriNets/all_data.xml +++ b/src/main/resources/petriNets/all_data.xml @@ -1,5 +1,5 @@ - + data/all_data All Data ALL diff --git a/src/main/resources/petriNets/datamap.xml b/src/main/resources/petriNets/datamap.xml index b4f58f4bad..559235aa56 100644 --- a/src/main/resources/petriNets/datamap.xml +++ b/src/main/resources/petriNets/datamap.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> data/taskref_demo2 1.0.0 TRD @@ -320,4 +320,4 @@ - \ No newline at end of file + diff --git a/src/main/resources/petriNets/engine-processes/dashboard.xml b/src/main/resources/petriNets/engine-processes/dashboard.xml index 6b6aa0c1c3..832052df50 100644 --- a/src/main/resources/petriNets/engine-processes/dashboard.xml +++ b/src/main/resources/petriNets/engine-processes/dashboard.xml @@ -1,4 +1,4 @@ - + dashboard DSH Dashboard @@ -290,4 +290,4 @@ t2 1 - \ No newline at end of file + diff --git a/src/main/resources/petriNets/engine-processes/dashboard_tile.xml b/src/main/resources/petriNets/engine-processes/dashboard_tile.xml index c6928563d3..ad0fe2b682 100644 --- a/src/main/resources/petriNets/engine-processes/dashboard_tile.xml +++ b/src/main/resources/petriNets/engine-processes/dashboard_tile.xml @@ -1,4 +1,4 @@ - + dashboard_tile DST Dashboard Tile @@ -1544,4 +1544,4 @@ 660 - \ No newline at end of file + diff --git a/src/main/resources/petriNets/engine-processes/export_filters.xml b/src/main/resources/petriNets/engine-processes/export_filters.xml index e057c9cb80..43e332135c 100644 --- a/src/main/resources/petriNets/engine-processes/export_filters.xml +++ b/src/main/resources/petriNets/engine-processes/export_filters.xml @@ -1,5 +1,5 @@ - + export_filters FTE Export of filters diff --git a/src/main/resources/petriNets/engine-processes/filter.xml b/src/main/resources/petriNets/engine-processes/filter.xml index 0be99e1940..4563e2d4c5 100644 --- a/src/main/resources/petriNets/engine-processes/filter.xml +++ b/src/main/resources/petriNets/engine-processes/filter.xml @@ -1,5 +1,5 @@ - + filter FTR Filter @@ -1091,4 +1091,4 @@ p5 1 - \ No newline at end of file + diff --git a/src/main/resources/petriNets/engine-processes/impersonation_config.xml b/src/main/resources/petriNets/engine-processes/impersonation_config.xml index 1e0778a8c6..25645cab61 100644 --- a/src/main/resources/petriNets/engine-processes/impersonation_config.xml +++ b/src/main/resources/petriNets/engine-processes/impersonation_config.xml @@ -1,4 +1,4 @@ - + impersonation_config 1.0.0 IPC @@ -794,4 +794,4 @@ t3 1 - \ No newline at end of file + diff --git a/src/main/resources/petriNets/engine-processes/impersonation_users_select.xml b/src/main/resources/petriNets/engine-processes/impersonation_users_select.xml index aa0d581e14..84cda085a4 100644 --- a/src/main/resources/petriNets/engine-processes/impersonation_users_select.xml +++ b/src/main/resources/petriNets/engine-processes/impersonation_users_select.xml @@ -1,4 +1,4 @@ - + impersonation_users_select 1.0.0 IPU @@ -93,4 +93,4 @@ t2_delegate - \ No newline at end of file + diff --git a/src/main/resources/petriNets/engine-processes/import_filters.xml b/src/main/resources/petriNets/engine-processes/import_filters.xml index ba4f173f29..af74e2c35f 100644 --- a/src/main/resources/petriNets/engine-processes/import_filters.xml +++ b/src/main/resources/petriNets/engine-processes/import_filters.xml @@ -1,5 +1,5 @@ - + import_filters FTI Import of filters diff --git a/src/main/resources/petriNets/engine-processes/org_group.xml b/src/main/resources/petriNets/engine-processes/org_group.xml index f11d3fac83..b23102b481 100644 --- a/src/main/resources/petriNets/engine-processes/org_group.xml +++ b/src/main/resources/petriNets/engine-processes/org_group.xml @@ -1,5 +1,5 @@ - + org_group 1.0.0 GRP diff --git a/src/main/resources/petriNets/engine-processes/preference_filter_item.xml b/src/main/resources/petriNets/engine-processes/preference_filter_item.xml index b1cfec77d5..2e12b82a51 100644 --- a/src/main/resources/petriNets/engine-processes/preference_filter_item.xml +++ b/src/main/resources/petriNets/engine-processes/preference_filter_item.xml @@ -1,5 +1,5 @@ - + preference_filter_item PFI Preference filter item @@ -313,7 +313,7 @@ default_headers Set default headers - + allowed_nets Allowed nets diff --git a/src/main/resources/petriNets/engine-processes/preference_item.xml b/src/main/resources/petriNets/engine-processes/preference_item.xml index b14ef9d3fc..b3a4b55676 100644 --- a/src/main/resources/petriNets/engine-processes/preference_item.xml +++ b/src/main/resources/petriNets/engine-processes/preference_item.xml @@ -1,4 +1,4 @@ - + preference_item PRI Preference Item @@ -2683,4 +2683,4 @@ children_order 1 - \ No newline at end of file + diff --git a/src/main/resources/petriNets/insurance_portal_demo.xml b/src/main/resources/petriNets/insurance_portal_demo.xml index 57740ab2b9..ee5d43f959 100644 --- a/src/main/resources/petriNets/insurance_portal_demo.xml +++ b/src/main/resources/petriNets/insurance_portal_demo.xml @@ -1,7 +1,7 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> examples/insurance/insurance IPD Insurance Portal Demo @@ -14647,4 +14647,4 @@ 2728 1 - \ No newline at end of file + diff --git a/src/main/resources/petriNets/insurance_role_test.xml b/src/main/resources/petriNets/insurance_role_test.xml index f9bd098e3f..aa7732e0fb 100644 --- a/src/main/resources/petriNets/insurance_role_test.xml +++ b/src/main/resources/petriNets/insurance_role_test.xml @@ -1,7 +1,7 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> false @@ -14651,4 +14651,4 @@ 2728 1 - \ No newline at end of file + diff --git a/src/main/resources/petriNets/leukemia.xml b/src/main/resources/petriNets/leukemia.xml index 8597ba75d0..cde6b23433 100644 --- a/src/main/resources/petriNets/leukemia.xml +++ b/src/main/resources/petriNets/leukemia.xml @@ -1,5 +1,5 @@ - + examples/leukemia/leukemia 1.0.0 LEU @@ -269,4 +269,4 @@ - \ No newline at end of file + diff --git a/src/main/resources/petriNets/leukemia_en.xml b/src/main/resources/petriNets/leukemia_en.xml index 0bee06055c..ffe7581f33 100644 --- a/src/main/resources/petriNets/leukemia_en.xml +++ b/src/main/resources/petriNets/leukemia_en.xml @@ -1,5 +1,5 @@ - + leukemia/leu 1.0.0 LEU diff --git a/src/main/resources/petriNets/mortgage/address.xml b/src/main/resources/petriNets/mortgage/address.xml index 6cd0a197f2..2b10003288 100644 --- a/src/main/resources/petriNets/mortgage/address.xml +++ b/src/main/resources/petriNets/mortgage/address.xml @@ -1,5 +1,5 @@ - + address ADD Address diff --git a/src/main/resources/petriNets/mortgage/financial_data.xml b/src/main/resources/petriNets/mortgage/financial_data.xml index c7299cc494..035fd5ab53 100644 --- a/src/main/resources/petriNets/mortgage/financial_data.xml +++ b/src/main/resources/petriNets/mortgage/financial_data.xml @@ -1,5 +1,5 @@ - + financial_data FIN Financial Data @@ -188,4 +188,4 @@ financial_data 1 - \ No newline at end of file + diff --git a/src/main/resources/petriNets/mortgage/financial_data_doc.xml b/src/main/resources/petriNets/mortgage/financial_data_doc.xml index f0bc35d2a2..f61bd98057 100644 --- a/src/main/resources/petriNets/mortgage/financial_data_doc.xml +++ b/src/main/resources/petriNets/mortgage/financial_data_doc.xml @@ -1,5 +1,5 @@ - + financial_data_doc FDC Financial Data Doc @@ -58,4 +58,4 @@ - \ No newline at end of file + diff --git a/src/main/resources/petriNets/mortgage/financial_data_func.xml b/src/main/resources/petriNets/mortgage/financial_data_func.xml index 52b9795eee..400b860421 100644 --- a/src/main/resources/petriNets/mortgage/financial_data_func.xml +++ b/src/main/resources/petriNets/mortgage/financial_data_func.xml @@ -1,5 +1,5 @@ - + financial_data_func FIN Financial Data @@ -192,4 +192,4 @@ financial_data 1 - \ No newline at end of file + diff --git a/src/main/resources/petriNets/mortgage/mortgage.xml b/src/main/resources/petriNets/mortgage/mortgage.xml index 1421b03602..ac51fd99e9 100644 --- a/src/main/resources/petriNets/mortgage/mortgage.xml +++ b/src/main/resources/petriNets/mortgage/mortgage.xml @@ -1,5 +1,5 @@ - + mortgage MOR Mortgage @@ -919,4 +919,4 @@ 16 1 - \ No newline at end of file + diff --git a/src/main/resources/petriNets/mortgage/mortgage_modeler.xml b/src/main/resources/petriNets/mortgage/mortgage_modeler.xml index 5446579f92..fadb1042f6 100644 --- a/src/main/resources/petriNets/mortgage/mortgage_modeler.xml +++ b/src/main/resources/petriNets/mortgage/mortgage_modeler.xml @@ -1,5 +1,5 @@ - + mortgage 1.0.0 MOR diff --git a/src/main/resources/petriNets/mortgage/personal_information.xml b/src/main/resources/petriNets/mortgage/personal_information.xml index ccc45ed691..693af2b11f 100644 --- a/src/main/resources/petriNets/mortgage/personal_information.xml +++ b/src/main/resources/petriNets/mortgage/personal_information.xml @@ -1,5 +1,5 @@ - + personal_information PER Personal Information @@ -65,4 +65,4 @@ - \ No newline at end of file + diff --git a/src/main/resources/petriNets/petriflow_schema.xsd b/src/main/resources/petriNets/petriflow_schema.xsd index 28e88a26d6..0bbd2530d6 100644 --- a/src/main/resources/petriNets/petriflow_schema.xsd +++ b/src/main/resources/petriNets/petriflow_schema.xsd @@ -3,6 +3,6 @@ - + - \ No newline at end of file + diff --git a/src/main/resources/petriNets/posudky.xml b/src/main/resources/petriNets/posudky.xml index b91919f3a4..3256813e8c 100644 --- a/src/main/resources/petriNets/posudky.xml +++ b/src/main/resources/petriNets/posudky.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> management/posudky Posudky PSD @@ -348,4 +348,4 @@ 19 1 - \ No newline at end of file + diff --git a/src/main/resources/petriNets/taskRef-propagation/child.xml b/src/main/resources/petriNets/taskRef-propagation/child.xml index c4143c3048..06830ac7ee 100644 --- a/src/main/resources/petriNets/taskRef-propagation/child.xml +++ b/src/main/resources/petriNets/taskRef-propagation/child.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> child 1.0.0 CHL diff --git a/src/main/resources/petriNets/taskRef-propagation/parent.xml b/src/main/resources/petriNets/taskRef-propagation/parent.xml index bd50af718c..84e9ed7e2e 100644 --- a/src/main/resources/petriNets/taskRef-propagation/parent.xml +++ b/src/main/resources/petriNets/taskRef-propagation/parent.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> parent 1.0.0 PAR diff --git a/src/main/resources/petriNets/test_model_immediate_data.xml b/src/main/resources/petriNets/test_model_immediate_data.xml index 7ef5521780..eefe3b4c0a 100644 --- a/src/main/resources/petriNets/test_model_immediate_data.xml +++ b/src/main/resources/petriNets/test_model_immediate_data.xml @@ -1,5 +1,5 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> true Nový prípad @@ -326,4 +326,4 @@ 7 1 - \ No newline at end of file + diff --git a/src/main/resources/petriNets/wizard.xml b/src/main/resources/petriNets/wizard.xml index c4acf2f9dd..05d3d8cda4 100644 --- a/src/main/resources/petriNets/wizard.xml +++ b/src/main/resources/petriNets/wizard.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> text text @@ -243,4 +243,4 @@ 3 1 - \ No newline at end of file + diff --git a/src/test/resources/actionref_test.xml b/src/test/resources/actionref_test.xml index 16ec8c1d9d..a9693d9cbd 100644 --- a/src/test/resources/actionref_test.xml +++ b/src/test/resources/actionref_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> actionref_test.xml TST Test @@ -125,4 +125,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/all_data.xml b/src/test/resources/all_data.xml index 076374ca25..b103851102 100644 --- a/src/test/resources/all_data.xml +++ b/src/test/resources/all_data.xml @@ -1,5 +1,5 @@ - + all_data All Data ALL diff --git a/src/test/resources/all_data_pdf.xml b/src/test/resources/all_data_pdf.xml index e2d8a3f82d..408e03836b 100644 --- a/src/test/resources/all_data_pdf.xml +++ b/src/test/resources/all_data_pdf.xml @@ -1,5 +1,5 @@ - + all_data All Data ALL diff --git a/src/test/resources/arc_order_test.xml b/src/test/resources/arc_order_test.xml index 27d9be69bb..8a63dff1e6 100644 --- a/src/test/resources/arc_order_test.xml +++ b/src/test/resources/arc_order_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> arc_order_test.xml TST Test @@ -35,4 +35,4 @@ 1 1 - \ No newline at end of file + diff --git a/src/test/resources/arc_reference_invalid_test.xml b/src/test/resources/arc_reference_invalid_test.xml index 85ade00852..92d8331625 100644 --- a/src/test/resources/arc_reference_invalid_test.xml +++ b/src/test/resources/arc_reference_invalid_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> arc_ref_invalid_test ARi @@ -29,4 +29,4 @@ 1 3 - \ No newline at end of file + diff --git a/src/test/resources/arc_reference_test.xml b/src/test/resources/arc_reference_test.xml index 63a241b672..8ddfde6422 100644 --- a/src/test/resources/arc_reference_test.xml +++ b/src/test/resources/arc_reference_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> arc_ref_test ART Arc ref test @@ -29,4 +29,4 @@ 1 2 - \ No newline at end of file + diff --git a/src/test/resources/assignRoleMainNet_test_.xml b/src/test/resources/assignRoleMainNet_test_.xml index 8c5bbd271d..53a21a1703 100644 --- a/src/test/resources/assignRoleMainNet_test_.xml +++ b/src/test/resources/assignRoleMainNet_test_.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> main ORG main @@ -21,4 +21,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/assignRoleSecondaryNet_test.xml b/src/test/resources/assignRoleSecondaryNet_test.xml index f81db9ed38..493dfab39a 100644 --- a/src/test/resources/assignRoleSecondaryNet_test.xml +++ b/src/test/resources/assignRoleSecondaryNet_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> secondary ORG secondary @@ -14,4 +14,4 @@ admin_secondary admin_secondary - \ No newline at end of file + diff --git a/src/test/resources/assign_cancel_finish_with_Case_test.xml b/src/test/resources/assign_cancel_finish_with_Case_test.xml index fade333fb7..eb87a06e4c 100644 --- a/src/test/resources/assign_cancel_finish_with_Case_test.xml +++ b/src/test/resources/assign_cancel_finish_with_Case_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> assign_cancel_finish_with_Case_net TST Test @@ -46,4 +46,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/autotrigger_taskref.xml b/src/test/resources/autotrigger_taskref.xml index a64af65355..eaa031db8c 100644 --- a/src/test/resources/autotrigger_taskref.xml +++ b/src/test/resources/autotrigger_taskref.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> Praca PRC Práca diff --git a/src/test/resources/case_choices_test.xml b/src/test/resources/case_choices_test.xml index 86a3ebc33f..8c9f355a07 100644 --- a/src/test/resources/case_choices_test.xml +++ b/src/test/resources/case_choices_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -50,4 +50,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/case_search_test.xml b/src/test/resources/case_search_test.xml index 86c2e06ffe..7c8db1daac 100644 --- a/src/test/resources/case_search_test.xml +++ b/src/test/resources/case_search_test.xml @@ -1,5 +1,5 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> case_search_test.xml TST Test @@ -70,4 +70,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/caseref_test.xml b/src/test/resources/caseref_test.xml index cc558f0a17..3df3f896cf 100644 --- a/src/test/resources/caseref_test.xml +++ b/src/test/resources/caseref_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> caseref_test.xml TST Test @@ -128,4 +128,4 @@ 8 1 - \ No newline at end of file + diff --git a/src/test/resources/change_allowed_nets_action_test.xml b/src/test/resources/change_allowed_nets_action_test.xml index c47c34aa97..9c9400a16c 100644 --- a/src/test/resources/change_allowed_nets_action_test.xml +++ b/src/test/resources/change_allowed_nets_action_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -55,4 +55,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/change_caseref_value_action_test.xml b/src/test/resources/change_caseref_value_action_test.xml index e82f26c761..10ebcae9a9 100644 --- a/src/test/resources/change_caseref_value_action_test.xml +++ b/src/test/resources/change_caseref_value_action_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> change_value TST Test @@ -81,4 +81,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/constructor_destructor.xml b/src/test/resources/constructor_destructor.xml index 043e290721..25629c073a 100644 --- a/src/test/resources/constructor_destructor.xml +++ b/src/test/resources/constructor_destructor.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> constructor_destructor CAD Constructor and Destructor diff --git a/src/test/resources/create_case_locale.xml b/src/test/resources/create_case_locale.xml index 99fe3759b8..00676b3b6a 100644 --- a/src/test/resources/create_case_locale.xml +++ b/src/test/resources/create_case_locale.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> currency_test.xml TST Test @@ -13,4 +13,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/currency_test.xml b/src/test/resources/currency_test.xml index 1fdf059b64..f806c11307 100644 --- a/src/test/resources/currency_test.xml +++ b/src/test/resources/currency_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> currency_test.xml TST Test @@ -23,4 +23,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/data_button_test.xml b/src/test/resources/data_button_test.xml index 8d92916484..95d4c80ca9 100644 --- a/src/test/resources/data_button_test.xml +++ b/src/test/resources/data_button_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -63,4 +63,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/data_map.xml b/src/test/resources/data_map.xml index cb94764563..b87ee09ec3 100644 --- a/src/test/resources/data_map.xml +++ b/src/test/resources/data_map.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -27,4 +27,4 @@ Zweite Option Dritte Option - \ No newline at end of file + diff --git a/src/test/resources/data_map_2.xml b/src/test/resources/data_map_2.xml index e415c4e3e2..29fdf4f8d4 100644 --- a/src/test/resources/data_map_2.xml +++ b/src/test/resources/data_map_2.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -30,4 +30,4 @@ Zweite Option Dritte Option - \ No newline at end of file + diff --git a/src/test/resources/data_service_referenced.xml b/src/test/resources/data_service_referenced.xml index ce077430b8..01343e84a8 100644 --- a/src/test/resources/data_service_referenced.xml +++ b/src/test/resources/data_service_referenced.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> referenced 1.0.0 RFD diff --git a/src/test/resources/data_service_taskref.xml b/src/test/resources/data_service_taskref.xml index 071cef8ced..d20c3fbe90 100644 --- a/src/test/resources/data_service_taskref.xml +++ b/src/test/resources/data_service_taskref.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> refering 1.0.0 REF diff --git a/src/test/resources/data_test.xml b/src/test/resources/data_test.xml index a9e4feb878..6fe0ca7115 100644 --- a/src/test/resources/data_test.xml +++ b/src/test/resources/data_test.xml @@ -1,5 +1,5 @@ - + 1 TES Data test @@ -269,4 +269,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/data_text_validation.xml b/src/test/resources/data_text_validation.xml index 2c9f5a9d08..cf90b3a361 100644 --- a/src/test/resources/data_text_validation.xml +++ b/src/test/resources/data_text_validation.xml @@ -1,6 +1,6 @@ F + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -265,4 +265,4 @@ F - \ No newline at end of file + diff --git a/src/test/resources/datagroup_test_layout.xml b/src/test/resources/datagroup_test_layout.xml index 4e5a0b2566..29202ba0c2 100644 --- a/src/test/resources/datagroup_test_layout.xml +++ b/src/test/resources/datagroup_test_layout.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> datagroup_test DGT Data group test diff --git a/src/test/resources/enum_list.xml b/src/test/resources/enum_list.xml index 808a464f4d..f9f5893bff 100644 --- a/src/test/resources/enum_list.xml +++ b/src/test/resources/enum_list.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -143,4 +143,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/enumeration_multichoice_options.xml b/src/test/resources/enumeration_multichoice_options.xml index e5ca5d5b9d..e0bcfc26be 100644 --- a/src/test/resources/enumeration_multichoice_options.xml +++ b/src/test/resources/enumeration_multichoice_options.xml @@ -1,5 +1,5 @@ - + enumeration_multichoice_options Enumeration/multichoice options EMO diff --git a/src/test/resources/event_test.xml b/src/test/resources/event_test.xml index 7821605b8b..49715c88f7 100644 --- a/src/test/resources/event_test.xml +++ b/src/test/resources/event_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -121,4 +121,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/field_view.xml b/src/test/resources/field_view.xml index 7c4dda54c1..4a771ee186 100644 --- a/src/test/resources/field_view.xml +++ b/src/test/resources/field_view.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -31,4 +31,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/file_test.xml b/src/test/resources/file_test.xml index 8bd07b4683..7557663904 100644 --- a/src/test/resources/file_test.xml +++ b/src/test/resources/file_test.xml @@ -1,5 +1,5 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -61,4 +61,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/file_test_net.xml b/src/test/resources/file_test_net.xml index 80ea01e33d..247c8fdf1c 100644 --- a/src/test/resources/file_test_net.xml +++ b/src/test/resources/file_test_net.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -101,4 +101,4 @@ 6 1 - \ No newline at end of file + diff --git a/src/test/resources/flow.xml b/src/test/resources/flow.xml index 54540fb0ff..4a21ac59be 100644 --- a/src/test/resources/flow.xml +++ b/src/test/resources/flow.xml @@ -1,4 +1,4 @@ - + new_model NEW New Model diff --git a/src/test/resources/initial_behavior.xml b/src/test/resources/initial_behavior.xml index e169b58a7b..2684213557 100644 --- a/src/test/resources/initial_behavior.xml +++ b/src/test/resources/initial_behavior.xml @@ -1,5 +1,5 @@ - + initial_behavior Initial behavior IBH diff --git a/src/test/resources/ipc_bulk.xml b/src/test/resources/ipc_bulk.xml index 11a6eac441..f6e150eeeb 100644 --- a/src/test/resources/ipc_bulk.xml +++ b/src/test/resources/ipc_bulk.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -35,4 +35,4 @@ 0 - \ No newline at end of file + diff --git a/src/test/resources/ipc_createCase.xml b/src/test/resources/ipc_createCase.xml index b78a5f1dee..09a03611dc 100644 --- a/src/test/resources/ipc_createCase.xml +++ b/src/test/resources/ipc_createCase.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> create_case_net TST Test @@ -29,4 +29,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/ipc_data.xml b/src/test/resources/ipc_data.xml index eea9eca299..4a99b3de73 100644 --- a/src/test/resources/ipc_data.xml +++ b/src/test/resources/ipc_data.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -89,4 +89,4 @@ 3 1 - \ No newline at end of file + diff --git a/src/test/resources/ipc_group.xml b/src/test/resources/ipc_group.xml index 356b05e4cd..85f837c1c5 100644 --- a/src/test/resources/ipc_group.xml +++ b/src/test/resources/ipc_group.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -42,4 +42,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/ipc_set_data.xml b/src/test/resources/ipc_set_data.xml index 97e5fff993..1e94beee85 100644 --- a/src/test/resources/ipc_set_data.xml +++ b/src/test/resources/ipc_set_data.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -71,4 +71,4 @@ 2 1 - \ No newline at end of file + diff --git a/src/test/resources/ipc_task_search.xml b/src/test/resources/ipc_task_search.xml index d893e2de58..c9211dbde0 100644 --- a/src/test/resources/ipc_task_search.xml +++ b/src/test/resources/ipc_task_search.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> create_case_net TST Test @@ -56,4 +56,4 @@ 0 - \ No newline at end of file + diff --git a/src/test/resources/ipc_transition_role.xml b/src/test/resources/ipc_transition_role.xml index 44780c237e..696d51769e 100644 --- a/src/test/resources/ipc_transition_role.xml +++ b/src/test/resources/ipc_transition_role.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -41,4 +41,4 @@ 0 - \ No newline at end of file + diff --git a/src/test/resources/ipc_where.xml b/src/test/resources/ipc_where.xml index e2202dacad..c9d6991a51 100644 --- a/src/test/resources/ipc_where.xml +++ b/src/test/resources/ipc_where.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -76,4 +76,4 @@ 0 - \ No newline at end of file + diff --git a/src/test/resources/mapping_test.xml b/src/test/resources/mapping_test.xml index d7ab7541dd..960b0f5f94 100755 --- a/src/test/resources/mapping_test.xml +++ b/src/test/resources/mapping_test.xml @@ -1,7 +1,7 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -898,4 +898,4 @@ 42 1 - \ No newline at end of file + diff --git a/src/test/resources/nae-1272_datagroup_divider_improvement.xml b/src/test/resources/nae-1272_datagroup_divider_improvement.xml index 4acaa3bdea..82ebc005f2 100644 --- a/src/test/resources/nae-1272_datagroup_divider_improvement.xml +++ b/src/test/resources/nae-1272_datagroup_divider_improvement.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> nae_1272 NAE NAE-1272 Datagroup divider improvement @@ -154,4 +154,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/net_clone.xml b/src/test/resources/net_clone.xml index 31c6e18510..4f54118ad3 100644 --- a/src/test/resources/net_clone.xml +++ b/src/test/resources/net_clone.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -73,4 +73,4 @@ 340 - \ No newline at end of file + diff --git a/src/test/resources/net_import_1.xml b/src/test/resources/net_import_1.xml index 1b6eb07a99..3f0d055e00 100644 --- a/src/test/resources/net_import_1.xml +++ b/src/test/resources/net_import_1.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> new_model 1.0.0 NEW @@ -55,4 +55,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/net_import_2.xml b/src/test/resources/net_import_2.xml index 520d3c4404..8593fc96c1 100644 --- a/src/test/resources/net_import_2.xml +++ b/src/test/resources/net_import_2.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> new_model 1.0.0 NEW @@ -33,4 +33,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/org_group.xml b/src/test/resources/org_group.xml index 0bbc98fe2a..5f03f2ceb6 100644 --- a/src/test/resources/org_group.xml +++ b/src/test/resources/org_group.xml @@ -1,5 +1,5 @@ - + org_group 1.0.0 GRP @@ -489,4 +489,4 @@ 28 1 - \ No newline at end of file + diff --git a/src/test/resources/pdf_run_action.xml b/src/test/resources/pdf_run_action.xml index 2797aa5fda..cba7da838c 100644 --- a/src/test/resources/pdf_run_action.xml +++ b/src/test/resources/pdf_run_action.xml @@ -1,4 +1,4 @@ - + 1 TES PDF test @@ -446,4 +446,4 @@ 1_delegate - \ No newline at end of file + diff --git a/src/test/resources/pdf_test_1.xml b/src/test/resources/pdf_test_1.xml index 7ffd56d668..fd4222adc5 100644 --- a/src/test/resources/pdf_test_1.xml +++ b/src/test/resources/pdf_test_1.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> 1 TES Data test diff --git a/src/test/resources/pdf_test_2.xml b/src/test/resources/pdf_test_2.xml index 24b53b5c11..15831035c5 100644 --- a/src/test/resources/pdf_test_2.xml +++ b/src/test/resources/pdf_test_2.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -143,4 +143,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/pdf_test_3.xml b/src/test/resources/pdf_test_3.xml index a7ad5d0223..4755336cba 100644 --- a/src/test/resources/pdf_test_3.xml +++ b/src/test/resources/pdf_test_3.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> new_model 1.0.0 PER diff --git a/src/test/resources/petriNets/NAE_1305_Loading_na_set_data_pre_button.xml b/src/test/resources/petriNets/NAE_1305_Loading_na_set_data_pre_button.xml index df867dae6f..729ea089a5 100644 --- a/src/test/resources/petriNets/NAE_1305_Loading_na_set_data_pre_button.xml +++ b/src/test/resources/petriNets/NAE_1305_Loading_na_set_data_pre_button.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> NAE_1305_Loading_na_set_data_pre_button ALL NAE_1305_Loading_na_set_data_pre_button @@ -127,4 +127,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/petriNets/NAE_1382_first_trans_auto.xml b/src/test/resources/petriNets/NAE_1382_first_trans_auto.xml index 7fcf657455..ea2f20a717 100644 --- a/src/test/resources/petriNets/NAE_1382_first_trans_auto.xml +++ b/src/test/resources/petriNets/NAE_1382_first_trans_auto.xml @@ -1,5 +1,5 @@ - + NAE_1382_first_trans_auto ERR New Model diff --git a/src/test/resources/petriNets/NAE_1382_first_trans_auto_2.xml b/src/test/resources/petriNets/NAE_1382_first_trans_auto_2.xml index c6e4777672..e2bf2005b9 100644 --- a/src/test/resources/petriNets/NAE_1382_first_trans_auto_2.xml +++ b/src/test/resources/petriNets/NAE_1382_first_trans_auto_2.xml @@ -1,5 +1,5 @@ - + NAE_1382_first_trans_auto ERR New Model diff --git a/src/test/resources/petriNets/action_delegate_concurrency_test.xml b/src/test/resources/petriNets/action_delegate_concurrency_test.xml index 4523fe8d79..935577e75f 100644 --- a/src/test/resources/petriNets/action_delegate_concurrency_test.xml +++ b/src/test/resources/petriNets/action_delegate_concurrency_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> action_delegate_concurrency_test 1.0.0 TRE @@ -36,4 +36,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/petriNets/all_data_refs.xml b/src/test/resources/petriNets/all_data_refs.xml index 69eb782f3b..c261c14cbd 100644 --- a/src/test/resources/petriNets/all_data_refs.xml +++ b/src/test/resources/petriNets/all_data_refs.xml @@ -1,5 +1,5 @@ - + all_data All Data ALL diff --git a/src/test/resources/petriNets/change_field_value_init.xml b/src/test/resources/petriNets/change_field_value_init.xml index f34a6c21fe..6bef2b17ba 100644 --- a/src/test/resources/petriNets/change_field_value_init.xml +++ b/src/test/resources/petriNets/change_field_value_init.xml @@ -1,5 +1,5 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> change_field_value_init change_field_value_init true @@ -121,4 +121,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/petriNets/changed_fields_allowed_nets.xml b/src/test/resources/petriNets/changed_fields_allowed_nets.xml index d88677a8a3..1f3c66814f 100644 --- a/src/test/resources/petriNets/changed_fields_allowed_nets.xml +++ b/src/test/resources/petriNets/changed_fields_allowed_nets.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> changed_fields_allowed_nets NEW New Model diff --git a/src/test/resources/petriNets/data_actions_test.xml b/src/test/resources/petriNets/data_actions_test.xml index e80ffca7ae..5cc07d079d 100644 --- a/src/test/resources/petriNets/data_actions_test.xml +++ b/src/test/resources/petriNets/data_actions_test.xml @@ -1,5 +1,5 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> data_actions_test 1.0.0 tst @@ -118,4 +118,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/petriNets/dynamic_case_name_test.xml b/src/test/resources/petriNets/dynamic_case_name_test.xml index cca7a064cf..21594887fa 100644 --- a/src/test/resources/petriNets/dynamic_case_name_test.xml +++ b/src/test/resources/petriNets/dynamic_case_name_test.xml @@ -1,5 +1,5 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> dynamic_choices dynamic_choices true @@ -30,4 +30,4 @@ auto - \ No newline at end of file + diff --git a/src/test/resources/petriNets/dynamic_choices.xml b/src/test/resources/petriNets/dynamic_choices.xml index 89ffc46aed..655bcccdc1 100644 --- a/src/test/resources/petriNets/dynamic_choices.xml +++ b/src/test/resources/petriNets/dynamic_choices.xml @@ -1,5 +1,5 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> dynamic_choices dynamic_choices true @@ -63,4 +63,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/petriNets/dynamic_init.xml b/src/test/resources/petriNets/dynamic_init.xml index d7593bed06..586c88c14e 100644 --- a/src/test/resources/petriNets/dynamic_init.xml +++ b/src/test/resources/petriNets/dynamic_init.xml @@ -1,5 +1,5 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> dynamic_init dynamic_init true @@ -100,4 +100,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/petriNets/dynamic_validations.xml b/src/test/resources/petriNets/dynamic_validations.xml index 892960bfa6..0e6c6da274 100644 --- a/src/test/resources/petriNets/dynamic_validations.xml +++ b/src/test/resources/petriNets/dynamic_validations.xml @@ -1,5 +1,5 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> dynamic_validations dynamic_validations true @@ -129,4 +129,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/petriNets/dynamic_validations_performance_test.xml b/src/test/resources/petriNets/dynamic_validations_performance_test.xml index e4d349a71d..d33ba5dc10 100644 --- a/src/test/resources/petriNets/dynamic_validations_performance_test.xml +++ b/src/test/resources/petriNets/dynamic_validations_performance_test.xml @@ -1,5 +1,5 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> dynamic_validations_performance dynamic_validations_performance true @@ -3301,4 +3301,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/petriNets/dynamic_validations_performance_test_comparison.xml b/src/test/resources/petriNets/dynamic_validations_performance_test_comparison.xml index 9b1684aed8..b9a7973bd9 100644 --- a/src/test/resources/petriNets/dynamic_validations_performance_test_comparison.xml +++ b/src/test/resources/petriNets/dynamic_validations_performance_test_comparison.xml @@ -1,5 +1,5 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> dynamic_validations_performance_comparison dynamic_validations_performance_comparison true @@ -3275,4 +3275,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/petriNets/function_overloading.xml b/src/test/resources/petriNets/function_overloading.xml index 0053e9e7d6..a3e58babc9 100644 --- a/src/test/resources/petriNets/function_overloading.xml +++ b/src/test/resources/petriNets/function_overloading.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> function_overloading FOF Test @@ -46,4 +46,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/petriNets/function_overloading_fail.xml b/src/test/resources/petriNets/function_overloading_fail.xml index e35e537652..5df7b1cb63 100644 --- a/src/test/resources/petriNets/function_overloading_fail.xml +++ b/src/test/resources/petriNets/function_overloading_fail.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> function_overloading_fail FOF Test @@ -30,4 +30,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/petriNets/function_overloading_fail_v2.xml b/src/test/resources/petriNets/function_overloading_fail_v2.xml index edb1a05ff2..88966c403d 100644 --- a/src/test/resources/petriNets/function_overloading_fail_v2.xml +++ b/src/test/resources/petriNets/function_overloading_fail_v2.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> function_overloading_fail FOF Test @@ -30,4 +30,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/petriNets/function_overloading_v2.xml b/src/test/resources/petriNets/function_overloading_v2.xml index 2a1f005e7c..3648f5c118 100644 --- a/src/test/resources/petriNets/function_overloading_v2.xml +++ b/src/test/resources/petriNets/function_overloading_v2.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> function_overloading FOF Test @@ -46,4 +46,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/petriNets/function_res.xml b/src/test/resources/petriNets/function_res.xml index 85c8b4ee10..6faa882219 100644 --- a/src/test/resources/petriNets/function_res.xml +++ b/src/test/resources/petriNets/function_res.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> function_res SFR Test @@ -41,4 +41,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/petriNets/function_res_v2.xml b/src/test/resources/petriNets/function_res_v2.xml index f92e7c9db6..d2c665b384 100644 --- a/src/test/resources/petriNets/function_res_v2.xml +++ b/src/test/resources/petriNets/function_res_v2.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> function_res SFR Test @@ -38,4 +38,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/petriNets/function_test.xml b/src/test/resources/petriNets/function_test.xml index 6a4964e2f3..90d20fe2fc 100644 --- a/src/test/resources/petriNets/function_test.xml +++ b/src/test/resources/petriNets/function_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> function_test SFT Test @@ -148,4 +148,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/petriNets/function_test_v2.xml b/src/test/resources/petriNets/function_test_v2.xml index 31af030796..26976d7670 100644 --- a/src/test/resources/petriNets/function_test_v2.xml +++ b/src/test/resources/petriNets/function_test_v2.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> function_test SFT Test @@ -143,4 +143,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/petriNets/groovy_shell_test.xml b/src/test/resources/petriNets/groovy_shell_test.xml index ab6db998f8..cc257a4f00 100644 --- a/src/test/resources/petriNets/groovy_shell_test.xml +++ b/src/test/resources/petriNets/groovy_shell_test.xml @@ -1,5 +1,5 @@ - + new_model NEW New Model diff --git a/src/test/resources/petriNets/impersonation_test.xml b/src/test/resources/petriNets/impersonation_test.xml index 7a0d199328..5ed2917f12 100644 --- a/src/test/resources/petriNets/impersonation_test.xml +++ b/src/test/resources/petriNets/impersonation_test.xml @@ -1,4 +1,4 @@ - + impersonation_test 1.0.0 IMP @@ -115,4 +115,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/petriNets/importer_upsert.xml b/src/test/resources/petriNets/importer_upsert.xml index 1d3165e3af..e3db46a1ca 100644 --- a/src/test/resources/petriNets/importer_upsert.xml +++ b/src/test/resources/petriNets/importer_upsert.xml @@ -1,5 +1,5 @@ - + importer_upsert importer_upsert IMP diff --git a/src/test/resources/petriNets/mortgage_net.xml b/src/test/resources/petriNets/mortgage_net.xml index 9e4ddcedd6..98204cb74a 100644 --- a/src/test/resources/petriNets/mortgage_net.xml +++ b/src/test/resources/petriNets/mortgage_net.xml @@ -1,5 +1,5 @@ - + mortgage MOR Mortgage @@ -916,4 +916,4 @@ 16 1 - \ No newline at end of file + diff --git a/src/test/resources/petriNets/nae_1276_Init_value_as_choice.xml b/src/test/resources/petriNets/nae_1276_Init_value_as_choice.xml index 835b186ab5..743de9c89d 100644 --- a/src/test/resources/petriNets/nae_1276_Init_value_as_choice.xml +++ b/src/test/resources/petriNets/nae_1276_Init_value_as_choice.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> new_init_test NIT New init test diff --git a/src/test/resources/petriNets/role_assign_remove_test.xml b/src/test/resources/petriNets/role_assign_remove_test.xml index 82553b484f..32b2d2cc2f 100644 --- a/src/test/resources/petriNets/role_assign_remove_test.xml +++ b/src/test/resources/petriNets/role_assign_remove_test.xml @@ -1,5 +1,5 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> role_assign_remove_test Role Assign And Remove RAR diff --git a/src/test/resources/petriNets/role_test.xml b/src/test/resources/petriNets/role_test.xml index 93f08f0f7a..8d2ad74328 100644 --- a/src/test/resources/petriNets/role_test.xml +++ b/src/test/resources/petriNets/role_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> role_test IMP role_test diff --git a/src/test/resources/petriNets/validation/valid_boolean.xml b/src/test/resources/petriNets/validation/valid_boolean.xml index 77c4c1e505..bfccf85113 100644 --- a/src/test/resources/petriNets/validation/valid_boolean.xml +++ b/src/test/resources/petriNets/validation/valid_boolean.xml @@ -1,4 +1,4 @@ - + valid_boolean VB1 Validacia BooleanFieldu @@ -81,4 +81,4 @@ p2 1 - \ No newline at end of file + diff --git a/src/test/resources/petriNets/validation/valid_date.xml b/src/test/resources/petriNets/validation/valid_date.xml index 008cd3b31b..4a16b5b7c5 100644 --- a/src/test/resources/petriNets/validation/valid_date.xml +++ b/src/test/resources/petriNets/validation/valid_date.xml @@ -1,5 +1,5 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> valid_date VD1 Validacia DateFieldu @@ -226,4 +226,4 @@ p2 1 - \ No newline at end of file + diff --git a/src/test/resources/petriNets/validation/valid_number.xml b/src/test/resources/petriNets/validation/valid_number.xml index bbcb706343..658d78546a 100644 --- a/src/test/resources/petriNets/validation/valid_number.xml +++ b/src/test/resources/petriNets/validation/valid_number.xml @@ -1,5 +1,5 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> valid_number VN1 Validacia NumberFieldu @@ -232,4 +232,4 @@ p2 1 - \ No newline at end of file + diff --git a/src/test/resources/petriNets/validation/valid_regex.xml b/src/test/resources/petriNets/validation/valid_regex.xml index 2b9dd0239a..3b9fa45e0d 100644 --- a/src/test/resources/petriNets/validation/valid_regex.xml +++ b/src/test/resources/petriNets/validation/valid_regex.xml @@ -1,4 +1,4 @@ - + valid_regex VR1 Validacia regexov @@ -201,4 +201,4 @@ p2 1 - \ No newline at end of file + diff --git a/src/test/resources/petriNets/validation/valid_text.xml b/src/test/resources/petriNets/validation/valid_text.xml index 6935722ddc..f3f5477a87 100644 --- a/src/test/resources/petriNets/validation/valid_text.xml +++ b/src/test/resources/petriNets/validation/valid_text.xml @@ -1,5 +1,5 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> valid_text VT1 Validacia TextFieldu @@ -182,4 +182,4 @@ p2 1 - \ No newline at end of file + diff --git a/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_combined.xml b/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_combined.xml index 3165ab2c23..e3c19481ed 100644 --- a/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_combined.xml +++ b/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_combined.xml @@ -1,5 +1,5 @@ - + permissions_test_anonymous_6 PTA Permissions test anonymous 6 diff --git a/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_custom.xml b/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_custom.xml index d464a3d94b..8a95b6330c 100644 --- a/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_custom.xml +++ b/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_custom.xml @@ -1,5 +1,5 @@ - + permissions_test_anonymous_4 PTA Permissions test anonymous 4 diff --git a/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_defined.xml b/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_defined.xml index b9451d5e14..429117b420 100644 --- a/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_defined.xml +++ b/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_defined.xml @@ -1,5 +1,5 @@ - + permissions_test_anonymous_2 PTA Permissions test anonymous 2 diff --git a/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_disabled.xml b/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_disabled.xml index becf2e971a..865ed55afe 100644 --- a/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_disabled.xml +++ b/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_disabled.xml @@ -1,5 +1,5 @@ - + permissions_test_anonymous_10 PTA Permissions test anonymous 10 diff --git a/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_missing.xml b/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_missing.xml index ca1bb62492..edae7d805d 100644 --- a/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_missing.xml +++ b/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_missing.xml @@ -1,5 +1,5 @@ - + permissions_test_anonymous_7 PTA Permissions test anonymous 7 diff --git a/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_negative.xml b/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_negative.xml index 00afb2c593..1f9f219b1a 100644 --- a/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_negative.xml +++ b/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_negative.xml @@ -1,5 +1,5 @@ - + permissions_test_anonymous_5 PTA Permissions test anonymous 5 diff --git a/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_reserved.xml b/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_reserved.xml index cea6fafa26..a6c04bb3e8 100644 --- a/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_reserved.xml +++ b/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_reserved.xml @@ -1,5 +1,5 @@ - + permissions_test_anonymous_8 PTA Permissions test anonymous 8 diff --git a/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_shadowed.xml b/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_shadowed.xml index 4f9bd8cf4c..c6c7fc729d 100644 --- a/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_shadowed.xml +++ b/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_shadowed.xml @@ -1,5 +1,5 @@ - + permissions_test_anonymous_3 PTA Permissions test anonymous 3 diff --git a/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_shadowed_userref.xml b/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_shadowed_userref.xml index 1e69657be3..aebd7cc7e0 100644 --- a/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_shadowed_userref.xml +++ b/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_shadowed_userref.xml @@ -1,5 +1,5 @@ - + permissions_test_anonymous_9 PTA Permissions test anonymous 9 diff --git a/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_shadowed_usersref.xml b/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_shadowed_usersref.xml index cdc455d6da..390626b56f 100644 --- a/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_shadowed_usersref.xml +++ b/src/test/resources/predefinedPermissions/role_permissions_anonymous_role_shadowed_usersref.xml @@ -1,5 +1,5 @@ - + permissions_test_anonymous_11 PTA Permissions test anonymous 11 diff --git a/src/test/resources/predefinedPermissions/role_permissions_combined_roles_defined.xml b/src/test/resources/predefinedPermissions/role_permissions_combined_roles_defined.xml index bc74887e06..a67f850b7f 100644 --- a/src/test/resources/predefinedPermissions/role_permissions_combined_roles_defined.xml +++ b/src/test/resources/predefinedPermissions/role_permissions_combined_roles_defined.xml @@ -1,5 +1,5 @@ - + permissions_test_combined_2 PTC Permissions test combined 2 diff --git a/src/test/resources/predefinedPermissions/role_permissions_combined_roles_undefined.xml b/src/test/resources/predefinedPermissions/role_permissions_combined_roles_undefined.xml index 446834a927..854bbe68be 100644 --- a/src/test/resources/predefinedPermissions/role_permissions_combined_roles_undefined.xml +++ b/src/test/resources/predefinedPermissions/role_permissions_combined_roles_undefined.xml @@ -1,5 +1,5 @@ - + permissions_test_combined_1 PTC Permissions test combined 1 diff --git a/src/test/resources/predefinedPermissions/role_permissions_default_role_combined.xml b/src/test/resources/predefinedPermissions/role_permissions_default_role_combined.xml index 061a9adcba..e46e7cf7bf 100644 --- a/src/test/resources/predefinedPermissions/role_permissions_default_role_combined.xml +++ b/src/test/resources/predefinedPermissions/role_permissions_default_role_combined.xml @@ -1,5 +1,5 @@ - + permissions_test_default_6 PTD Permissions test default 6 diff --git a/src/test/resources/predefinedPermissions/role_permissions_default_role_custom.xml b/src/test/resources/predefinedPermissions/role_permissions_default_role_custom.xml index a8820f38b4..9938318bed 100644 --- a/src/test/resources/predefinedPermissions/role_permissions_default_role_custom.xml +++ b/src/test/resources/predefinedPermissions/role_permissions_default_role_custom.xml @@ -1,5 +1,5 @@ - + permissions_test_default_4 PTD Permissions test default 4 diff --git a/src/test/resources/predefinedPermissions/role_permissions_default_role_defined.xml b/src/test/resources/predefinedPermissions/role_permissions_default_role_defined.xml index 9e877ec7a8..2cede6a9f9 100644 --- a/src/test/resources/predefinedPermissions/role_permissions_default_role_defined.xml +++ b/src/test/resources/predefinedPermissions/role_permissions_default_role_defined.xml @@ -1,5 +1,5 @@ - + permissions_test_default_2 PTD Permissions test default 2 diff --git a/src/test/resources/predefinedPermissions/role_permissions_default_role_disabled.xml b/src/test/resources/predefinedPermissions/role_permissions_default_role_disabled.xml index 490145a3fd..72a3c59eca 100644 --- a/src/test/resources/predefinedPermissions/role_permissions_default_role_disabled.xml +++ b/src/test/resources/predefinedPermissions/role_permissions_default_role_disabled.xml @@ -1,5 +1,5 @@ - + permissions_test_default_10 PTD Permissions test default 10 diff --git a/src/test/resources/predefinedPermissions/role_permissions_default_role_missing.xml b/src/test/resources/predefinedPermissions/role_permissions_default_role_missing.xml index c66effbf3d..a2ff5e4f30 100644 --- a/src/test/resources/predefinedPermissions/role_permissions_default_role_missing.xml +++ b/src/test/resources/predefinedPermissions/role_permissions_default_role_missing.xml @@ -1,5 +1,5 @@ - + permissions_test_default_7 PTD Permissions test default 7 diff --git a/src/test/resources/predefinedPermissions/role_permissions_default_role_negative.xml b/src/test/resources/predefinedPermissions/role_permissions_default_role_negative.xml index c7574cd641..27615b0c51 100644 --- a/src/test/resources/predefinedPermissions/role_permissions_default_role_negative.xml +++ b/src/test/resources/predefinedPermissions/role_permissions_default_role_negative.xml @@ -1,5 +1,5 @@ - + permissions_test_default_5 PTD Permissions test default 5 diff --git a/src/test/resources/predefinedPermissions/role_permissions_default_role_reserved.xml b/src/test/resources/predefinedPermissions/role_permissions_default_role_reserved.xml index 5756c8d623..555b6a488d 100644 --- a/src/test/resources/predefinedPermissions/role_permissions_default_role_reserved.xml +++ b/src/test/resources/predefinedPermissions/role_permissions_default_role_reserved.xml @@ -1,5 +1,5 @@ - + permissions_test_default_8 PTD Permissions test default 8 diff --git a/src/test/resources/predefinedPermissions/role_permissions_default_role_shadowed.xml b/src/test/resources/predefinedPermissions/role_permissions_default_role_shadowed.xml index 9c3437a052..930fa09614 100644 --- a/src/test/resources/predefinedPermissions/role_permissions_default_role_shadowed.xml +++ b/src/test/resources/predefinedPermissions/role_permissions_default_role_shadowed.xml @@ -1,5 +1,5 @@ - + permissions_test_default_3 PTD Permissions test default 3 diff --git a/src/test/resources/predefinedPermissions/role_permissions_default_role_shadowed_userref.xml b/src/test/resources/predefinedPermissions/role_permissions_default_role_shadowed_userref.xml index 131a28f280..7e095f8533 100644 --- a/src/test/resources/predefinedPermissions/role_permissions_default_role_shadowed_userref.xml +++ b/src/test/resources/predefinedPermissions/role_permissions_default_role_shadowed_userref.xml @@ -1,5 +1,5 @@ - + permissions_test_default_9 PTD Permissions test default 9 diff --git a/src/test/resources/predefinedPermissions/role_permissions_default_role_shadowed_usersref.xml b/src/test/resources/predefinedPermissions/role_permissions_default_role_shadowed_usersref.xml index 3b2a7359c9..25298a9a7e 100644 --- a/src/test/resources/predefinedPermissions/role_permissions_default_role_shadowed_usersref.xml +++ b/src/test/resources/predefinedPermissions/role_permissions_default_role_shadowed_usersref.xml @@ -1,5 +1,5 @@ - + permissions_test_default_11 PTD Permissions test default 11 diff --git a/src/test/resources/priloha.xml b/src/test/resources/priloha.xml index 1645d2474c..7e15542391 100644 --- a/src/test/resources/priloha.xml +++ b/src/test/resources/priloha.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation='https://petriflow.org/petriflow.schema.xsd'> priloha IPR 1.0.0 diff --git a/src/test/resources/process_delete_test.xml b/src/test/resources/process_delete_test.xml index b921eef0be..3859937941 100644 --- a/src/test/resources/process_delete_test.xml +++ b/src/test/resources/process_delete_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> processDeleteTest PDT Process Delete Test diff --git a/src/test/resources/process_search_test.xml b/src/test/resources/process_search_test.xml index f92fedb067..3f53f9069f 100644 --- a/src/test/resources/process_search_test.xml +++ b/src/test/resources/process_search_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> processSearchTest PST Process Search Test diff --git a/src/test/resources/remoteFileField.xml b/src/test/resources/remoteFileField.xml index 0176593c89..a77c644a0a 100644 --- a/src/test/resources/remoteFileField.xml +++ b/src/test/resources/remoteFileField.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> remote_file_field_net TST Test @@ -30,4 +30,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/remoteFileListField.xml b/src/test/resources/remoteFileListField.xml index bd7ae00ca1..ac1da8b04e 100644 --- a/src/test/resources/remoteFileListField.xml +++ b/src/test/resources/remoteFileListField.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> remote_file_list_field_net TST Remote file list field test @@ -27,4 +27,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/removeRole_test.xml b/src/test/resources/removeRole_test.xml index f4ad76410b..3a7c78d768 100644 --- a/src/test/resources/removeRole_test.xml +++ b/src/test/resources/removeRole_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -32,4 +32,4 @@ manager manager - \ No newline at end of file + diff --git a/src/test/resources/role_cancel_test.xml b/src/test/resources/role_cancel_test.xml index e0700dbb56..54f10be23a 100644 --- a/src/test/resources/role_cancel_test.xml +++ b/src/test/resources/role_cancel_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -35,4 +35,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/rolref_view.xml b/src/test/resources/rolref_view.xml index 4988b7d9d3..173725a088 100644 --- a/src/test/resources/rolref_view.xml +++ b/src/test/resources/rolref_view.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -47,4 +47,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/rule_engine_test.xml b/src/test/resources/rule_engine_test.xml index 787e57a747..7c1ad63912 100644 --- a/src/test/resources/rule_engine_test.xml +++ b/src/test/resources/rule_engine_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> rule_engine_test 1.0.0 RLN diff --git a/src/test/resources/simple_taskref.xml b/src/test/resources/simple_taskref.xml index 423f9100e8..4278089966 100644 --- a/src/test/resources/simple_taskref.xml +++ b/src/test/resources/simple_taskref.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> simple_taskref STR simple taskref diff --git a/src/test/resources/taskRefLayoutTest.xml b/src/test/resources/taskRefLayoutTest.xml index f726cb094b..f1432c7776 100644 --- a/src/test/resources/taskRefLayoutTest.xml +++ b/src/test/resources/taskRefLayoutTest.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> taskRefLayoutTest 1.0.0 NEW diff --git a/src/test/resources/taskRefLayoutTest2.xml b/src/test/resources/taskRefLayoutTest2.xml index 2d73ac9567..d62f9f7e23 100644 --- a/src/test/resources/taskRefLayoutTest2.xml +++ b/src/test/resources/taskRefLayoutTest2.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> taskRefLayoutTest2 1.0.0 NEW diff --git a/src/test/resources/taskRefLayoutTest3.xml b/src/test/resources/taskRefLayoutTest3.xml index 8cede4b1de..72e576ff67 100644 --- a/src/test/resources/taskRefLayoutTest3.xml +++ b/src/test/resources/taskRefLayoutTest3.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> taskRefLayoutTest3 1.0.0 NEW diff --git a/src/test/resources/taskRefLayoutTest4.xml b/src/test/resources/taskRefLayoutTest4.xml index dca0763f44..1052ed6150 100644 --- a/src/test/resources/taskRefLayoutTest4.xml +++ b/src/test/resources/taskRefLayoutTest4.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> taskRefLayoutTest3 1.0.0 NEW diff --git a/src/test/resources/taskRef_propagation_test_child.xml b/src/test/resources/taskRef_propagation_test_child.xml index 2a56be1d16..405bf10a10 100644 --- a/src/test/resources/taskRef_propagation_test_child.xml +++ b/src/test/resources/taskRef_propagation_test_child.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> child 1.0.0 CHL diff --git a/src/test/resources/taskRef_propagation_test_parent.xml b/src/test/resources/taskRef_propagation_test_parent.xml index 04d00788b9..f9373aa515 100644 --- a/src/test/resources/taskRef_propagation_test_parent.xml +++ b/src/test/resources/taskRef_propagation_test_parent.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> parent 1.0.0 PAR diff --git a/src/test/resources/task_authentication_service_test.xml b/src/test/resources/task_authentication_service_test.xml index 03e79da942..fb6cb8c6cd 100644 --- a/src/test/resources/task_authentication_service_test.xml +++ b/src/test/resources/task_authentication_service_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> 1 TST TaskAuthenticationService test diff --git a/src/test/resources/task_authorization_service_test.xml b/src/test/resources/task_authorization_service_test.xml index 6662e550a5..4ace8c27f1 100644 --- a/src/test/resources/task_authorization_service_test.xml +++ b/src/test/resources/task_authorization_service_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> wst WST WorkflowAuthorizationService test diff --git a/src/test/resources/task_authorization_service_test_with_userRefs.xml b/src/test/resources/task_authorization_service_test_with_userRefs.xml index 67ca085953..ef5c5f338d 100644 --- a/src/test/resources/task_authorization_service_test_with_userRefs.xml +++ b/src/test/resources/task_authorization_service_test_with_userRefs.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> wst_usersRef WSU WorkflowAuthorizationService test diff --git a/src/test/resources/task_cancel_net.xml b/src/test/resources/task_cancel_net.xml index 7c4673e3a8..d73f36eae3 100644 --- a/src/test/resources/task_cancel_net.xml +++ b/src/test/resources/task_cancel_net.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -69,4 +69,4 @@ 1 - \ No newline at end of file + diff --git a/src/test/resources/task_events.xml b/src/test/resources/task_events.xml index 41a9af4629..a74caa83bd 100644 --- a/src/test/resources/task_events.xml +++ b/src/test/resources/task_events.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -34,4 +34,4 @@ 0 - \ No newline at end of file + diff --git a/src/test/resources/task_reindex_test.xml b/src/test/resources/task_reindex_test.xml index 3b924d3d2e..925de789dd 100644 --- a/src/test/resources/task_reindex_test.xml +++ b/src/test/resources/task_reindex_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> task_reindex_test TST task_reindex_test @@ -78,4 +78,4 @@ 9 1 - \ No newline at end of file + diff --git a/src/test/resources/taskref_demo.xml b/src/test/resources/taskref_demo.xml index 4e38bcf135..8b6b22abdf 100644 --- a/src/test/resources/taskref_demo.xml +++ b/src/test/resources/taskref_demo.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> taskref_demo 1.0.0 TRD diff --git a/src/test/resources/taskref_init.xml b/src/test/resources/taskref_init.xml index 7adfa558b4..c91cd8351d 100644 --- a/src/test/resources/taskref_init.xml +++ b/src/test/resources/taskref_init.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> taskref_init_test TIT Task ref init test net diff --git a/src/test/resources/test_autocomplete_dynamic.xml b/src/test/resources/test_autocomplete_dynamic.xml index 44ca561442..03d660ad8a 100644 --- a/src/test/resources/test_autocomplete_dynamic.xml +++ b/src/test/resources/test_autocomplete_dynamic.xml @@ -1,5 +1,5 @@ - + test_autocomplete_dynamic Autocomplete Dynamic Net ADN diff --git a/src/test/resources/test_icon_enum.xml b/src/test/resources/test_icon_enum.xml index 957a2c9640..f9313270fc 100644 --- a/src/test/resources/test_icon_enum.xml +++ b/src/test/resources/test_icon_enum.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test_icon_enum Icon Enum Net IEN diff --git a/src/test/resources/test_inter_data_actions_dynamic.xml b/src/test/resources/test_inter_data_actions_dynamic.xml index 43b188af49..26635c21d7 100644 --- a/src/test/resources/test_inter_data_actions_dynamic.xml +++ b/src/test/resources/test_inter_data_actions_dynamic.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test TST Test @@ -69,4 +69,4 @@ 2 1 - \ No newline at end of file + diff --git a/src/test/resources/test_inter_data_actions_static.xml b/src/test/resources/test_inter_data_actions_static.xml index fa1e6a4873..f9434af77b 100644 --- a/src/test/resources/test_inter_data_actions_static.xml +++ b/src/test/resources/test_inter_data_actions_static.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> test_inter_data_actions_static.xml TST Test @@ -85,4 +85,4 @@ 1 1 - \ No newline at end of file + diff --git a/src/test/resources/test_setData.xml b/src/test/resources/test_setData.xml index 86fde7e400..04c0563d5d 100644 --- a/src/test/resources/test_setData.xml +++ b/src/test/resources/test_setData.xml @@ -1,4 +1,4 @@ - + test_setData TSD TestSetData diff --git a/src/test/resources/this_kw_test.xml b/src/test/resources/this_kw_test.xml index 7f3f648425..5ef7628e5e 100644 --- a/src/test/resources/this_kw_test.xml +++ b/src/test/resources/this_kw_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> this_kw_test TKW This keyword test net @@ -71,4 +71,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/user_list.xml b/src/test/resources/user_list.xml index 7f5c223400..3741fd56d8 100644 --- a/src/test/resources/user_list.xml +++ b/src/test/resources/user_list.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> user_list User list ULT diff --git a/src/test/resources/userrefs_test.xml b/src/test/resources/userrefs_test.xml index e2b0c67e70..438ff582ea 100644 --- a/src/test/resources/userrefs_test.xml +++ b/src/test/resources/userrefs_test.xml @@ -1,5 +1,5 @@ - + testing_model TSM Testing Model diff --git a/src/test/resources/variable_arc_test.xml b/src/test/resources/variable_arc_test.xml index 2c2426bf1a..199883e5db 100644 --- a/src/test/resources/variable_arc_test.xml +++ b/src/test/resources/variable_arc_test.xml @@ -1,5 +1,5 @@ - + variable_arc_test.xml TST Test diff --git a/src/test/resources/view_permission_test.xml b/src/test/resources/view_permission_test.xml index 48271ca624..3d4819fe42 100644 --- a/src/test/resources/view_permission_test.xml +++ b/src/test/resources/view_permission_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> vpt VPT ViewPermissionTest test diff --git a/src/test/resources/view_permission_with_userRefs_test.xml b/src/test/resources/view_permission_with_userRefs_test.xml index adeea8ba53..1adbecfb8e 100644 --- a/src/test/resources/view_permission_with_userRefs_test.xml +++ b/src/test/resources/view_permission_with_userRefs_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> vpt_userRefs VPT ViewPermissionTest test diff --git a/src/test/resources/workflow_authorization_service_test.xml b/src/test/resources/workflow_authorization_service_test.xml index 0d690ed872..064aa7d689 100644 --- a/src/test/resources/workflow_authorization_service_test.xml +++ b/src/test/resources/workflow_authorization_service_test.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> wst WST WorkflowAuthorizationService test diff --git a/src/test/resources/workflow_authorization_service_test_with_userRefs.xml b/src/test/resources/workflow_authorization_service_test_with_userRefs.xml index f88c05d638..3801963e3b 100644 --- a/src/test/resources/workflow_authorization_service_test_with_userRefs.xml +++ b/src/test/resources/workflow_authorization_service_test_with_userRefs.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://petriflow.org/petriflow.schema.xsd"> wst_usersRef WSU WorkflowAuthorizationService test From c7afa3f86b7b541886f76806023e46159c39b17d Mon Sep 17 00:00:00 2001 From: Milan Mladoniczky <6153201+tuplle@users.noreply.github.com> Date: Mon, 1 Dec 2025 22:30:42 +0100 Subject: [PATCH 49/92] Update MinIO image version in GitHub workflows - Replaced MinIO image from `bitnami/minio:2022` to `bitnamilegacy/minio:2025.7.23` in `pr-build.yml`, `release-build.yml`, and `master-build.yml`. - Ensures workflows use the updated and compatible MinIO legacy image across all builds. --- .github/workflows/master-build.yml | 4 ++-- .github/workflows/pr-build.yml | 4 ++-- .github/workflows/release-build.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/master-build.yml b/.github/workflows/master-build.yml index 07e94d9276..79c87fd6a6 100644 --- a/.github/workflows/master-build.yml +++ b/.github/workflows/master-build.yml @@ -53,7 +53,7 @@ jobs: options: -e="discovery.type=single-node" -e="xpack.security.enabled=false" --health-cmd="curl http://localhost:9200/_cluster/health" --health-interval=10s --health-timeout=5s --health-retries=10 minio: - image: docker.io/bitnami/minio:2022 + image: docker.io/bitnamilegacy/minio:2025.7.23 ports: - 9000:9000 - 9001:9001 @@ -143,4 +143,4 @@ jobs: with: add: docs pathspec_error_handling: exitImmediately - message: 'CI - Update documentation' \ No newline at end of file + message: 'CI - Update documentation' diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index 975cff98da..0005231f06 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -52,7 +52,7 @@ jobs: options: -e="discovery.type=single-node" -e="xpack.security.enabled=false" --health-cmd="curl http://localhost:9200/_cluster/health" --health-interval=10s --health-timeout=5s --health-retries=10 minio: - image: docker.io/bitnami/minio:2022 + image: docker.io/bitnamilegacy/minio:2025.7.23 ports: - 9000:9000 - 9001:9001 @@ -110,4 +110,4 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: mvn -B test \ No newline at end of file + run: mvn -B test diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index d76764a729..6bb03e0077 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -64,7 +64,7 @@ jobs: options: -e="discovery.type=single-node" -e="xpack.security.enabled=false" --health-cmd="curl http://localhost:9200/_cluster/health" --health-interval=10s --health-timeout=5s --health-retries=10 minio: - image: docker.io/bitnami/minio:2022 + image: docker.io/bitnamilegacy/minio:2025.7.23 ports: - 9000:9000 - 9001:9001 From dc9036cd687a4b69a81548c58a14b0343f6574d4 Mon Sep 17 00:00:00 2001 From: Milan Mladoniczky <6153201+tuplle@users.noreply.github.com> Date: Mon, 8 Dec 2025 10:36:21 +0100 Subject: [PATCH 50/92] [NAE-2246] - Enable Redis TLS & Configure Redis Sentinel - Introduced Redis sentinel configuration with support for master and nodes. - Added lazy initialization for `IElasticCaseMappingService` and `IElasticCaseService`. --- .../configuration/SessionConfiguration.java | 82 +++++++++++++++++-- .../workflow/service/WorkflowService.java | 2 + src/main/resources/application-dev.properties | 6 +- 3 files changed, 84 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/configuration/SessionConfiguration.java b/src/main/java/com/netgrif/application/engine/configuration/SessionConfiguration.java index afe31ba8f4..b986e087e3 100644 --- a/src/main/java/com/netgrif/application/engine/configuration/SessionConfiguration.java +++ b/src/main/java/com/netgrif/application/engine/configuration/SessionConfiguration.java @@ -1,16 +1,24 @@ package com.netgrif.application.engine.configuration; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisNode; +import org.springframework.data.redis.connection.RedisSentinelConfiguration; import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.jedis.JedisClientConfiguration; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import org.springframework.security.web.session.HttpSessionEventPublisher; import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession; import org.springframework.session.web.http.HeaderHttpSessionIdResolver; import org.springframework.session.web.http.HttpSessionIdResolver; +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j @Configuration @EnableRedisHttpSession(redisNamespace = "spring:session:${spring.session.redis.namespace}") @ConditionalOnProperty( @@ -31,17 +39,81 @@ public class SessionConfiguration { @Value("${spring.session.redis.password:#{null}}") private String password; + @Value("${spring.session.redis.ssl:#{null}}") + private Boolean ssl; + + @Value("${spring.redis.sentinel.master:#{null}}") + private String sentinelMasterName; + + @Value("${spring.redis.sentinel.nodes:#{null}}") + private List sentinelNodes; + + @Value("${spring.redis.sentinel.port:#{null}}") + private Integer sentinelPort = 26379; + + @Value("${spring.redis.sentinel.username:#{null}}") + private String sentinelUsername; + + @Value("${spring.redis.sentinel.password:#{null}}") + private String sentinelPassword; @Bean public JedisConnectionFactory jedisConnectionFactory() { - hostName = hostName == null ? "localhost" : hostName; - port = port == null || port == 0 ? 6379 : port; + if (sentinelMasterName != null && !sentinelMasterName.isEmpty()) { + return redisSentinelConfiguration(); + } else { + return standaloneRedisConfiguration(); + } + } + + protected JedisConnectionFactory standaloneRedisConfiguration() { + String hostName = this.hostName == null ? "localhost" : this.hostName; + int port = this.port == 0 ? 6379 : this.port; RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(hostName, port); - if(username != null && password !=null && !username.isEmpty() && !password.isEmpty()){ + if (hasCredentials(username, password)) { redisStandaloneConfiguration.setUsername(username); redisStandaloneConfiguration.setPassword(password); } - return new JedisConnectionFactory(redisStandaloneConfiguration); + JedisClientConfiguration clientConfiguration = jedisClientConfiguration(); + return new JedisConnectionFactory(redisStandaloneConfiguration, clientConfiguration); + } + + protected JedisConnectionFactory redisSentinelConfiguration() { + RedisSentinelConfiguration sentinelConfiguration = new RedisSentinelConfiguration(); + sentinelConfiguration.setMaster(sentinelMasterName); + List nodes = sentinelNodes.stream().map(node -> { + try { + return RedisNode.fromString(node); + } catch (Exception e) { + log.warn("Parsing redis sentinel node {} has failed. Trying to use the value as an address without port and adding default sentinel port {}", node, sentinelPort, e); + return new RedisNode(node, sentinelPort); + } + }).collect(Collectors.toList()); + sentinelConfiguration.setSentinels(nodes); + + if (hasCredentials(username, password)) { + sentinelConfiguration.setUsername(username); + sentinelConfiguration.setPassword(password); + } + if (hasCredentials(sentinelUsername, sentinelPassword)) { + sentinelConfiguration.setSentinelUsername(sentinelUsername); + sentinelConfiguration.setSentinelPassword(sentinelPassword); + } + + JedisClientConfiguration clientConfiguration = jedisClientConfiguration(); + return new JedisConnectionFactory(sentinelConfiguration, clientConfiguration); + } + + protected JedisClientConfiguration jedisClientConfiguration() { + if (ssl) { + return JedisClientConfiguration.builder().useSsl().build(); + } + return JedisClientConfiguration.defaultConfiguration(); + } + + private boolean hasCredentials(String username, String password) { + return username != null && !username.isBlank() && + password != null && !password.isBlank(); } @Bean @@ -54,4 +126,4 @@ public HttpSessionEventPublisher httpSessionEventPublisher() { return new HttpSessionEventPublisher(); } -} \ No newline at end of file +} diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/WorkflowService.java b/src/main/java/com/netgrif/application/engine/workflow/service/WorkflowService.java index 472df01d5a..3d9ac2dd2b 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/WorkflowService.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/WorkflowService.java @@ -98,6 +98,7 @@ public class WorkflowService implements IWorkflowService { @Autowired protected IInitValueExpressionEvaluator initValueExpressionEvaluator; + @Lazy @Autowired protected IElasticCaseMappingService caseMappingService; @@ -110,6 +111,7 @@ public class WorkflowService implements IWorkflowService { protected IElasticCaseService elasticCaseService; + @Lazy @Autowired public void setElasticCaseService(IElasticCaseService elasticCaseService) { this.elasticCaseService = elasticCaseService; diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index cfc1901cc4..5f47f37c6e 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -45,4 +45,8 @@ nae.cache.petriNetCache=petriNetCache nae.storage.minio.enabled=true nae.storage.minio.hosts.host_1.host=http://127.0.0.1:9000 nae.storage.minio.hosts.host_1.user=root -nae.storage.minio.hosts.host_1.password=password \ No newline at end of file +nae.storage.minio.hosts.host_1.password=password + +# Redis sentinel +#spring.redis.sentinel.master=mymaster +#spring.redis.sentinel.nodes=127.0.0.1:26379 From 4f13e26fb4f3ca178c2d6a5b0ef6b73399b37e37 Mon Sep 17 00:00:00 2001 From: Milan Mladoniczky <6153201+tuplle@users.noreply.github.com> Date: Mon, 8 Dec 2025 11:14:23 +0100 Subject: [PATCH 51/92] [NAE-2246] - Enable Redis TLS & Configure Redis Sentinel - Added default values for Redis port, SSL, and sentinel port to improve configuration reliability. - Ensured backward compatibility for Redis setup with sensible defaults. --- .../engine/configuration/SessionConfiguration.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/configuration/SessionConfiguration.java b/src/main/java/com/netgrif/application/engine/configuration/SessionConfiguration.java index b986e087e3..9eefcfae57 100644 --- a/src/main/java/com/netgrif/application/engine/configuration/SessionConfiguration.java +++ b/src/main/java/com/netgrif/application/engine/configuration/SessionConfiguration.java @@ -30,8 +30,8 @@ public class SessionConfiguration { @Value("${spring.session.redis.host}") private String hostName; - @Value("${spring.session.redis.port}") - private Integer port; + @Value("${spring.session.redis.port:6379}") + private Integer port = 6379; @Value("${spring.session.redis.username:#{null}}") private String username; @@ -39,7 +39,7 @@ public class SessionConfiguration { @Value("${spring.session.redis.password:#{null}}") private String password; - @Value("${spring.session.redis.ssl:#{null}}") + @Value("${spring.session.redis.ssl:false}") private Boolean ssl; @Value("${spring.redis.sentinel.master:#{null}}") @@ -48,7 +48,7 @@ public class SessionConfiguration { @Value("${spring.redis.sentinel.nodes:#{null}}") private List sentinelNodes; - @Value("${spring.redis.sentinel.port:#{null}}") + @Value("${spring.redis.sentinel.port:26379}") private Integer sentinelPort = 26379; @Value("${spring.redis.sentinel.username:#{null}}") From 2feda2f373b86c3287ee3f4f97634a17db1aa97f Mon Sep 17 00:00:00 2001 From: chvostek Date: Tue, 16 Dec 2025 16:28:22 +0100 Subject: [PATCH 52/92] [NAE-2303] TaskRef Security Improvements - improve dataSet behavior check --- .../engine/workflow/service/DataService.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java b/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java index bdfbbbf90d..f54dae6876 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java @@ -232,6 +232,9 @@ public SetDataEventOutcome setData(Task task, ObjectNode values, Map> behaviorMap = dataField.getBehavior(); + if (behaviorMap == null) { + return false; + } + Set behaviorSet = behaviorMap.get(transId); + return behaviorSet != null && behaviorSet.contains(FieldBehavior.EDITABLE); + } + @Override public GetDataGroupsEventOutcome getDataGroups(String taskId, Locale locale) { return getDataGroups(taskId, locale, new HashSet<>(), 0, null); From 2d75ead14a83d56e7825a8d7c584ebf658b447cc Mon Sep 17 00:00:00 2001 From: chvostek Date: Wed, 17 Dec 2025 12:03:27 +0100 Subject: [PATCH 53/92] [NAE-2303] TaskRef Security Improvements - forbid setData for taskRef and caseRef fields on endpoint calls --- .../engine/workflow/service/DataService.java | 13 +++++++++++++ .../workflow/service/interfaces/IDataService.java | 2 ++ .../engine/workflow/web/AbstractTaskController.java | 7 ++++++- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java b/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java index f54dae6876..de0c86b343 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java @@ -68,6 +68,8 @@ public class DataService implements IDataService { public static final int MONGO_ID_LENGTH = 24; + private static final Set setDataForbiddenFieldTypes = Set.of(FieldType.TASK_REF, FieldType.CASE_REF); + @Autowired protected ApplicationEventPublisher publisher; @@ -219,6 +221,11 @@ public SetDataEventOutcome setData(Task task, ObjectNode values) { @Override public SetDataEventOutcome setData(Task task, ObjectNode values, Map params) { + return setData(task, values, params, false); + } + + @Override + public SetDataEventOutcome setData(Task task, ObjectNode values, Map params, boolean applyForbiddenTypes) { Case useCase = workflowService.findOne(task.getCaseId()); IUser user = userService.getLoggedOrSystem(); @@ -230,6 +237,12 @@ public SetDataEventOutcome setData(Task task, ObjectNode values, Map { String fieldId = entry.getKey(); + if (applyForbiddenTypes) { + FieldType fieldType = useCase.getField(fieldId).getType(); + if (setDataForbiddenFieldTypes.contains(fieldType)) { + return; + } + } DataField dataField = useCase.getDataSet().get(fieldId); if (dataField != null) { if (!isDataFieldEditable(dataField, task.getTransitionId())) { diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/IDataService.java b/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/IDataService.java index 48dc9244a9..d143fe4b7a 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/IDataService.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/IDataService.java @@ -36,6 +36,8 @@ public interface IDataService { SetDataEventOutcome setData(Task task, ObjectNode values, Map params); + SetDataEventOutcome setData(Task task, ObjectNode values, Map params, boolean applyForbiddenTypes); + FileFieldInputStream getFile(Case useCase, Task task, FileField field, boolean forPreview) throws FileNotFoundException; FileFieldInputStream getFile(Case useCase, Task task, FileField field, boolean forPreview, Map params) throws FileNotFoundException; diff --git a/src/main/java/com/netgrif/application/engine/workflow/web/AbstractTaskController.java b/src/main/java/com/netgrif/application/engine/workflow/web/AbstractTaskController.java index 890e053fcd..1c2b04d50b 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/web/AbstractTaskController.java +++ b/src/main/java/com/netgrif/application/engine/workflow/web/AbstractTaskController.java @@ -211,7 +211,12 @@ public EntityModel getData(String taskId, Locale locale public EntityModel setData(String taskId, ObjectNode dataBody, Locale locale) { try { Map outcomes = new HashMap<>(); - dataBody.fields().forEachRemaining(it -> outcomes.put(it.getKey(), dataService.setData(it.getKey(), it.getValue().deepCopy()))); + dataBody.fields().forEachRemaining(fieldChangesEntry -> { + String taskIdToChangeWith = fieldChangesEntry.getKey(); + Task taskToChangeWith = taskService.findOne(taskIdToChangeWith); + outcomes.put(taskIdToChangeWith, dataService.setData(taskToChangeWith, + fieldChangesEntry.getValue().deepCopy(), new HashMap<>(), true)); + }); SetDataEventOutcome mainOutcome = taskService.getMainOutcome(outcomes, taskId); return EventOutcomeWithMessageResource.successMessage("Data field values have been successfully set", LocalisedEventOutcomeFactory.from(mainOutcome, LocaleContextHolder.getLocale())); From 8e0a6c4e4af2c0c54f22e30865708db688dca830 Mon Sep 17 00:00:00 2001 From: chvostek Date: Wed, 17 Dec 2025 14:48:29 +0100 Subject: [PATCH 54/92] [NAE-2303] TaskRef Security Improvements - improve security --- .../engine/workflow/service/DataService.java | 9 +++-- .../engine/workflow/service/TaskService.java | 3 ++ .../workflow/web/AbstractTaskController.java | 35 ++++++++++++++++--- .../workflow/web/PublicTaskController.java | 6 ++-- .../engine/workflow/web/TaskController.java | 6 ++-- 5 files changed, 47 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java b/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java index de0c86b343..d5d8db4bc3 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java @@ -224,8 +224,11 @@ public SetDataEventOutcome setData(Task task, ObjectNode values, Map params, boolean applyForbiddenTypes) { + public SetDataEventOutcome setData(Task task, ObjectNode values, Map params, boolean runSafe) { Case useCase = workflowService.findOne(task.getCaseId()); IUser user = userService.getLoggedOrSystem(); @@ -237,7 +240,7 @@ public SetDataEventOutcome setData(Task task, ObjectNode values, Map { String fieldId = entry.getKey(); - if (applyForbiddenTypes) { + if (runSafe) { FieldType fieldType = useCase.getField(fieldId).getType(); if (setDataForbiddenFieldTypes.contains(fieldType)) { return; @@ -245,7 +248,7 @@ public SetDataEventOutcome setData(Task task, ObjectNode values, Map outco } } mainOutcome = outcomes.remove(key); + if (mainOutcome == null) { + return null; + } mainOutcome.addOutcomes(new ArrayList<>(outcomes.values())); return mainOutcome; } diff --git a/src/main/java/com/netgrif/application/engine/workflow/web/AbstractTaskController.java b/src/main/java/com/netgrif/application/engine/workflow/web/AbstractTaskController.java index 1c2b04d50b..74481c9f6a 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/web/AbstractTaskController.java +++ b/src/main/java/com/netgrif/application/engine/workflow/web/AbstractTaskController.java @@ -5,6 +5,7 @@ import com.netgrif.application.engine.elastic.service.interfaces.IElasticTaskService; import com.netgrif.application.engine.elastic.web.requestbodies.singleaslist.SingleElasticTaskSearchRequestAsList; import com.netgrif.application.engine.eventoutcomes.LocalisedEventOutcomeFactory; +import com.netgrif.application.engine.petrinet.domain.dataset.FieldType; import com.netgrif.application.engine.petrinet.domain.throwable.TransitionNotExecutableException; import com.netgrif.application.engine.workflow.domain.IllegalArgumentWithChangedFieldsException; import com.netgrif.application.engine.workflow.domain.MergeFilterOperation; @@ -16,6 +17,7 @@ import com.netgrif.application.engine.workflow.service.FileFieldInputStream; import com.netgrif.application.engine.workflow.service.interfaces.IDataService; import com.netgrif.application.engine.workflow.service.interfaces.ITaskService; +import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService; import com.netgrif.application.engine.workflow.web.requestbodies.file.FileFieldRequest; import com.netgrif.application.engine.workflow.web.requestbodies.singleaslist.SingleTaskSearchRequestAsList; import com.netgrif.application.engine.workflow.web.responsebodies.*; @@ -38,10 +40,8 @@ import org.springframework.web.multipart.MultipartFile; import java.io.FileNotFoundException; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; +import java.util.*; +import java.util.stream.Collectors; public abstract class AbstractTaskController { @@ -51,11 +51,15 @@ public abstract class AbstractTaskController { private final IDataService dataService; + private final IWorkflowService workflowService; + private final IElasticTaskService searchService; - public AbstractTaskController(ITaskService taskService, IDataService dataService, IElasticTaskService searchService) { + public AbstractTaskController(ITaskService taskService, IDataService dataService, IWorkflowService workflowService, + IElasticTaskService searchService) { this.taskService = taskService; this.dataService = dataService; + this.workflowService = workflowService; this.searchService = searchService; } @@ -210,14 +214,35 @@ public EntityModel getData(String taskId, Locale locale public EntityModel setData(String taskId, ObjectNode dataBody, Locale locale) { try { + List dataGroups = dataService.getDataGroups(taskId, locale).getData(); + Set referencedTaskIds = new HashSet<>(); + referencedTaskIds.add(taskId); + for (com.netgrif.application.engine.petrinet.domain.DataGroup dataGroup : dataGroups) { + // todo 2303 NPE + Set referencedTaskIdsByDataGroup = dataGroup.getFields().getContent().stream() + .filter(localisedField -> localisedField.getType() == FieldType.TASK_REF + && localisedField.getValue() instanceof List + && !((List) localisedField.getValue()).isEmpty()) + .map(localisedField -> (List) localisedField.getValue()) + .flatMap(List::stream) + .collect(Collectors.toSet()); + referencedTaskIds.addAll(referencedTaskIdsByDataGroup); + } Map outcomes = new HashMap<>(); dataBody.fields().forEachRemaining(fieldChangesEntry -> { String taskIdToChangeWith = fieldChangesEntry.getKey(); + if (!referencedTaskIds.contains(taskIdToChangeWith)) { + return; + } Task taskToChangeWith = taskService.findOne(taskIdToChangeWith); outcomes.put(taskIdToChangeWith, dataService.setData(taskToChangeWith, fieldChangesEntry.getValue().deepCopy(), new HashMap<>(), true)); }); SetDataEventOutcome mainOutcome = taskService.getMainOutcome(outcomes, taskId); + if (mainOutcome == null) { + Task task = taskService.findOne(taskId); + mainOutcome = new SetDataEventOutcome(workflowService.findOne(task.getCaseId()), task); + } return EventOutcomeWithMessageResource.successMessage("Data field values have been successfully set", LocalisedEventOutcomeFactory.from(mainOutcome, LocaleContextHolder.getLocale())); } catch (IllegalArgumentWithChangedFieldsException e) { diff --git a/src/main/java/com/netgrif/application/engine/workflow/web/PublicTaskController.java b/src/main/java/com/netgrif/application/engine/workflow/web/PublicTaskController.java index 30294e760c..0281826219 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/web/PublicTaskController.java +++ b/src/main/java/com/netgrif/application/engine/workflow/web/PublicTaskController.java @@ -7,6 +7,7 @@ import com.netgrif.application.engine.workflow.domain.eventoutcomes.response.EventOutcomeWithMessage; import com.netgrif.application.engine.workflow.service.interfaces.IDataService; import com.netgrif.application.engine.workflow.service.interfaces.ITaskService; +import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService; import com.netgrif.application.engine.workflow.web.requestbodies.file.FileFieldRequest; import com.netgrif.application.engine.workflow.web.requestbodies.singleaslist.SingleTaskSearchRequestAsList; import com.netgrif.application.engine.workflow.web.responsebodies.LocalisedTaskResource; @@ -49,8 +50,9 @@ public class PublicTaskController extends AbstractTaskController { private final ITaskService taskService; private final IDataService dataService; - public PublicTaskController(ITaskService taskService, IDataService dataService, IUserService userService) { - super(taskService, dataService, null); + public PublicTaskController(ITaskService taskService, IDataService dataService, IUserService userService, + IWorkflowService workflowService) { + super(taskService, dataService, workflowService, null); this.taskService = taskService; this.dataService = dataService; this.userService = userService; diff --git a/src/main/java/com/netgrif/application/engine/workflow/web/TaskController.java b/src/main/java/com/netgrif/application/engine/workflow/web/TaskController.java index b9690306c3..593f37ce4e 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/web/TaskController.java +++ b/src/main/java/com/netgrif/application/engine/workflow/web/TaskController.java @@ -9,6 +9,7 @@ import com.netgrif.application.engine.workflow.domain.eventoutcomes.response.EventOutcomeWithMessage; import com.netgrif.application.engine.workflow.service.interfaces.IDataService; import com.netgrif.application.engine.workflow.service.interfaces.ITaskService; +import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService; import com.netgrif.application.engine.workflow.web.requestbodies.file.FileFieldRequest; import com.netgrif.application.engine.workflow.web.requestbodies.singleaslist.SingleTaskSearchRequestAsList; import com.netgrif.application.engine.workflow.web.responsebodies.CountResponse; @@ -51,8 +52,9 @@ public class TaskController extends AbstractTaskController { public static final Logger log = LoggerFactory.getLogger(TaskController.class); - public TaskController(ITaskService taskService, IDataService dataService, IElasticTaskService searchService) { - super(taskService, dataService, searchService); + public TaskController(ITaskService taskService, IDataService dataService, IWorkflowService workflowService, + IElasticTaskService searchService) { + super(taskService, dataService, workflowService, searchService); } @Override From 339b64de38ba3d26207a99214d862a2f9a3523cc Mon Sep 17 00:00:00 2001 From: chvostek Date: Thu, 18 Dec 2025 07:41:10 +0100 Subject: [PATCH 55/92] [NAE-2303] TaskRef Security Improvements - add javadoc --- .../engine/workflow/service/DataService.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java b/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java index d5d8db4bc3..8c66f71110 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java @@ -225,7 +225,15 @@ public SetDataEventOutcome setData(Task task, ObjectNode values, Map params, boolean runSafe) { From 6bb523d8a2990e7ff6cdcd5ca16b206d447084d9 Mon Sep 17 00:00:00 2001 From: chvostek Date: Thu, 18 Dec 2025 07:46:45 +0100 Subject: [PATCH 56/92] [NAE-2303] TaskRef Security Improvements - remove todo --- .../application/engine/workflow/web/AbstractTaskController.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/netgrif/application/engine/workflow/web/AbstractTaskController.java b/src/main/java/com/netgrif/application/engine/workflow/web/AbstractTaskController.java index 74481c9f6a..ede4f088b1 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/web/AbstractTaskController.java +++ b/src/main/java/com/netgrif/application/engine/workflow/web/AbstractTaskController.java @@ -218,7 +218,6 @@ public EntityModel setData(String taskId, ObjectNode da Set referencedTaskIds = new HashSet<>(); referencedTaskIds.add(taskId); for (com.netgrif.application.engine.petrinet.domain.DataGroup dataGroup : dataGroups) { - // todo 2303 NPE Set referencedTaskIdsByDataGroup = dataGroup.getFields().getContent().stream() .filter(localisedField -> localisedField.getType() == FieldType.TASK_REF && localisedField.getValue() instanceof List From 1dc1dc7cf54c81ed921362c949fc9ced63a820a7 Mon Sep 17 00:00:00 2001 From: chvostek Date: Thu, 18 Dec 2025 08:56:03 +0100 Subject: [PATCH 57/92] [NAE-2303] TaskRef Security Improvements - implement task controller tests --- .../engine/workflow/TaskControllerTest.groovy | 110 +++++++++++-- .../petriNets/task_controller_set_data.xml | 154 ++++++++++++++++++ 2 files changed, 252 insertions(+), 12 deletions(-) create mode 100644 src/test/resources/petriNets/task_controller_set_data.xml diff --git a/src/test/groovy/com/netgrif/application/engine/workflow/TaskControllerTest.groovy b/src/test/groovy/com/netgrif/application/engine/workflow/TaskControllerTest.groovy index ce970f467d..867c387bb4 100644 --- a/src/test/groovy/com/netgrif/application/engine/workflow/TaskControllerTest.groovy +++ b/src/test/groovy/com/netgrif/application/engine/workflow/TaskControllerTest.groovy @@ -1,5 +1,7 @@ package com.netgrif.application.engine.workflow +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.node.ObjectNode import com.netgrif.application.engine.TestHelper import com.netgrif.application.engine.auth.domain.Authority import com.netgrif.application.engine.auth.domain.User @@ -9,7 +11,6 @@ import com.netgrif.application.engine.auth.service.interfaces.IUserService import com.netgrif.application.engine.elastic.service.interfaces.IElasticTaskService import com.netgrif.application.engine.petrinet.domain.PetriNet import com.netgrif.application.engine.petrinet.domain.VersionType -import com.netgrif.application.engine.petrinet.domain.dataset.FileFieldValue import com.netgrif.application.engine.petrinet.domain.dataset.FileListFieldValue import com.netgrif.application.engine.petrinet.domain.roles.ProcessRole import com.netgrif.application.engine.petrinet.service.ProcessRoleService @@ -18,12 +19,12 @@ import com.netgrif.application.engine.startup.ImportHelper import com.netgrif.application.engine.startup.SuperCreator import com.netgrif.application.engine.utils.FullPageRequest import com.netgrif.application.engine.workflow.domain.Case +import com.netgrif.application.engine.workflow.domain.DataField import com.netgrif.application.engine.workflow.domain.Task import com.netgrif.application.engine.workflow.service.TaskSearchService import com.netgrif.application.engine.workflow.service.TaskService import com.netgrif.application.engine.workflow.service.interfaces.IDataService import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService -import com.netgrif.application.engine.workflow.web.PublicTaskController import com.netgrif.application.engine.workflow.web.TaskController import com.netgrif.application.engine.workflow.web.WorkflowController import com.netgrif.application.engine.workflow.web.requestbodies.TaskSearchRequest @@ -87,7 +88,9 @@ class TaskControllerTest { @Autowired private TaskController taskController - private PetriNet net + private PetriNet allDataNet + + private PetriNet setDataNet private Case useCase @@ -106,7 +109,7 @@ class TaskControllerTest { state: UserState.ACTIVE, authorities: [authorityService.getOrCreate(Authority.user)] as Set, processRoles: [] as Set)) - importNet() + importNets() } @Test @@ -118,7 +121,7 @@ class TaskControllerTest { @Test void testDeleteFile() { - Case testCase = helper.createCase("My case", net) + Case testCase = helper.createCase("My case", allDataNet) String taskId = testCase.tasks.find {it.transition == "1"}.task dataService.saveFile(taskId, "file", new MockMultipartFile("test", new byte[] {})) @@ -132,7 +135,7 @@ class TaskControllerTest { @Test void testDeleteFileByName() { - Case testCase = helper.createCase("My case", net) + Case testCase = helper.createCase("My case", allDataNet) String taskId = testCase.tasks.find {it.transition == "1"}.task dataService.saveFiles(taskId, "fileList", new MockMultipartFile[] {new MockMultipartFile("test", "test", null, new byte[] {})}) @@ -144,6 +147,79 @@ class TaskControllerTest { assert ((FileListFieldValue) testCase.dataSet["fileList"].value).namesPaths == null || ((FileListFieldValue) testCase.dataSet["fileList"].value).namesPaths.size() == 0 } + @Test + void testSetDataFieldTypeRestriction() { + Case testCase = helper.createCase("My case", setDataNet) + String taskId = testCase.tasks.find { it.transition == "testSetDataFieldTypeRestriction" }.task + + ObjectNode dataSet = populateDataset([(taskId):["taskRef_0": ["type": "taskRef", "value": [taskId]]]]) + def response = taskController.setData(taskId, dataSet, Locale.default) + assert response != null && response.content.outcome != null + assert response.content.outcome.changedFields.changedFields.isEmpty() + assert ((List) workflowService.findOne(testCase.stringId).getDataField("taskRef_0").getValue()).isEmpty() + + dataSet = populateDataset([(taskId):["caseRef_0": ["type": "caseRef", "value": [testCase.stringId]]]]) + response = taskController.setData(taskId, dataSet, Locale.default) + assert response != null && response.content.outcome != null + assert response.content.outcome.changedFields.changedFields.isEmpty() + assert ((List) workflowService.findOne(testCase.stringId).getDataField("caseRef_0").getValue()).isEmpty() + } + + @Test + void testSetDataVisibleField() { + Case testCase = helper.createCase("My case", setDataNet) + String taskId = testCase.tasks.find { it.transition == "data" }.task + + ObjectNode dataSet = populateDataset([(taskId):["text_1": ["type": "text", "value": "awd"]]]) + def response = taskController.setData(taskId, dataSet, Locale.default) + assert response != null && response.content.outcome == null + assert response.content.error != null + + // todo: test visible behavior based on parent taskRef behavior + } + + @Test + void testSetDataNestedTaskRefRestrictions() { + Case testCase1 = helper.createCase("testCase1", setDataNet) + String taskId = testCase1.tasks.find { it.transition == "testSetDataNestedTaskRefRestrictions" }.task + Case testCase2 = helper.createCase("testCase2", setDataNet) + Case testCase3 = helper.createCase("testCase3", setDataNet) + + DataField case1DataField = testCase1.getDataField("taskRef_0") + case1DataField.setValue(List.of(testCase2.tasks.find { it.transition == "testSetDataNestedTaskRefRestrictions" }.task)) + workflowService.save(testCase1) + + DataField case2DataField = testCase2.getDataField("taskRef_0") + case2DataField.setValue(List.of(testCase3.tasks.find { it.transition == "data" }.task)) + workflowService.save(testCase2) + + String nestedOtherTaskId = testCase2.tasks.find { it.transition == "data" }.task + ObjectNode dataSet = populateDataset([(nestedOtherTaskId):["text_0": ["type": "text", "value": "awd"]]]) + def response = taskController.setData(taskId, dataSet, Locale.default) + assert response != null && response.content.outcome != null + assert response.content.outcome.changedFields.changedFields.isEmpty() + assert workflowService.findOne(testCase2.stringId).getDataField("text_0").getValue() == null + + String nestedTaskId = testCase3.tasks.find { it.transition == "data" }.task + dataSet = populateDataset([(nestedTaskId):["text_0": ["type": "text", "value": "awd"]]]) + response = taskController.setData(taskId, dataSet, Locale.default) + assert response != null && response.content.outcome != null + assert !response.content.outcome.changedFields.changedFields.isEmpty() + assert workflowService.findOne(testCase3.stringId).getDataField("text_0").getValue() == "awd" + } + + @Test + void testSetDataNonReferencedField() { + Case testCase = helper.createCase("My case", setDataNet) + String taskId = testCase.tasks.find { it.transition == "testSetDataNonReferencedField" }.task + + ObjectNode dataSet = populateDataset([(taskId):["text_1": ["type": "text", "value": "awd"]]]) + def response = taskController.setData(taskId, dataSet, Locale.default) + + assert response != null && response.content.outcome == null + assert response.content.error != null + } + void testWithRoleAndUserref() { createCase() @@ -167,15 +243,19 @@ class TaskControllerTest { assert !findTasksByMongo().empty } - void importNet() { - PetriNet netOptional = helper.createNet("all_data_refs.xml", VersionType.MAJOR).get() - assert netOptional != null - net = netOptional + void importNets() { + PetriNet allDataNet = helper.createNet("all_data_refs.xml", VersionType.MAJOR).get() + assert allDataNet != null + this.allDataNet = allDataNet + + PetriNet setDataNet = helper.createNet("task_controller_set_data.xml", VersionType.MAJOR).get() + assert setDataNet != null + this.setDataNet = setDataNet } void createCase() { useCase = null - useCase = helper.createCase("My case", net) + useCase = helper.createCase("My case", allDataNet) assert useCase != null } @@ -203,7 +283,7 @@ class TaskControllerTest { } void setUserRole() { - List roles = processRoleService.findAll(net.stringId) + List roles = processRoleService.findAll(allDataNet.stringId) for (ProcessRole role : roles) { if (role.importId == "process_role") { @@ -219,4 +299,10 @@ class TaskControllerTest { Page tasks = taskService.search(taskSearchRequestList, new FullPageRequest(), userService.findByEmail(DUMMY_USER_MAIL, false).transformToLoggedUser(), new Locale("en"), false) return tasks } + + static ObjectNode populateDataset(Map>> data) { + ObjectMapper mapper = new ObjectMapper() + String json = mapper.writeValueAsString(data) + return mapper.readTree(json) as ObjectNode + } } diff --git a/src/test/resources/petriNets/task_controller_set_data.xml b/src/test/resources/petriNets/task_controller_set_data.xml new file mode 100644 index 0000000000..60743a813a --- /dev/null +++ b/src/test/resources/petriNets/task_controller_set_data.xml @@ -0,0 +1,154 @@ + + task_controller_set_data + 1.0.0 + TCS + task_controller_set_data + device_hub + true + true + false + + taskRef_0 + + </data> + <data type="caseRef"> + <id>caseRef_0</id> + <title/> + </data> + <data type="text"> + <id>text_0</id> + <title/> + </data> + <data type="text"> + <id>text_1</id> + <title/> + </data> + <transition> + <id>testSetDataFieldTypeRestriction</id> + <x>720</x> + <y>336</y> + <label>testSetDataFieldTypeRestriction</label> + <dataGroup> + <id>testSetDataFieldTypeRestriction_0</id> + <cols>4</cols> + <layout>grid</layout> + <dataRef> + <id>taskRef_0</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>1</x> + <y>1</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>caseRef_0</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>1</x> + <y>2</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + </dataGroup> + </transition> + <transition> + <id>testSetDataNestedTaskRefRestrictions</id> + <x>720</x> + <y>436</y> + <label>testSetDataNestedTaskRefRestrictions</label> + <dataGroup> + <id>testSetDataNestedTaskRefRestrictions_0</id> + <cols>4</cols> + <layout>grid</layout> + <dataRef> + <id>taskRef_0</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>1</x> + <y>1</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + </dataGroup> + </transition> + <transition> + <id>data</id> + <x>720</x> + <y>564</y> + <label>data</label> + <dataGroup> + <id>data_0</id> + <cols>4</cols> + <layout>grid</layout> + <dataRef> + <id>text_0</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>1</x> + <y>1</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>text_1</id> + <logic> + <behavior>visible</behavior> + </logic> + <layout> + <x>1</x> + <y>2</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + </dataGroup> + </transition> + <transition> + <id>testSetDataNonReferencedField</id> + <x>720</x> + <y>664</y> + <label>testSetDataNonReferencedField</label> + <dataGroup> + <id>data_0</id> + <cols>4</cols> + <layout>grid</layout> + <dataRef> + <id>text_0</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>1</x> + <y>1</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + </dataGroup> + </transition> +</document> \ No newline at end of file From fbfa791c56bd31fcf96ba66dbd9c5eaddaa70bdd Mon Sep 17 00:00:00 2001 From: Milan Mladoniczky <6153201+tuplle@users.noreply.github.com> Date: Thu, 18 Dec 2025 17:05:15 +0100 Subject: [PATCH 58/92] [NAE-2310] Elasticsearch fulltext query input sanitation - Introduced `ElasticsearchQuerySanitizer` for escaping and removing reserved characters during query sanitization. - Updated relevant request parsing classes to apply sanitization on `fullText` fields. - Added unit tests for `ElasticsearchQuerySanitizer` to ensure robust functionality. --- .../service/ElasticsearchQuerySanitizer.java | 66 +++++++++++++++++++ .../web/requestbodies/CaseSearchRequest.java | 6 +- .../ElasticTaskSearchRequest.java | 8 ++- .../SingleCaseSearchRequestAsList.java | 6 +- ...chRequestSingleItemAsListDeserializer.java | 47 +++++++++++++ ...chRequestSingleItemAsListDeserializer.java | 47 +++++++++++++ .../SingleTaskSearchRequestAsList.java | 6 +- .../ElasticsearchQuerySanitizerTest.java | 40 +++++++++++ 8 files changed, 215 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/netgrif/application/engine/elastic/service/ElasticsearchQuerySanitizer.java create mode 100644 src/main/java/com/netgrif/application/engine/workflow/utils/CaseSearchRequestSingleItemAsListDeserializer.java create mode 100644 src/main/java/com/netgrif/application/engine/workflow/utils/TaskSearchRequestSingleItemAsListDeserializer.java create mode 100644 src/test/java/com/netgrif/application/engine/elastic/ElasticsearchQuerySanitizerTest.java diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticsearchQuerySanitizer.java b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticsearchQuerySanitizer.java new file mode 100644 index 0000000000..51ffbaf744 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticsearchQuerySanitizer.java @@ -0,0 +1,66 @@ +package com.netgrif.application.engine.elastic.service; + + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + + +@Slf4j +public class ElasticsearchQuerySanitizer { + + public static final String[] RESERVED_CHARACTERS_TO_ESCAPE = {"\\", "+", "-", "=", "&&", "||", "!", "(", ")", "{", "}", "[", "]", "^", "\"", "~", "*", "?", ":", "/", "AND", "OR", "NOT", " "}; + public static final String[] RESERVED_CHARACTERS_TO_REMOVE = {">", "<"}; + public static final Map<String, String> RESERVED_KEYWORDS = prepareReservedKeywords(); + + public static String sanitize(String query) { + return sanitize(query, null); + } + + public static String sanitize(String query, String[] exclude) { + Map<String, String> keywordsToEscape = excludeKeywords(exclude); + String sanitized = keywordsToEscape.entrySet().stream() + .reduce(query, (q, entry) -> StringUtils.replace(q, entry.getKey(), entry.getValue()), (q1, q2) -> q2); + log.trace("Sanitized query: {}", sanitized); + return sanitized; + } + + protected static Map<String, String> prepareReservedKeywords() { + if (RESERVED_CHARACTERS_TO_ESCAPE == null || RESERVED_CHARACTERS_TO_REMOVE == null) { + log.error("Set of reserved characters to escape or remove are null"); + return new HashMap<>(); + } + Map<String, String> result = new HashMap<>(); + for (String reservedString : RESERVED_CHARACTERS_TO_ESCAPE) { + String escaped = Arrays.stream(reservedString.split("")) + .map(c -> "\\" + c) + .collect(Collectors.joining("")); + result.put(reservedString, escaped); + } + for (String reservedString : RESERVED_CHARACTERS_TO_REMOVE) { + result.put(reservedString, "\\ "); + } + + return Collections.unmodifiableMap(result); + } + + protected static Map<String, String> excludeKeywords(String[] exclude) { + if (exclude == null || exclude.length == 0) { + return RESERVED_KEYWORDS; + } + Map<String, String> keywordsToEscape = new HashMap<>(RESERVED_KEYWORDS); + for (String toExclude : exclude) { + if (RESERVED_KEYWORDS.containsKey(toExclude)) { + keywordsToEscape.remove(toExclude); + } + } + return Collections.unmodifiableMap(keywordsToEscape); + } + + +} diff --git a/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/CaseSearchRequest.java b/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/CaseSearchRequest.java index 61397d6164..735604811b 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/CaseSearchRequest.java +++ b/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/CaseSearchRequest.java @@ -1,6 +1,7 @@ package com.netgrif.application.engine.elastic.web.requestbodies; import com.fasterxml.jackson.annotation.JsonFormat; +import com.netgrif.application.engine.elastic.service.ElasticsearchQuerySanitizer; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -60,7 +61,7 @@ public CaseSearchRequest(Map<String, Object> request) { } if (request.containsKey("author") && request.get("author") instanceof List) { List<Map<String, String>> authors = (List<Map<String, String>>) request.get("author"); - this.author = authors.stream().map(map -> { + this.author = authors.stream().map(map -> { Author authorRequest = new Author(); if (map.containsKey("id")) authorRequest.id = map.get("id"); @@ -75,7 +76,8 @@ public CaseSearchRequest(Map<String, Object> request) { this.data = (Map<String, String>) request.get("data"); } if (request.containsKey("fullText") && request.get("fullText") instanceof String) { - this.fullText = (String) request.get("fullText"); + String originalFullText = (String) request.get("fullText"); + this.fullText = ElasticsearchQuerySanitizer.sanitize(originalFullText); } if (request.containsKey("transition") && request.get("transition") instanceof List) { this.transition = (List<String>) request.get("transition"); diff --git a/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/ElasticTaskSearchRequest.java b/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/ElasticTaskSearchRequest.java index a01e639ae7..39fc80d150 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/ElasticTaskSearchRequest.java +++ b/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/ElasticTaskSearchRequest.java @@ -1,5 +1,6 @@ package com.netgrif.application.engine.elastic.web.requestbodies; +import com.netgrif.application.engine.elastic.service.ElasticsearchQuerySanitizer; import com.netgrif.application.engine.workflow.web.requestbodies.TaskSearchRequest; import com.netgrif.application.engine.workflow.web.requestbodies.taskSearch.PetriNet; import com.netgrif.application.engine.workflow.web.requestbodies.taskSearch.TaskSearchCaseRequest; @@ -14,14 +15,14 @@ @AllArgsConstructor public class ElasticTaskSearchRequest extends TaskSearchRequest { public String query; - + public ElasticTaskSearchRequest(Map<String, Object> request) { if (request.containsKey("role") && request.get("role") instanceof List) { this.role = (List<String>) request.get("role"); } if (request.containsKey("useCase") && request.get("useCase") instanceof List) { List<Map<String, String>> useCases = (List<Map<String, String>>) request.get("useCase"); - this.useCase = useCases.stream().map(map -> { + this.useCase = useCases.stream().map(map -> { TaskSearchCaseRequest useCase = new TaskSearchCaseRequest(); if (map.containsKey("id")) useCase.id = map.get("id"); @@ -44,7 +45,8 @@ public ElasticTaskSearchRequest(Map<String, Object> request) { this.transitionId = (List<String>) request.get("transitionId"); } if (request.containsKey("fullText") && request.get("fullText") instanceof String) { - this.fullText = (String) request.get("fullText"); + String originalFullText = (String) request.get("fullText"); + this.fullText = ElasticsearchQuerySanitizer.sanitize(originalFullText); } if (request.containsKey("group") && request.get("group") instanceof List) { this.group = (List<String>) request.get("group"); diff --git a/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/singleaslist/SingleCaseSearchRequestAsList.java b/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/singleaslist/SingleCaseSearchRequestAsList.java index 3d6a988f67..866aef4904 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/singleaslist/SingleCaseSearchRequestAsList.java +++ b/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/singleaslist/SingleCaseSearchRequestAsList.java @@ -3,8 +3,8 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.netgrif.application.engine.elastic.web.requestbodies.CaseSearchRequest; import com.netgrif.application.engine.utils.SingleItemAsList; -import com.netgrif.application.engine.utils.SingleItemAsListDeserializer; +import com.netgrif.application.engine.workflow.utils.CaseSearchRequestSingleItemAsListDeserializer; -@JsonDeserialize(using = SingleItemAsListDeserializer.class, contentAs = CaseSearchRequest.class) +@JsonDeserialize(using = CaseSearchRequestSingleItemAsListDeserializer.class, contentAs = CaseSearchRequest.class) public class SingleCaseSearchRequestAsList extends SingleItemAsList<CaseSearchRequest> { -} \ No newline at end of file +} diff --git a/src/main/java/com/netgrif/application/engine/workflow/utils/CaseSearchRequestSingleItemAsListDeserializer.java b/src/main/java/com/netgrif/application/engine/workflow/utils/CaseSearchRequestSingleItemAsListDeserializer.java new file mode 100644 index 0000000000..621433f021 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/workflow/utils/CaseSearchRequestSingleItemAsListDeserializer.java @@ -0,0 +1,47 @@ +package com.netgrif.application.engine.workflow.utils; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.netgrif.application.engine.elastic.service.ElasticsearchQuerySanitizer; +import com.netgrif.application.engine.elastic.web.requestbodies.CaseSearchRequest; +import com.netgrif.application.engine.elastic.web.requestbodies.singleaslist.SingleCaseSearchRequestAsList; +import com.netgrif.application.engine.utils.SingleItemAsList; +import com.netgrif.application.engine.utils.SingleItemAsListDeserializer; + +import java.io.IOException; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.List; + +public class CaseSearchRequestSingleItemAsListDeserializer extends SingleItemAsListDeserializer { + + protected CaseSearchRequestSingleItemAsListDeserializer() { + super(); + } + + protected CaseSearchRequestSingleItemAsListDeserializer(Class<? extends SingleItemAsList> vc) { + super(vc); + } + + @Override + public Object deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, IllegalArgumentException { + Object result = super.deserialize(jsonParser, deserializationContext); + if (isCaseSearchRequestWrapper(result)) { + List<CaseSearchRequest> list = ((SingleCaseSearchRequestAsList) result).getList(); + list.forEach(request -> + request.fullText = ElasticsearchQuerySanitizer.sanitize(request.fullText)); + } + return result; + } + + protected boolean isCaseSearchRequestWrapper(Object object) { + try { + Type superClass = object.getClass().getGenericSuperclass(); + return object instanceof SingleCaseSearchRequestAsList || + (superClass != null && + ((ParameterizedType) superClass).getActualTypeArguments()[0] == CaseSearchRequest.class); + } catch (Exception e) { + return false; + } + } +} diff --git a/src/main/java/com/netgrif/application/engine/workflow/utils/TaskSearchRequestSingleItemAsListDeserializer.java b/src/main/java/com/netgrif/application/engine/workflow/utils/TaskSearchRequestSingleItemAsListDeserializer.java new file mode 100644 index 0000000000..dec3a24744 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/workflow/utils/TaskSearchRequestSingleItemAsListDeserializer.java @@ -0,0 +1,47 @@ +package com.netgrif.application.engine.workflow.utils; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.netgrif.application.engine.elastic.service.ElasticsearchQuerySanitizer; +import com.netgrif.application.engine.utils.SingleItemAsList; +import com.netgrif.application.engine.utils.SingleItemAsListDeserializer; +import com.netgrif.application.engine.workflow.web.requestbodies.TaskSearchRequest; +import com.netgrif.application.engine.workflow.web.requestbodies.singleaslist.SingleTaskSearchRequestAsList; + +import java.io.IOException; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.List; + +public class TaskSearchRequestSingleItemAsListDeserializer extends SingleItemAsListDeserializer { + + protected TaskSearchRequestSingleItemAsListDeserializer() { + super(); + } + + protected TaskSearchRequestSingleItemAsListDeserializer(Class<? extends SingleItemAsList> vc) { + super(vc); + } + + @Override + public Object deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, IllegalArgumentException { + Object result = super.deserialize(jsonParser, deserializationContext); + if (isTaskSearchRequestWrapper(result)) { + List<TaskSearchRequest> list = ((SingleTaskSearchRequestAsList) result).getList(); + list.forEach(request -> + request.fullText = ElasticsearchQuerySanitizer.sanitize(request.fullText)); + } + return result; + } + + protected boolean isTaskSearchRequestWrapper(Object object) { + try { + Type superClass = object.getClass().getGenericSuperclass(); + return object instanceof SingleTaskSearchRequestAsList || + (superClass != null && + ((ParameterizedType) superClass).getActualTypeArguments()[0] == TaskSearchRequest.class); + } catch (Exception e) { + return false; + } + } +} diff --git a/src/main/java/com/netgrif/application/engine/workflow/web/requestbodies/singleaslist/SingleTaskSearchRequestAsList.java b/src/main/java/com/netgrif/application/engine/workflow/web/requestbodies/singleaslist/SingleTaskSearchRequestAsList.java index c8cd7b9343..954aff6f2e 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/web/requestbodies/singleaslist/SingleTaskSearchRequestAsList.java +++ b/src/main/java/com/netgrif/application/engine/workflow/web/requestbodies/singleaslist/SingleTaskSearchRequestAsList.java @@ -2,9 +2,9 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.netgrif.application.engine.utils.SingleItemAsList; -import com.netgrif.application.engine.utils.SingleItemAsListDeserializer; +import com.netgrif.application.engine.workflow.utils.TaskSearchRequestSingleItemAsListDeserializer; import com.netgrif.application.engine.workflow.web.requestbodies.TaskSearchRequest; -@JsonDeserialize(using = SingleItemAsListDeserializer.class, contentAs = TaskSearchRequest.class) +@JsonDeserialize(using = TaskSearchRequestSingleItemAsListDeserializer.class, contentAs = TaskSearchRequest.class) public class SingleTaskSearchRequestAsList extends SingleItemAsList<TaskSearchRequest> { -} \ No newline at end of file +} diff --git a/src/test/java/com/netgrif/application/engine/elastic/ElasticsearchQuerySanitizerTest.java b/src/test/java/com/netgrif/application/engine/elastic/ElasticsearchQuerySanitizerTest.java new file mode 100644 index 0000000000..3fe37e1457 --- /dev/null +++ b/src/test/java/com/netgrif/application/engine/elastic/ElasticsearchQuerySanitizerTest.java @@ -0,0 +1,40 @@ +package com.netgrif.application.engine.elastic; + +import com.netgrif.application.engine.ApplicationEngine; +import com.netgrif.application.engine.elastic.service.ElasticsearchQuerySanitizer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@ExtendWith(SpringExtension.class) +@ActiveProfiles({"test"}) +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = ApplicationEngine.class +) +@TestPropertySource( + locations = "classpath:application-test.properties" +) +class ElasticsearchQuerySanitizerTest { + + private static final Logger log = LoggerFactory.getLogger(ElasticsearchQuerySanitizerTest.class); + + @Test + void shouldSanitizeQuery() { + String query = "identifier: some_value AND field.keyword.value <> other_value"; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query); + log.info("Sanitized query: {}", sanitized); + assertNotNull(sanitized); + assertEquals("identifier\\:\\ some_value\\ \\A\\N\\D\\ field.keyword.value\\ \\ \\ " + + "\\ other_value", sanitized); + } + +} From ed2a1b63eb1ef56c3e97ae2ace5fc9cd87c36c35 Mon Sep 17 00:00:00 2001 From: Milan Mladoniczky <6153201+tuplle@users.noreply.github.com> Date: Thu, 18 Dec 2025 17:43:35 +0100 Subject: [PATCH 59/92] [NAE-2310] Elasticsearch fulltext query input sanitation - Ensures proper contextual deserialization based on the type of the target property. --- ...rchRequestSingleItemAsListDeserializer.java | 18 +++++++++++++++++- ...rchRequestSingleItemAsListDeserializer.java | 16 +++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/workflow/utils/CaseSearchRequestSingleItemAsListDeserializer.java b/src/main/java/com/netgrif/application/engine/workflow/utils/CaseSearchRequestSingleItemAsListDeserializer.java index 621433f021..686ce93a23 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/utils/CaseSearchRequestSingleItemAsListDeserializer.java +++ b/src/main/java/com/netgrif/application/engine/workflow/utils/CaseSearchRequestSingleItemAsListDeserializer.java @@ -1,28 +1,44 @@ package com.netgrif.application.engine.workflow.utils; import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.BeanProperty; import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonDeserializer; import com.netgrif.application.engine.elastic.service.ElasticsearchQuerySanitizer; import com.netgrif.application.engine.elastic.web.requestbodies.CaseSearchRequest; import com.netgrif.application.engine.elastic.web.requestbodies.singleaslist.SingleCaseSearchRequestAsList; import com.netgrif.application.engine.utils.SingleItemAsList; import com.netgrif.application.engine.utils.SingleItemAsListDeserializer; +import lombok.extern.slf4j.Slf4j; import java.io.IOException; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.List; +@Slf4j public class CaseSearchRequestSingleItemAsListDeserializer extends SingleItemAsListDeserializer { protected CaseSearchRequestSingleItemAsListDeserializer() { - super(); + this(null); } protected CaseSearchRequestSingleItemAsListDeserializer(Class<? extends SingleItemAsList> vc) { super(vc); } + @Override + public JsonDeserializer<?> createContextual(DeserializationContext deserializationContext, BeanProperty beanProperty) { + final JavaType type; + if (beanProperty != null) + type = beanProperty.getType(); + else + type = deserializationContext.getContextualType(); + + return new CaseSearchRequestSingleItemAsListDeserializer((Class<? extends SingleItemAsList>) type.getRawClass()); + } + @Override public Object deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, IllegalArgumentException { Object result = super.deserialize(jsonParser, deserializationContext); diff --git a/src/main/java/com/netgrif/application/engine/workflow/utils/TaskSearchRequestSingleItemAsListDeserializer.java b/src/main/java/com/netgrif/application/engine/workflow/utils/TaskSearchRequestSingleItemAsListDeserializer.java index dec3a24744..324954048b 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/utils/TaskSearchRequestSingleItemAsListDeserializer.java +++ b/src/main/java/com/netgrif/application/engine/workflow/utils/TaskSearchRequestSingleItemAsListDeserializer.java @@ -1,7 +1,10 @@ package com.netgrif.application.engine.workflow.utils; import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.BeanProperty; import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonDeserializer; import com.netgrif.application.engine.elastic.service.ElasticsearchQuerySanitizer; import com.netgrif.application.engine.utils.SingleItemAsList; import com.netgrif.application.engine.utils.SingleItemAsListDeserializer; @@ -16,13 +19,24 @@ public class TaskSearchRequestSingleItemAsListDeserializer extends SingleItemAsListDeserializer { protected TaskSearchRequestSingleItemAsListDeserializer() { - super(); + this(null); } protected TaskSearchRequestSingleItemAsListDeserializer(Class<? extends SingleItemAsList> vc) { super(vc); } + @Override + public JsonDeserializer<?> createContextual(DeserializationContext deserializationContext, BeanProperty beanProperty) { + final JavaType type; + if (beanProperty != null) + type = beanProperty.getType(); + else + type = deserializationContext.getContextualType(); + + return new TaskSearchRequestSingleItemAsListDeserializer((Class<? extends SingleItemAsList>) type.getRawClass()); + } + @Override public Object deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, IllegalArgumentException { Object result = super.deserialize(jsonParser, deserializationContext); From 43d3a787218d019b2a0659fa8083f158601c69e4 Mon Sep 17 00:00:00 2001 From: Milan Mladoniczky <6153201+tuplle@users.noreply.github.com> Date: Thu, 18 Dec 2025 18:12:05 +0100 Subject: [PATCH 60/92] [NAE-2310] Elasticsearch fulltext query input sanitation - Added detailed Javadoc for `ElasticsearchQuerySanitizer` methods. - Updated custom deserializers to sanitize `fullText` fields in `TaskSearchRequest` and `CaseSearchRequest` using `ElasticsearchQuerySanitizer`. --- .../service/ElasticsearchQuerySanitizer.java | 35 +++++++++++++++++++ ...chRequestSingleItemAsListDeserializer.java | 29 +++++++++++++-- ...chRequestSingleItemAsListDeserializer.java | 23 ++++++++++++ 3 files changed, 85 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticsearchQuerySanitizer.java b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticsearchQuerySanitizer.java index 51ffbaf744..61a81753c1 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticsearchQuerySanitizer.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticsearchQuerySanitizer.java @@ -11,6 +11,16 @@ import java.util.stream.Collectors; +/** + * The ElasticsearchQuerySanitizer class is responsible for sanitizing Elasticsearch queries + * by escaping or removing reserved characters and keywords. This is essential to ensure proper + * handling of Elasticsearch queries and to prevent syntax issues caused by special characters or + * reserved words. + * <p> + * This class provides utility methods to sanitize query strings by escaping predefined reserved + * characters, removing certain reserved characters, and excluding specific keywords if provided. + * The reserved characters and keywords are predefined and managed internally. + */ @Slf4j public class ElasticsearchQuerySanitizer { @@ -18,11 +28,36 @@ public class ElasticsearchQuerySanitizer { public static final String[] RESERVED_CHARACTERS_TO_REMOVE = {">", "<"}; public static final Map<String, String> RESERVED_KEYWORDS = prepareReservedKeywords(); + /** + * Sanitizes the provided Elasticsearch query string by escaping or removing certain reserved + * characters and excluding specific keywords if applicable. + * <p> + * This method applies default sanitization rules and does not consider keyword exclusions. + * + * @param query the Elasticsearch query string to sanitize, such as a search query or filter. + * It must not be null to ensure proper sanitization. + * @return the sanitized query string with reserved characters handled appropriately. + * If the input is empty or null, the behavior depends on the implemented sanitization logic. + */ public static String sanitize(String query) { return sanitize(query, null); } + /** + * Sanitizes the given query string by replacing reserved keywords with their sanitized equivalents, + * excluding the specified keywords from sanitization. + * + * @param query the query string to sanitize, which may contain reserved characters and keywords. + * This string must not be null. + * @param exclude an array of keywords to exclude from sanitization. If null or empty, all reserved + * keywords will be considered for sanitization. + * @return the sanitized query string with reserved keywords appropriately replaced, and excluded + * keywords untouched. + */ public static String sanitize(String query, String[] exclude) { + if (query == null || query.isBlank()) { + return query; + } Map<String, String> keywordsToEscape = excludeKeywords(exclude); String sanitized = keywordsToEscape.entrySet().stream() .reduce(query, (q, entry) -> StringUtils.replace(q, entry.getKey(), entry.getValue()), (q1, q2) -> q2); diff --git a/src/main/java/com/netgrif/application/engine/workflow/utils/CaseSearchRequestSingleItemAsListDeserializer.java b/src/main/java/com/netgrif/application/engine/workflow/utils/CaseSearchRequestSingleItemAsListDeserializer.java index 686ce93a23..cb077015a9 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/utils/CaseSearchRequestSingleItemAsListDeserializer.java +++ b/src/main/java/com/netgrif/application/engine/workflow/utils/CaseSearchRequestSingleItemAsListDeserializer.java @@ -10,14 +10,25 @@ import com.netgrif.application.engine.elastic.web.requestbodies.singleaslist.SingleCaseSearchRequestAsList; import com.netgrif.application.engine.utils.SingleItemAsList; import com.netgrif.application.engine.utils.SingleItemAsListDeserializer; -import lombok.extern.slf4j.Slf4j; import java.io.IOException; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.List; -@Slf4j +/** + * Custom deserializer for handling JSON deserialization of objects that extend + * the {@link SingleItemAsList} class, specifically designed for handling + * {@link CaseSearchRequest} and ensuring its fields are properly sanitized. + * <p> + * This deserializer extends the functionality of {@link SingleItemAsListDeserializer} + * to additionally process the deserialized objects that represent case search requests. + * It ensures that the `fullText` field in each case search request is sanitized + * using {@link ElasticsearchQuerySanitizer}. + * <p> + * It also provides a mechanism to dynamically determine the appropriate type + * using the contextual information during deserialization. + */ public class CaseSearchRequestSingleItemAsListDeserializer extends SingleItemAsListDeserializer { protected CaseSearchRequestSingleItemAsListDeserializer() { @@ -39,6 +50,20 @@ public JsonDeserializer<?> createContextual(DeserializationContext deserializati return new CaseSearchRequestSingleItemAsListDeserializer((Class<? extends SingleItemAsList>) type.getRawClass()); } + /** + * Deserializes a JSON structure into an object, specifically handling instances that + * may extend the {@code SingleCaseSearchRequestAsList}. During deserialization, it + * sanitizes the `fullText` field in each {@code CaseSearchRequest} object for security + * purposes using {@code ElasticsearchQuerySanitizer}. + * + * @param jsonParser the {@code JsonParser} used for reading the JSON input + * @param deserializationContext the {@code DeserializationContext} providing access + * to contextual information during deserialization + * @return the deserialized object, with sanitization applied if it is an instance of + * {@code SingleCaseSearchRequestAsList} + * @throws IOException if any I/O error occurs during deserialization + * @throws IllegalArgumentException if the object could not be properly instantiated or deserialized + */ @Override public Object deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, IllegalArgumentException { Object result = super.deserialize(jsonParser, deserializationContext); diff --git a/src/main/java/com/netgrif/application/engine/workflow/utils/TaskSearchRequestSingleItemAsListDeserializer.java b/src/main/java/com/netgrif/application/engine/workflow/utils/TaskSearchRequestSingleItemAsListDeserializer.java index 324954048b..422d697b8b 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/utils/TaskSearchRequestSingleItemAsListDeserializer.java +++ b/src/main/java/com/netgrif/application/engine/workflow/utils/TaskSearchRequestSingleItemAsListDeserializer.java @@ -16,6 +16,15 @@ import java.lang.reflect.Type; import java.util.List; +/** + * Custom deserializer for handling cases where single `TaskSearchRequest` items + * are sent as lists or standalone entities during JSON deserialization. + * + * This class extends the `SingleItemAsListDeserializer`, enabling support for + * deserialization scenarios where JSON may represent either a single item or a list of items. + * It ensures compatibility with `SingleTaskSearchRequestAsList` by sanitizing the `fullText` field + * in each `TaskSearchRequest` instance using the `ElasticsearchQuerySanitizer`. + */ public class TaskSearchRequestSingleItemAsListDeserializer extends SingleItemAsListDeserializer { protected TaskSearchRequestSingleItemAsListDeserializer() { @@ -37,6 +46,20 @@ public JsonDeserializer<?> createContextual(DeserializationContext deserializati return new TaskSearchRequestSingleItemAsListDeserializer((Class<? extends SingleItemAsList>) type.getRawClass()); } + /** + * Deserializes a JSON input into an object while handling cases where a single + * `TaskSearchRequest` or a list of `TaskSearchRequest` objects is included. If + * the object is a `SingleTaskSearchRequestAsList`, it processes each `TaskSearchRequest` + * in the list by sanitizing the `fullText` field using `ElasticsearchQuerySanitizer`. + * + * @param jsonParser the JSON parser used to parse the incoming JSON content + * @param deserializationContext the context for deserialization, providing shared + * state and configuration + * @return the deserialized object, with sanitization applied to `TaskSearchRequest.fullText` + * if applicable + * @throws IOException if an I/O error occurs during parsing + * @throws IllegalArgumentException if the deserialization process encounters an error + */ @Override public Object deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, IllegalArgumentException { Object result = super.deserialize(jsonParser, deserializationContext); From 17bf9a0d5eb6ad8d76205f7b23150776ad6581c7 Mon Sep 17 00:00:00 2001 From: Milan Mladoniczky <6153201+tuplle@users.noreply.github.com> Date: Thu, 18 Dec 2025 23:52:26 +0100 Subject: [PATCH 61/92] [NAE-2310] Elasticsearch fulltext query input sanitation - Enhanced `ElasticsearchQuerySanitizerTest` with additional unit tests for various edge cases. - Simplified `ElasticsearchQuerySanitizer` implementation for better readability and efficiency. --- .../service/ElasticsearchQuerySanitizer.java | 9 +- .../utils/SingleItemAsListDeserializer.java | 21 +- ...chRequestSingleItemAsListDeserializer.java | 23 +-- ...chRequestSingleItemAsListDeserializer.java | 33 +-- .../ElasticsearchQuerySanitizerTest.java | 191 +++++++++++++++--- 5 files changed, 197 insertions(+), 80 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticsearchQuerySanitizer.java b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticsearchQuerySanitizer.java index 61a81753c1..0a46ed1753 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticsearchQuerySanitizer.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticsearchQuerySanitizer.java @@ -59,17 +59,14 @@ public static String sanitize(String query, String[] exclude) { return query; } Map<String, String> keywordsToEscape = excludeKeywords(exclude); - String sanitized = keywordsToEscape.entrySet().stream() - .reduce(query, (q, entry) -> StringUtils.replace(q, entry.getKey(), entry.getValue()), (q1, q2) -> q2); + String sanitized = StringUtils.replaceEach(query, + keywordsToEscape.keySet().toArray(new String[0]), + keywordsToEscape.values().toArray(new String[0])); log.trace("Sanitized query: {}", sanitized); return sanitized; } protected static Map<String, String> prepareReservedKeywords() { - if (RESERVED_CHARACTERS_TO_ESCAPE == null || RESERVED_CHARACTERS_TO_REMOVE == null) { - log.error("Set of reserved characters to escape or remove are null"); - return new HashMap<>(); - } Map<String, String> result = new HashMap<>(); for (String reservedString : RESERVED_CHARACTERS_TO_ESCAPE) { String escaped = Arrays.stream(reservedString.split("")) diff --git a/src/main/java/com/netgrif/application/engine/utils/SingleItemAsListDeserializer.java b/src/main/java/com/netgrif/application/engine/utils/SingleItemAsListDeserializer.java index 2b19385925..74bc7010ad 100644 --- a/src/main/java/com/netgrif/application/engine/utils/SingleItemAsListDeserializer.java +++ b/src/main/java/com/netgrif/application/engine/utils/SingleItemAsListDeserializer.java @@ -9,7 +9,10 @@ import org.springframework.web.server.ResponseStatusException; import java.io.IOException; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; import java.util.List; +import java.util.Objects; public class SingleItemAsListDeserializer extends StdDeserializer<Object> implements ContextualDeserializer { @@ -27,13 +30,16 @@ protected SingleItemAsListDeserializer(Class<? extends SingleItemAsList> vc) { @Override public JsonDeserializer<?> createContextual(DeserializationContext deserializationContext, BeanProperty beanProperty) { + return new SingleItemAsListDeserializer((Class<? extends SingleItemAsList>) getItemClass(deserializationContext, beanProperty)); + } + + protected Class<?> getItemClass(DeserializationContext deserializationContext, BeanProperty beanProperty) { final JavaType type; if (beanProperty != null) type = beanProperty.getType(); else type = deserializationContext.getContextualType(); - - return new SingleItemAsListDeserializer((Class<? extends SingleItemAsList>) type.getRawClass()); + return type.getRawClass(); } @Override @@ -64,4 +70,15 @@ public Object deserialize(JsonParser jsonParser, DeserializationContext deserial return wrapper; } + + protected boolean isWrapperClass(Object object, Class<?> wrapperClass, Class<?> wrappedClass) { + try { + Type superClass = object.getClass().getGenericSuperclass(); + return Objects.equals(object.getClass(), wrapperClass) || + (superClass != null && + Objects.equals(((ParameterizedType) superClass).getActualTypeArguments()[0], wrappedClass)); + } catch (Exception e) { + return false; + } + } } diff --git a/src/main/java/com/netgrif/application/engine/workflow/utils/CaseSearchRequestSingleItemAsListDeserializer.java b/src/main/java/com/netgrif/application/engine/workflow/utils/CaseSearchRequestSingleItemAsListDeserializer.java index cb077015a9..e66690ac8e 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/utils/CaseSearchRequestSingleItemAsListDeserializer.java +++ b/src/main/java/com/netgrif/application/engine/workflow/utils/CaseSearchRequestSingleItemAsListDeserializer.java @@ -3,7 +3,6 @@ import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.BeanProperty; import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JsonDeserializer; import com.netgrif.application.engine.elastic.service.ElasticsearchQuerySanitizer; import com.netgrif.application.engine.elastic.web.requestbodies.CaseSearchRequest; @@ -12,8 +11,6 @@ import com.netgrif.application.engine.utils.SingleItemAsListDeserializer; import java.io.IOException; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; import java.util.List; /** @@ -41,13 +38,7 @@ protected CaseSearchRequestSingleItemAsListDeserializer(Class<? extends SingleIt @Override public JsonDeserializer<?> createContextual(DeserializationContext deserializationContext, BeanProperty beanProperty) { - final JavaType type; - if (beanProperty != null) - type = beanProperty.getType(); - else - type = deserializationContext.getContextualType(); - - return new CaseSearchRequestSingleItemAsListDeserializer((Class<? extends SingleItemAsList>) type.getRawClass()); + return new CaseSearchRequestSingleItemAsListDeserializer((Class<? extends SingleItemAsList>) getItemClass(deserializationContext, beanProperty)); } /** @@ -67,7 +58,7 @@ public JsonDeserializer<?> createContextual(DeserializationContext deserializati @Override public Object deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, IllegalArgumentException { Object result = super.deserialize(jsonParser, deserializationContext); - if (isCaseSearchRequestWrapper(result)) { + if (isWrapperClass(result, SingleCaseSearchRequestAsList.class, CaseSearchRequest.class)) { List<CaseSearchRequest> list = ((SingleCaseSearchRequestAsList) result).getList(); list.forEach(request -> request.fullText = ElasticsearchQuerySanitizer.sanitize(request.fullText)); @@ -75,14 +66,4 @@ public Object deserialize(JsonParser jsonParser, DeserializationContext deserial return result; } - protected boolean isCaseSearchRequestWrapper(Object object) { - try { - Type superClass = object.getClass().getGenericSuperclass(); - return object instanceof SingleCaseSearchRequestAsList || - (superClass != null && - ((ParameterizedType) superClass).getActualTypeArguments()[0] == CaseSearchRequest.class); - } catch (Exception e) { - return false; - } - } } diff --git a/src/main/java/com/netgrif/application/engine/workflow/utils/TaskSearchRequestSingleItemAsListDeserializer.java b/src/main/java/com/netgrif/application/engine/workflow/utils/TaskSearchRequestSingleItemAsListDeserializer.java index 422d697b8b..3d65056b3f 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/utils/TaskSearchRequestSingleItemAsListDeserializer.java +++ b/src/main/java/com/netgrif/application/engine/workflow/utils/TaskSearchRequestSingleItemAsListDeserializer.java @@ -3,7 +3,6 @@ import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.BeanProperty; import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JsonDeserializer; import com.netgrif.application.engine.elastic.service.ElasticsearchQuerySanitizer; import com.netgrif.application.engine.utils.SingleItemAsList; @@ -12,14 +11,12 @@ import com.netgrif.application.engine.workflow.web.requestbodies.singleaslist.SingleTaskSearchRequestAsList; import java.io.IOException; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; import java.util.List; /** * Custom deserializer for handling cases where single `TaskSearchRequest` items * are sent as lists or standalone entities during JSON deserialization. - * + * <p> * This class extends the `SingleItemAsListDeserializer`, enabling support for * deserialization scenarios where JSON may represent either a single item or a list of items. * It ensures compatibility with `SingleTaskSearchRequestAsList` by sanitizing the `fullText` field @@ -37,13 +34,7 @@ protected TaskSearchRequestSingleItemAsListDeserializer(Class<? extends SingleIt @Override public JsonDeserializer<?> createContextual(DeserializationContext deserializationContext, BeanProperty beanProperty) { - final JavaType type; - if (beanProperty != null) - type = beanProperty.getType(); - else - type = deserializationContext.getContextualType(); - - return new TaskSearchRequestSingleItemAsListDeserializer((Class<? extends SingleItemAsList>) type.getRawClass()); + return new TaskSearchRequestSingleItemAsListDeserializer((Class<? extends SingleItemAsList>) getItemClass(deserializationContext, beanProperty)); } /** @@ -52,18 +43,18 @@ public JsonDeserializer<?> createContextual(DeserializationContext deserializati * the object is a `SingleTaskSearchRequestAsList`, it processes each `TaskSearchRequest` * in the list by sanitizing the `fullText` field using `ElasticsearchQuerySanitizer`. * - * @param jsonParser the JSON parser used to parse the incoming JSON content + * @param jsonParser the JSON parser used to parse the incoming JSON content * @param deserializationContext the context for deserialization, providing shared - * state and configuration + * state and configuration * @return the deserialized object, with sanitization applied to `TaskSearchRequest.fullText` - * if applicable - * @throws IOException if an I/O error occurs during parsing + * if applicable + * @throws IOException if an I/O error occurs during parsing * @throws IllegalArgumentException if the deserialization process encounters an error */ @Override public Object deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, IllegalArgumentException { Object result = super.deserialize(jsonParser, deserializationContext); - if (isTaskSearchRequestWrapper(result)) { + if (isWrapperClass(result, SingleTaskSearchRequestAsList.class, TaskSearchRequest.class)) { List<TaskSearchRequest> list = ((SingleTaskSearchRequestAsList) result).getList(); list.forEach(request -> request.fullText = ElasticsearchQuerySanitizer.sanitize(request.fullText)); @@ -71,14 +62,4 @@ public Object deserialize(JsonParser jsonParser, DeserializationContext deserial return result; } - protected boolean isTaskSearchRequestWrapper(Object object) { - try { - Type superClass = object.getClass().getGenericSuperclass(); - return object instanceof SingleTaskSearchRequestAsList || - (superClass != null && - ((ParameterizedType) superClass).getActualTypeArguments()[0] == TaskSearchRequest.class); - } catch (Exception e) { - return false; - } - } } diff --git a/src/test/java/com/netgrif/application/engine/elastic/ElasticsearchQuerySanitizerTest.java b/src/test/java/com/netgrif/application/engine/elastic/ElasticsearchQuerySanitizerTest.java index 3fe37e1457..f204bdd17e 100644 --- a/src/test/java/com/netgrif/application/engine/elastic/ElasticsearchQuerySanitizerTest.java +++ b/src/test/java/com/netgrif/application/engine/elastic/ElasticsearchQuerySanitizerTest.java @@ -1,40 +1,181 @@ package com.netgrif.application.engine.elastic; -import com.netgrif.application.engine.ApplicationEngine; import com.netgrif.application.engine.elastic.service.ElasticsearchQuerySanitizer; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.TestPropertySource; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -@ExtendWith(SpringExtension.class) -@ActiveProfiles({"test"}) -@SpringBootTest( - webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, - classes = ApplicationEngine.class -) -@TestPropertySource( - locations = "classpath:application-test.properties" -) -class ElasticsearchQuerySanitizerTest { - - private static final Logger log = LoggerFactory.getLogger(ElasticsearchQuerySanitizerTest.class); + +import static org.junit.jupiter.api.Assertions.*; + +public class ElasticsearchQuerySanitizerTest { @Test void shouldSanitizeQuery() { String query = "identifier: some_value AND field.keyword.value <> other_value"; String sanitized = ElasticsearchQuerySanitizer.sanitize(query); - log.info("Sanitized query: {}", sanitized); assertNotNull(sanitized); assertEquals("identifier\\:\\ some_value\\ \\A\\N\\D\\ field.keyword.value\\ \\ \\ " + "\\ other_value", sanitized); } + @Test + void shouldEscapeReservedCharacters() { + String query = "test\\query"; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query); + assertEquals("test\\\\query", sanitized); + } + + @Test + void shouldEscapeSpecialOperators() { + String query = "field: value + other - something = test"; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query); + assertEquals("field\\:\\ value\\ \\+\\ other\\ \\-\\ something\\ \\=\\ test", sanitized); + } + + @Test + void shouldEscapeLogicalOperators() { + String query = "field1 && field2 || field3"; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query); + assertEquals("field1\\ \\&\\&\\ field2\\ \\|\\|\\ field3", sanitized); + } + + @Test + void shouldEscapeBracketsAndParentheses() { + String query = "field: (value) {range} [array]"; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query); + assertEquals("field\\:\\ \\(value\\)\\ \\{range\\}\\ \\[array\\]", sanitized); + } + + @Test + void shouldEscapeWildcardsAndSpecialChars() { + String query = "test*query?with^special\"chars~"; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query); + assertEquals("test\\*query\\?with\\^special\\\"chars\\~", sanitized); + } + + @Test + void shouldRemoveAngleBrackets() { + String query = "value<100 AND value>50"; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query); + assertEquals("value\\ 100\\ \\A\\N\\D\\ value\\ 50", sanitized); + } + + @Test + void shouldEscapeSlashes() { + String query = "path/to/resource"; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query); + assertEquals("path\\/to\\/resource", sanitized); + } + + @Test + void shouldEscapeNegationOperator() { + String query = "!important"; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query); + assertEquals("\\!important", sanitized); + } + + @Test + void shouldEscapeKeywordsAndOrNot() { + String query = "field1 AND field2 OR field3 NOT field4"; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query); + assertEquals("field1\\ \\A\\N\\D\\ field2\\ \\O\\R\\ field3\\ \\N\\O\\T\\ field4", sanitized); + } + + @Test + void shouldHandleNullQuery() { + String sanitized = ElasticsearchQuerySanitizer.sanitize(null); + assertNull(sanitized); + } + + @Test + void shouldHandleEmptyQuery() { + String sanitized = ElasticsearchQuerySanitizer.sanitize(""); + assertEquals("", sanitized); + } + + @Test + void shouldHandleBlankQuery() { + String sanitized = ElasticsearchQuerySanitizer.sanitize(" "); + assertEquals(" ", sanitized); + } + + @Test + void shouldHandleMultipleReservedCharactersInSequence() { + String query = "field:value&&another||test"; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query); + assertEquals("field\\:value\\&\\&another\\|\\|test", sanitized); + } + + @Test + void shouldSanitizeWithExcludedKeywords() { + String query = "field:value AND other:test"; + String[] exclude = {"AND"," "}; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query, exclude); + assertEquals("field\\:value AND other\\:test", sanitized); + } + + @Test + void shouldSanitizeWithMultipleExcludedKeywords() { + String query = "field:value AND other:test OR another:value"; + String[] exclude = {"AND", "OR", " "}; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query, exclude); + assertEquals("field\\:value AND other\\:test OR another\\:value", sanitized); + } + + @Test + void shouldSanitizeWithExcludedSpecialCharacters() { + String query = "field:value + test - other"; + String[] exclude = {"+", "-", " "}; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query, exclude); + assertEquals("field\\:value + test - other", sanitized); + } + + @Test + void shouldHandleNullExcludeArray() { + String query = "field: value AND test"; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query, null); + assertEquals("field\\:\\ value\\ \\A\\N\\D\\ test", sanitized); + } + + @Test + void shouldHandleEmptyExcludeArray() { + String query = "field: value AND test"; + String[] exclude = {}; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query, exclude); + assertEquals("field\\:\\ value\\ \\A\\N\\D\\ test", sanitized); + } + + @Test + void shouldIgnoreNonReservedKeywordsInExclude() { + String query = "field: value AND test"; + String[] exclude = {"NONEXISTENT", "INVALID"}; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query, exclude); + assertEquals("field\\:\\ value\\ \\A\\N\\D\\ test", sanitized); + } + + @Test + void shouldHandleComplexQueryWithMixedCharacters() { + String query = "title:(\"test query\" AND status:active) OR tags:[java, spring] && created_at:[2023 TO 2024]"; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query); + assertNotNull(sanitized); + assertTrue(sanitized.contains("\\:")); + assertTrue(sanitized.contains("\\(")); + assertTrue(sanitized.contains("\\)")); + assertTrue(sanitized.contains("\\[")); + assertTrue(sanitized.contains("\\]")); + } + + @Test + void shouldEscapeSpaces() { + String query = "hello world"; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query); + assertEquals("hello\\ world", sanitized); + } + + @Test + void shouldHandleQueryWithOnlyReservedCharacters() { + String query = "+-=!(){}[]^\"~*?:/"; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query); + assertNotNull(sanitized); + assertNotEquals(query, sanitized); + } + } From df0c11e79427b275b894673e7b06018f21a71d7b Mon Sep 17 00:00:00 2001 From: Milan Mladoniczky <6153201+tuplle@users.noreply.github.com> Date: Fri, 19 Dec 2025 00:18:33 +0100 Subject: [PATCH 62/92] [NAE-2310] Elasticsearch fulltext query input sanitation - Updated `TaskSearchRequestSingleItemAsListDeserializer` to support `SingleElasticTaskSearchRequestAsList`. - Applied `ElasticsearchQuerySanitizer` to sanitize `fullText` in elastic task search requests during deserialization. --- .../SingleElasticTaskSearchRequestAsList.java | 6 +++--- ...askSearchRequestSingleItemAsListDeserializer.java | 12 ++++++++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/singleaslist/SingleElasticTaskSearchRequestAsList.java b/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/singleaslist/SingleElasticTaskSearchRequestAsList.java index 7dd274545b..a2d0e19e12 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/singleaslist/SingleElasticTaskSearchRequestAsList.java +++ b/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/singleaslist/SingleElasticTaskSearchRequestAsList.java @@ -3,8 +3,8 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.netgrif.application.engine.elastic.web.requestbodies.ElasticTaskSearchRequest; import com.netgrif.application.engine.utils.SingleItemAsList; -import com.netgrif.application.engine.utils.SingleItemAsListDeserializer; +import com.netgrif.application.engine.workflow.utils.TaskSearchRequestSingleItemAsListDeserializer; -@JsonDeserialize(using = SingleItemAsListDeserializer.class, contentAs = ElasticTaskSearchRequest.class) +@JsonDeserialize(using = TaskSearchRequestSingleItemAsListDeserializer.class, contentAs = ElasticTaskSearchRequest.class) public class SingleElasticTaskSearchRequestAsList extends SingleItemAsList<ElasticTaskSearchRequest> { -} \ No newline at end of file +} diff --git a/src/main/java/com/netgrif/application/engine/workflow/utils/TaskSearchRequestSingleItemAsListDeserializer.java b/src/main/java/com/netgrif/application/engine/workflow/utils/TaskSearchRequestSingleItemAsListDeserializer.java index 3d65056b3f..690a95fcf0 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/utils/TaskSearchRequestSingleItemAsListDeserializer.java +++ b/src/main/java/com/netgrif/application/engine/workflow/utils/TaskSearchRequestSingleItemAsListDeserializer.java @@ -5,12 +5,15 @@ import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; import com.netgrif.application.engine.elastic.service.ElasticsearchQuerySanitizer; +import com.netgrif.application.engine.elastic.web.requestbodies.ElasticTaskSearchRequest; +import com.netgrif.application.engine.elastic.web.requestbodies.singleaslist.SingleElasticTaskSearchRequestAsList; import com.netgrif.application.engine.utils.SingleItemAsList; import com.netgrif.application.engine.utils.SingleItemAsListDeserializer; import com.netgrif.application.engine.workflow.web.requestbodies.TaskSearchRequest; import com.netgrif.application.engine.workflow.web.requestbodies.singleaslist.SingleTaskSearchRequestAsList; import java.io.IOException; +import java.util.Collections; import java.util.List; /** @@ -54,8 +57,13 @@ public JsonDeserializer<?> createContextual(DeserializationContext deserializati @Override public Object deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, IllegalArgumentException { Object result = super.deserialize(jsonParser, deserializationContext); - if (isWrapperClass(result, SingleTaskSearchRequestAsList.class, TaskSearchRequest.class)) { - List<TaskSearchRequest> list = ((SingleTaskSearchRequestAsList) result).getList(); + if (isWrapperClass(result, SingleTaskSearchRequestAsList.class, TaskSearchRequest.class) || + isWrapperClass(result, SingleElasticTaskSearchRequestAsList.class, ElasticTaskSearchRequest.class)) { + List<? extends TaskSearchRequest> list = result instanceof SingleTaskSearchRequestAsList ? + ((SingleTaskSearchRequestAsList) result).getList() : + (result instanceof SingleElasticTaskSearchRequestAsList ? + ((SingleElasticTaskSearchRequestAsList) result).getList() : + Collections.emptyList()); list.forEach(request -> request.fullText = ElasticsearchQuerySanitizer.sanitize(request.fullText)); } From 5f95ede1eb9f50e3cacc0037f84819edef811910 Mon Sep 17 00:00:00 2001 From: Milan Mladoniczky <6153201+tuplle@users.noreply.github.com> Date: Fri, 19 Dec 2025 00:28:10 +0100 Subject: [PATCH 63/92] [NAE-2310] Elasticsearch fulltext query input sanitation - Added new test cases in `ElasticsearchQuerySanitizerTest` to cover additional scenarios. --- .../ElasticsearchQuerySanitizerTest.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/test/java/com/netgrif/application/engine/elastic/ElasticsearchQuerySanitizerTest.java b/src/test/java/com/netgrif/application/engine/elastic/ElasticsearchQuerySanitizerTest.java index f204bdd17e..c3ef5bf53e 100644 --- a/src/test/java/com/netgrif/application/engine/elastic/ElasticsearchQuerySanitizerTest.java +++ b/src/test/java/com/netgrif/application/engine/elastic/ElasticsearchQuerySanitizerTest.java @@ -178,4 +178,28 @@ void shouldHandleQueryWithOnlyReservedCharacters() { assertNotEquals(query, sanitized); } + @Test + void shouldNotModifyQueryWithoutReservedCharacters() { + String query = "simple search query"; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query); + // Note: space is a reserved character, so this will be escaped + assertEquals("simple\\ search\\ query", sanitized); + } + + @Test + void shouldNotModifyQueryWithoutReservedCharactersExcludeingSpace() { + String query = "simple search query"; + String[] exclude = {" "}; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query, exclude); + // Note: space is a reserved character, so this will be escaped + assertEquals("simple search query", sanitized); + } + + @Test + void shouldNotModifyQueryWithoutAnyReservedCharacters() { + String query = "simplesearchquery"; + String sanitized = ElasticsearchQuerySanitizer.sanitize(query); + assertEquals("simplesearchquery", sanitized); + } + } From 5fa7cef80142421543a389196a63123ca9597945 Mon Sep 17 00:00:00 2001 From: Milan Mladoniczky <6153201+tuplle@users.noreply.github.com> Date: Fri, 19 Dec 2025 00:43:36 +0100 Subject: [PATCH 64/92] [NAE-2310] Elasticsearch fulltext query input sanitation - Corrected method name typo in `ElasticsearchQuerySanitizerTest`. - Refactored `TaskSearchRequestSingleItemAsListDeserializer` for better readability and initialization clarity. --- ...TaskSearchRequestSingleItemAsListDeserializer.java | 11 ++++++----- .../elastic/ElasticsearchQuerySanitizerTest.java | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/workflow/utils/TaskSearchRequestSingleItemAsListDeserializer.java b/src/main/java/com/netgrif/application/engine/workflow/utils/TaskSearchRequestSingleItemAsListDeserializer.java index 690a95fcf0..ddf302b04a 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/utils/TaskSearchRequestSingleItemAsListDeserializer.java +++ b/src/main/java/com/netgrif/application/engine/workflow/utils/TaskSearchRequestSingleItemAsListDeserializer.java @@ -59,11 +59,12 @@ public Object deserialize(JsonParser jsonParser, DeserializationContext deserial Object result = super.deserialize(jsonParser, deserializationContext); if (isWrapperClass(result, SingleTaskSearchRequestAsList.class, TaskSearchRequest.class) || isWrapperClass(result, SingleElasticTaskSearchRequestAsList.class, ElasticTaskSearchRequest.class)) { - List<? extends TaskSearchRequest> list = result instanceof SingleTaskSearchRequestAsList ? - ((SingleTaskSearchRequestAsList) result).getList() : - (result instanceof SingleElasticTaskSearchRequestAsList ? - ((SingleElasticTaskSearchRequestAsList) result).getList() : - Collections.emptyList()); + List<? extends TaskSearchRequest> list = Collections.emptyList(); + if (result instanceof SingleTaskSearchRequestAsList) { + list = ((SingleTaskSearchRequestAsList) result).getList(); + } else if (result instanceof SingleElasticTaskSearchRequestAsList) { + list = ((SingleElasticTaskSearchRequestAsList) result).getList(); + } list.forEach(request -> request.fullText = ElasticsearchQuerySanitizer.sanitize(request.fullText)); } diff --git a/src/test/java/com/netgrif/application/engine/elastic/ElasticsearchQuerySanitizerTest.java b/src/test/java/com/netgrif/application/engine/elastic/ElasticsearchQuerySanitizerTest.java index c3ef5bf53e..884436bebd 100644 --- a/src/test/java/com/netgrif/application/engine/elastic/ElasticsearchQuerySanitizerTest.java +++ b/src/test/java/com/netgrif/application/engine/elastic/ElasticsearchQuerySanitizerTest.java @@ -187,7 +187,7 @@ void shouldNotModifyQueryWithoutReservedCharacters() { } @Test - void shouldNotModifyQueryWithoutReservedCharactersExcludeingSpace() { + void shouldNotModifyQueryWithoutReservedCharactersExcludingSpace() { String query = "simple search query"; String[] exclude = {" "}; String sanitized = ElasticsearchQuerySanitizer.sanitize(query, exclude); From c0cb354c2a2d899dc3b7b2100316743a160ff04b Mon Sep 17 00:00:00 2001 From: chvostek <chvostek@netgrif.com> Date: Fri, 19 Dec 2025 08:12:13 +0100 Subject: [PATCH 65/92] [NAE-2303] TaskRef Security Improvements - handle possible NPE in DataService --- .../application/engine/workflow/service/DataService.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java b/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java index 8c66f71110..2078aef801 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java @@ -249,8 +249,11 @@ public SetDataEventOutcome setData(Task task, ObjectNode values, Map<String, Str values.fields().forEachRemaining(entry -> { String fieldId = entry.getKey(); if (runSafe) { - FieldType fieldType = useCase.getField(fieldId).getType(); - if (setDataForbiddenFieldTypes.contains(fieldType)) { + Field<?> field = useCase.getField(fieldId); + if (field == null) { + throw new IllegalArgumentException("Such field with id [" + fieldId + "] does not exist in petri net [" + useCase.getPetriNetId() + "]"); + } + if (setDataForbiddenFieldTypes.contains(field.getType())) { return; } } From 87916b25b403298a04286b595bd71b75896e6616 Mon Sep 17 00:00:00 2001 From: chvostek <chvostek@netgrif.com> Date: Fri, 19 Dec 2025 08:12:50 +0100 Subject: [PATCH 66/92] [NAE-2303] TaskRef Security Improvements - rename parameter in IDataService --- .../engine/workflow/service/interfaces/IDataService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/IDataService.java b/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/IDataService.java index d143fe4b7a..3f3f11724a 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/IDataService.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/IDataService.java @@ -36,7 +36,7 @@ public interface IDataService { SetDataEventOutcome setData(Task task, ObjectNode values, Map<String, String> params); - SetDataEventOutcome setData(Task task, ObjectNode values, Map<String, String> params, boolean applyForbiddenTypes); + SetDataEventOutcome setData(Task task, ObjectNode values, Map<String, String> params, boolean runSafe); FileFieldInputStream getFile(Case useCase, Task task, FileField field, boolean forPreview) throws FileNotFoundException; From 629ae6f32248e85bfa7c7ddca2cd58f00a6fe05f Mon Sep 17 00:00:00 2001 From: chvostek <chvostek@netgrif.com> Date: Fri, 19 Dec 2025 08:17:44 +0100 Subject: [PATCH 67/92] [NAE-2303] TaskRef Security Improvements - handle possible NPE in AbstractTaskController - fix typo --- .../workflow/web/AbstractTaskController.java | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/workflow/web/AbstractTaskController.java b/src/main/java/com/netgrif/application/engine/workflow/web/AbstractTaskController.java index ede4f088b1..0e29554265 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/web/AbstractTaskController.java +++ b/src/main/java/com/netgrif/application/engine/workflow/web/AbstractTaskController.java @@ -238,10 +238,7 @@ public EntityModel<EventOutcomeWithMessage> setData(String taskId, ObjectNode da fieldChangesEntry.getValue().deepCopy(), new HashMap<>(), true)); }); SetDataEventOutcome mainOutcome = taskService.getMainOutcome(outcomes, taskId); - if (mainOutcome == null) { - Task task = taskService.findOne(taskId); - mainOutcome = new SetDataEventOutcome(workflowService.findOne(task.getCaseId()), task); - } + mainOutcome = handleMainSetDataEventOutcome(mainOutcome, taskId); return EventOutcomeWithMessageResource.successMessage("Data field values have been successfully set", LocalisedEventOutcomeFactory.from(mainOutcome, LocaleContextHolder.getLocale())); } catch (IllegalArgumentWithChangedFieldsException e) { @@ -259,6 +256,7 @@ public EntityModel<EventOutcomeWithMessage> saveFile(String taskId, MultipartFil Map<String, SetDataEventOutcome> outcomes = new HashMap<>(); outcomes.put(dataBody.getParentTaskId(), dataService.saveFile(dataBody.getParentTaskId(), dataBody.getFieldId(), multipartFile)); SetDataEventOutcome mainOutcome = taskService.getMainOutcome(outcomes, taskId); + mainOutcome = handleMainSetDataEventOutcome(mainOutcome, taskId); return EventOutcomeWithMessageResource.successMessage("Data field values have been successfully set", LocalisedEventOutcomeFactory.from(mainOutcome, LocaleContextHolder.getLocale())); } catch (IllegalArgumentWithChangedFieldsException e) { @@ -291,7 +289,8 @@ public EntityModel<EventOutcomeWithMessage> deleteFile(String taskId, String fie Map<String, SetDataEventOutcome> outcomes = new HashMap<>(); outcomes.put(taskId, dataService.deleteFile(taskId, fieldId)); SetDataEventOutcome mainOutcome = taskService.getMainOutcome(outcomes, taskId); - return EventOutcomeWithMessageResource.successMessage("Data field values have been sucessfully set", + mainOutcome = handleMainSetDataEventOutcome(mainOutcome, taskId); + return EventOutcomeWithMessageResource.successMessage("Data field values have been successfully set", LocalisedEventOutcomeFactory.from(mainOutcome, LocaleContextHolder.getLocale())); } @@ -299,7 +298,8 @@ public EntityModel<EventOutcomeWithMessage> saveFiles(String taskId, MultipartFi Map<String, SetDataEventOutcome> outcomes = new HashMap<>(); outcomes.put(requestBody.getParentTaskId(), dataService.saveFiles(requestBody.getParentTaskId(), requestBody.getFieldId(), multipartFiles)); SetDataEventOutcome mainOutcome = taskService.getMainOutcome(outcomes, taskId); - return EventOutcomeWithMessageResource.successMessage("Data field values have been sucessfully set", + mainOutcome = handleMainSetDataEventOutcome(mainOutcome, taskId); + return EventOutcomeWithMessageResource.successMessage("Data field values have been successfully set", LocalisedEventOutcomeFactory.from(mainOutcome, LocaleContextHolder.getLocale())); } @@ -323,7 +323,8 @@ public EntityModel<EventOutcomeWithMessage> deleteNamedFile(String taskId, Strin Map<String, SetDataEventOutcome> outcomes = new HashMap<>(); outcomes.put(taskId, dataService.deleteFileByName(taskId, fieldId, name)); SetDataEventOutcome mainOutcome = taskService.getMainOutcome(outcomes, taskId); - return EventOutcomeWithMessageResource.successMessage("Data field values have been sucessfully set", + mainOutcome = handleMainSetDataEventOutcome(mainOutcome, taskId); + return EventOutcomeWithMessageResource.successMessage("Data field values have been successfully set", LocalisedEventOutcomeFactory.from(mainOutcome, LocaleContextHolder.getLocale())); } @@ -339,4 +340,13 @@ public ResponseEntity<Resource> getFilePreview(String taskId, String fieldId) th .headers(headers) .body(fileFieldInputStream != null ? new InputStreamResource(fileFieldInputStream.getInputStream()) : null); } + + protected SetDataEventOutcome handleMainSetDataEventOutcome(SetDataEventOutcome mainOutcome, String taskId) { + if (mainOutcome == null) { + Task task = taskService.findOne(taskId); + return new SetDataEventOutcome(workflowService.findOne(task.getCaseId()), task); + } else { + return mainOutcome; + } + } } From 383bae4f649374fc55861d5bc457ac9ba0564768 Mon Sep 17 00:00:00 2001 From: chvostek <chvostek@netgrif.com> Date: Fri, 19 Dec 2025 08:19:15 +0100 Subject: [PATCH 68/92] [NAE-2303] TaskRef Security Improvements - rename method in TaskControllerTest --- .../engine/workflow/TaskControllerTest.groovy | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/test/groovy/com/netgrif/application/engine/workflow/TaskControllerTest.groovy b/src/test/groovy/com/netgrif/application/engine/workflow/TaskControllerTest.groovy index 867c387bb4..ebbd492060 100644 --- a/src/test/groovy/com/netgrif/application/engine/workflow/TaskControllerTest.groovy +++ b/src/test/groovy/com/netgrif/application/engine/workflow/TaskControllerTest.groovy @@ -152,13 +152,13 @@ class TaskControllerTest { Case testCase = helper.createCase("My case", setDataNet) String taskId = testCase.tasks.find { it.transition == "testSetDataFieldTypeRestriction" }.task - ObjectNode dataSet = populateDataset([(taskId):["taskRef_0": ["type": "taskRef", "value": [taskId]]]]) + ObjectNode dataSet = populateNestedDataset([(taskId):["taskRef_0": ["type": "taskRef", "value": [taskId]]]]) def response = taskController.setData(taskId, dataSet, Locale.default) assert response != null && response.content.outcome != null assert response.content.outcome.changedFields.changedFields.isEmpty() assert ((List<String>) workflowService.findOne(testCase.stringId).getDataField("taskRef_0").getValue()).isEmpty() - dataSet = populateDataset([(taskId):["caseRef_0": ["type": "caseRef", "value": [testCase.stringId]]]]) + dataSet = populateNestedDataset([(taskId):["caseRef_0": ["type": "caseRef", "value": [testCase.stringId]]]]) response = taskController.setData(taskId, dataSet, Locale.default) assert response != null && response.content.outcome != null assert response.content.outcome.changedFields.changedFields.isEmpty() @@ -170,7 +170,7 @@ class TaskControllerTest { Case testCase = helper.createCase("My case", setDataNet) String taskId = testCase.tasks.find { it.transition == "data" }.task - ObjectNode dataSet = populateDataset([(taskId):["text_1": ["type": "text", "value": "awd"]]]) + ObjectNode dataSet = populateNestedDataset([(taskId):["text_1": ["type": "text", "value": "awd"]]]) def response = taskController.setData(taskId, dataSet, Locale.default) assert response != null && response.content.outcome == null assert response.content.error != null @@ -194,14 +194,14 @@ class TaskControllerTest { workflowService.save(testCase2) String nestedOtherTaskId = testCase2.tasks.find { it.transition == "data" }.task - ObjectNode dataSet = populateDataset([(nestedOtherTaskId):["text_0": ["type": "text", "value": "awd"]]]) + ObjectNode dataSet = populateNestedDataset([(nestedOtherTaskId):["text_0": ["type": "text", "value": "awd"]]]) def response = taskController.setData(taskId, dataSet, Locale.default) assert response != null && response.content.outcome != null assert response.content.outcome.changedFields.changedFields.isEmpty() assert workflowService.findOne(testCase2.stringId).getDataField("text_0").getValue() == null String nestedTaskId = testCase3.tasks.find { it.transition == "data" }.task - dataSet = populateDataset([(nestedTaskId):["text_0": ["type": "text", "value": "awd"]]]) + dataSet = populateNestedDataset([(nestedTaskId):["text_0": ["type": "text", "value": "awd"]]]) response = taskController.setData(taskId, dataSet, Locale.default) assert response != null && response.content.outcome != null assert !response.content.outcome.changedFields.changedFields.isEmpty() @@ -213,7 +213,7 @@ class TaskControllerTest { Case testCase = helper.createCase("My case", setDataNet) String taskId = testCase.tasks.find { it.transition == "testSetDataNonReferencedField" }.task - ObjectNode dataSet = populateDataset([(taskId):["text_1": ["type": "text", "value": "awd"]]]) + ObjectNode dataSet = populateNestedDataset([(taskId):["text_1": ["type": "text", "value": "awd"]]]) def response = taskController.setData(taskId, dataSet, Locale.default) assert response != null && response.content.outcome == null @@ -300,7 +300,7 @@ class TaskControllerTest { return tasks } - static ObjectNode populateDataset(Map<String, Map<String, Map<String, String>>> data) { + static ObjectNode populateNestedDataset(Map<String, Map<String, Map<String, String>>> data) { ObjectMapper mapper = new ObjectMapper() String json = mapper.writeValueAsString(data) return mapper.readTree(json) as ObjectNode From cc304888a77026e1cd787587e9ae0f2544bd66ad Mon Sep 17 00:00:00 2001 From: chvostek <chvostek@netgrif.com> Date: Fri, 19 Dec 2025 08:48:24 +0100 Subject: [PATCH 69/92] [NAE-2303] TaskRef Security Improvements - change the parameter type in TaskControllerTest --- .../application/engine/workflow/TaskControllerTest.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/groovy/com/netgrif/application/engine/workflow/TaskControllerTest.groovy b/src/test/groovy/com/netgrif/application/engine/workflow/TaskControllerTest.groovy index ebbd492060..e30f8af7f8 100644 --- a/src/test/groovy/com/netgrif/application/engine/workflow/TaskControllerTest.groovy +++ b/src/test/groovy/com/netgrif/application/engine/workflow/TaskControllerTest.groovy @@ -300,7 +300,7 @@ class TaskControllerTest { return tasks } - static ObjectNode populateNestedDataset(Map<String, Map<String, Map<String, String>>> data) { + static ObjectNode populateNestedDataset(Map<String, Map<String, Map<String, Object>>> data) { ObjectMapper mapper = new ObjectMapper() String json = mapper.writeValueAsString(data) return mapper.readTree(json) as ObjectNode From 881548b9d8c7385cab1709781d722abc0b572937 Mon Sep 17 00:00:00 2001 From: chvostek <chvostek@netgrif.com> Date: Fri, 19 Dec 2025 09:46:34 +0100 Subject: [PATCH 70/92] [NAE-2303] TaskRef Security Improvements - rename parameter --- .../application/engine/workflow/service/DataService.java | 8 ++++---- .../engine/workflow/service/interfaces/IDataService.java | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java b/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java index 2078aef801..c97e5e0546 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java @@ -230,13 +230,13 @@ public SetDataEventOutcome setData(Task task, ObjectNode values, Map<String, Str * @param task the task object of which the data are updated * @param values information about how to update the data fields * @param params additional information to be injected to the action delegate context - * @param runSafe if set to true, additional validations are going to be applied when updating the data fields. If + * @param runStrict if set to true, additional validations are going to be applied when updating the data fields. If * set to false, minimal restrictions are considered. * * @return outcome containing Case, Task and changes that have been made. * */ @Override - public SetDataEventOutcome setData(Task task, ObjectNode values, Map<String, String> params, boolean runSafe) { + public SetDataEventOutcome setData(Task task, ObjectNode values, Map<String, String> params, boolean runStrict) { Case useCase = workflowService.findOne(task.getCaseId()); IUser user = userService.getLoggedOrSystem(); @@ -248,7 +248,7 @@ public SetDataEventOutcome setData(Task task, ObjectNode values, Map<String, Str SetDataEventOutcome outcome = new SetDataEventOutcome(useCase, task); values.fields().forEachRemaining(entry -> { String fieldId = entry.getKey(); - if (runSafe) { + if (runStrict) { Field<?> field = useCase.getField(fieldId); if (field == null) { throw new IllegalArgumentException("Such field with id [" + fieldId + "] does not exist in petri net [" + useCase.getPetriNetId() + "]"); @@ -259,7 +259,7 @@ public SetDataEventOutcome setData(Task task, ObjectNode values, Map<String, Str } DataField dataField = useCase.getDataSet().get(fieldId); if (dataField != null) { - if (runSafe && !isDataFieldEditable(dataField, task.getTransitionId())) { + if (runStrict && !isDataFieldEditable(dataField, task.getTransitionId())) { throw new IllegalArgumentException("Cannot edit data field [" + fieldId + "], which is not editable on transition [" + task.getTransitionId() + "]."); } Field field = useCase.getPetriNet().getField(fieldId).get(); diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/IDataService.java b/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/IDataService.java index 3f3f11724a..a2cb4211f8 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/IDataService.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/IDataService.java @@ -36,7 +36,7 @@ public interface IDataService { SetDataEventOutcome setData(Task task, ObjectNode values, Map<String, String> params); - SetDataEventOutcome setData(Task task, ObjectNode values, Map<String, String> params, boolean runSafe); + SetDataEventOutcome setData(Task task, ObjectNode values, Map<String, String> params, boolean runStrict); FileFieldInputStream getFile(Case useCase, Task task, FileField field, boolean forPreview) throws FileNotFoundException; From 3a1e4f389bf43385c9c2c8a1dab6df23d1a70c57 Mon Sep 17 00:00:00 2001 From: Milan Mladoniczky <6153201+tuplle@users.noreply.github.com> Date: Mon, 22 Dec 2025 12:58:50 +0100 Subject: [PATCH 71/92] Added Redis connection and read timeouts - Introduced `connection-timeout` and `read-timeout` properties for Redis configuration. - Updated `SessionConfiguration` to utilize the new timeout properties. - Enhanced logging to include Redis configuration details for better traceability. --- .../configuration/SessionConfiguration.java | 21 +++++++++++++++++-- src/main/resources/application.properties | 2 ++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/configuration/SessionConfiguration.java b/src/main/java/com/netgrif/application/engine/configuration/SessionConfiguration.java index 9eefcfae57..5f06dc6329 100644 --- a/src/main/java/com/netgrif/application/engine/configuration/SessionConfiguration.java +++ b/src/main/java/com/netgrif/application/engine/configuration/SessionConfiguration.java @@ -15,6 +15,7 @@ import org.springframework.session.web.http.HeaderHttpSessionIdResolver; import org.springframework.session.web.http.HttpSessionIdResolver; +import java.time.Duration; import java.util.List; import java.util.stream.Collectors; @@ -42,6 +43,12 @@ public class SessionConfiguration { @Value("${spring.session.redis.ssl:false}") private Boolean ssl; + @Value("${spring.session.redis.connection-timeout:2}") + private Long connectionTimeout; // duration in seconds + + @Value("${spring.session.redis.read-timeout:2}") + private Long readTimeout; // duration in seconds + @Value("${spring.redis.sentinel.master:#{null}}") private String sentinelMasterName; @@ -75,6 +82,7 @@ protected JedisConnectionFactory standaloneRedisConfiguration() { redisStandaloneConfiguration.setPassword(password); } JedisClientConfiguration clientConfiguration = jedisClientConfiguration(); + log.debug("Redis standalone configuration: host: {}; port: {}; username: {}", hostName, port, username); return new JedisConnectionFactory(redisStandaloneConfiguration, clientConfiguration); } @@ -101,14 +109,23 @@ protected JedisConnectionFactory redisSentinelConfiguration() { } JedisClientConfiguration clientConfiguration = jedisClientConfiguration(); + log.debug("Redis sentinel configuration: master: {}; nodes: {}; username: {}", sentinelMasterName, sentinelNodes, username); return new JedisConnectionFactory(sentinelConfiguration, clientConfiguration); } protected JedisClientConfiguration jedisClientConfiguration() { + JedisClientConfiguration.JedisClientConfigurationBuilder builder = JedisClientConfiguration.builder(); + if (connectionTimeout != null && connectionTimeout != 0) { + builder = builder.connectTimeout(Duration.ofSeconds(connectionTimeout)); + } + if (readTimeout != null && readTimeout != 0) { + builder = builder.readTimeout(Duration.ofSeconds(readTimeout)); + } if (ssl) { - return JedisClientConfiguration.builder().useSsl().build(); + builder = builder.useSsl().and(); } - return JedisClientConfiguration.defaultConfiguration(); + log.debug("Redis client configuration: connection timeout:{}; read timeout:{}; ssl:{}", connectionTimeout, readTimeout, ssl); + return builder.build(); } private boolean hasCredentials(String username, String password) { diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8d996df2fc..24e03947da 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -71,6 +71,8 @@ spring.session.store-type=redis spring.session.redis.host=${REDIS_HOST:localhost} spring.session.redis.port=${REDIS_PORT:6379} spring.session.redis.namespace=${DATABASE_NAME:nae} +spring.session.redis.connection-timeout=5 +spring.session.redis.read-timeout=5 #Security nae.database.password=${DATABASE_encrypt_password:password} From 9efef6f8d439fd5ee53177c61c84ebadd30ee911 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Voz=C3=A1r?= <dominikvozr@gmail.com> Date: Mon, 12 Jan 2026 12:02:53 +0100 Subject: [PATCH 72/92] [NAE-2342] Added new property nae.quartz.mongoOptionWriteConcernW for quartz mongo write concern policy --- .../engine/configuration/quartz/QuartzConfiguration.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/com/netgrif/application/engine/configuration/quartz/QuartzConfiguration.java b/src/main/java/com/netgrif/application/engine/configuration/quartz/QuartzConfiguration.java index b31b45a181..294c7915cf 100644 --- a/src/main/java/com/netgrif/application/engine/configuration/quartz/QuartzConfiguration.java +++ b/src/main/java/com/netgrif/application/engine/configuration/quartz/QuartzConfiguration.java @@ -38,6 +38,9 @@ public class QuartzConfiguration { @Value("${nae.quartz.dbName:nae}") private String db; + @Value("${nae.quartz.mongoOptionWriteConcernW:majority}") + private String mongoOptionWriteConcernW; + @Bean public Properties quartzProperties() throws IOException { PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean(); @@ -74,6 +77,7 @@ public SchedulerFactoryBean schedulerFactoryBean() throws Exception { properties.setProperty("org.quartz.jobStore.mongoUri", uri); } properties.setProperty("org.quartz.jobStore.dbName", db); + properties.setProperty("org.quartz.jobStore.mongoOptionWriteConcernW", mongoOptionWriteConcernW); properties.setProperty("org.quartz.jobStore.class", "com.netgrif.quartz.mongodb.MongoDBJobStore"); properties.setProperty("spring.quartz.properties.org.quartz.jobStore.isClustered", "false"); properties.setProperty("org.quartz.jobStore.isClustered", "true"); From 0614adf723e8a350f1ab038105516daf56886802 Mon Sep 17 00:00:00 2001 From: Milan Mladoniczky <6153201+tuplle@users.noreply.github.com> Date: Wed, 18 Feb 2026 12:31:10 +0100 Subject: [PATCH 73/92] Enable manual trigger for release builds --- .github/workflows/release-build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 6bb03e0077..d6c91beb18 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -2,6 +2,7 @@ name: Publish package to GitHub Packages on: release: types: [ published ] + workflow_dispatch: jobs: build: From 1febaf3f3e95ee8d2fe2db556bac60d6ca27a805 Mon Sep 17 00:00:00 2001 From: Milan Mladoniczky <6153201+tuplle@users.noreply.github.com> Date: Wed, 18 Feb 2026 12:36:47 +0100 Subject: [PATCH 74/92] Remove unused steps from release-build workflow - Deleted outdated steps related to environment release checks and SonarCloud cache. - Cleaned up commented-out code to improve maintainability and readability of the workflow. - Retained essential steps for Maven package caching and building processes. --- .github/workflows/release-build.yml | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index d6c91beb18..f3cf250066 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -18,19 +18,11 @@ jobs: with: java-version: 11 distribution: 'adopt' - - uses: cardinalby/git-get-release-action@v1 - id: getEnvRelease - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Get Project Version from pom.xml uses: entimaniac/read-pom-version-action@1.0.0 id: getVersion - # - name: Check Enviroment release - # if: ${{ !((steps.getEnvRelease.outputs.prerelease && contains(steps.getVersion.outputs.version, '-SNAPSHOT')) || (!steps.getEnvRelease.outputs.prerelease && !contains(steps.getVersion.outputs.version, '-SNAPSHOT'))) }} - # run: exit 1 - - name: Cache Maven packages uses: actions/cache@v3 with: @@ -89,13 +81,6 @@ jobs: java-version: 11 distribution: 'adopt' -# - name: Cache SonarCloud packages -# uses: actions/cache@v3 -# with: -# path: ~/.sonar/cache -# key: ${{ runner.os }}-sonar -# restore-keys: ${{ runner.os }}-sonar - - name: Cache Maven packages uses: actions/cache@v3 with: @@ -109,13 +94,6 @@ jobs: - name: Build run: mvn clean package install -DskipTests=true -# Upgrade Java -# - name: Build, test, and analyze -# env: -# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -# SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} -# run: mvn -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=netgrif_application-engine - - name: Build, test env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 345ab2bde7279f161eb3e073a4111d4a07ea339a Mon Sep 17 00:00:00 2001 From: Milan Mladoniczky <6153201+tuplle@users.noreply.github.com> Date: Wed, 18 Feb 2026 17:48:32 +0100 Subject: [PATCH 75/92] Update service images in release-build workflow - Upgraded MongoDB service image from `mongo:4.4` to `mongo:6`. - Bumped Elasticsearch service image from `elasticsearch:7.17.3` to `elasticsearch:7.17.28`. - Ensures compatibility with newer versions for improved performance and security. --- .github/workflows/release-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index f3cf250066..b5fdc3067e 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -40,7 +40,7 @@ jobs: needs: build services: mongo: - image: mongo:4.4 + image: mongo:6 ports: - 27017:27017 @@ -50,7 +50,7 @@ jobs: - 6379:6379 elasticsearch: - image: elasticsearch:7.17.3 + image: elasticsearch:7.17.28 ports: - 9200:9200 - 9300:9300 From a5bb2150f55b45b01499f3c7cd4945e6b9c12b90 Mon Sep 17 00:00:00 2001 From: Milan Mladoniczky <6153201+tuplle@users.noreply.github.com> Date: Mon, 9 Mar 2026 14:34:48 +0100 Subject: [PATCH 76/92] Added `ApplicationShutdownProvider` to enable graceful application shutdown. - Add `existsById` method and improve task resolution - Introduced a new `existsById` method in `IUserService` to check user existence. - Modified `resolveUserRef` in `TaskService` to return a list of resolved tasks instead of void. - Ensured consistency in validations by replacing `size()` checks with `isEmpty()`. --- .../MigrationOrderedCommandLineRunner.groovy | 20 ++++++++++-- .../engine/startup/RunnerController.groovy | 2 +- .../auth/service/AbstractUserService.java | 5 +++ .../auth/service/interfaces/IUserService.java | 2 ++ .../ApplicationShutdownProvider.java | 31 +++++++++++++++++++ .../engine/workflow/service/TaskService.java | 13 +++----- .../workflow/service/WorkflowService.java | 4 +-- .../service/interfaces/ITaskService.java | 5 ++- 8 files changed, 66 insertions(+), 16 deletions(-) create mode 100644 src/main/java/com/netgrif/application/engine/configuration/ApplicationShutdownProvider.java diff --git a/src/main/groovy/com/netgrif/application/engine/migration/MigrationOrderedCommandLineRunner.groovy b/src/main/groovy/com/netgrif/application/engine/migration/MigrationOrderedCommandLineRunner.groovy index 8fc940ac52..ff4044873d 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/MigrationOrderedCommandLineRunner.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/MigrationOrderedCommandLineRunner.groovy @@ -1,6 +1,6 @@ package com.netgrif.application.engine.migration - +import com.netgrif.application.engine.configuration.ApplicationShutdownProvider import com.netgrif.application.engine.petrinet.service.interfaces.IPetriNetService import com.netgrif.application.engine.startup.AbstractOrderedCommandLineRunner import org.slf4j.Logger @@ -14,6 +14,7 @@ abstract class MigrationOrderedCommandLineRunner extends AbstractOrderedCommandL private static final Logger log = LoggerFactory.getLogger(MigrationOrderedCommandLineRunner) private String title = this.class.simpleName + private boolean shutdownAfterFinish = false @Autowired private MigrationRepository repository @@ -21,6 +22,9 @@ abstract class MigrationOrderedCommandLineRunner extends AbstractOrderedCommandL @Autowired private IPetriNetService service + @Autowired + private ApplicationShutdownProvider shutdownProvider + @Override void run(String... strings) throws Exception { if (repository.existsByTitle(title)) { @@ -33,7 +37,19 @@ abstract class MigrationOrderedCommandLineRunner extends AbstractOrderedCommandL repository.save(new Migration(title)) service.evictAllCaches() log.info("Migration ${title} applied") + if (shutdownAfterFinish) { + sleep(100) + shutdownProvider.shutdown(this.class) + } + } + + protected enableShutdownAfterFinish() { + this.shutdownAfterFinish = true; + } + + protected disableShutdownAfterFinish() { + this.shutdownAfterFinish = false; } abstract void migrate() -} \ No newline at end of file +} diff --git a/src/main/groovy/com/netgrif/application/engine/startup/RunnerController.groovy b/src/main/groovy/com/netgrif/application/engine/startup/RunnerController.groovy index f02d30e1c8..d653c4c4d3 100644 --- a/src/main/groovy/com/netgrif/application/engine/startup/RunnerController.groovy +++ b/src/main/groovy/com/netgrif/application/engine/startup/RunnerController.groovy @@ -43,4 +43,4 @@ class RunnerController { } return runnerOrder } -} \ No newline at end of file +} diff --git a/src/main/java/com/netgrif/application/engine/auth/service/AbstractUserService.java b/src/main/java/com/netgrif/application/engine/auth/service/AbstractUserService.java index 2a3be9d5ed..d9e68491cf 100644 --- a/src/main/java/com/netgrif/application/engine/auth/service/AbstractUserService.java +++ b/src/main/java/com/netgrif/application/engine/auth/service/AbstractUserService.java @@ -53,6 +53,11 @@ public void addDefaultAuthorities(IUser user) { } } + @Override + public boolean existsById(String id) { + return repository.existsById(id); + } + @Override public IUser assignAuthority(String userId, String authorityId) { IUser user = resolveById(userId, true); diff --git a/src/main/java/com/netgrif/application/engine/auth/service/interfaces/IUserService.java b/src/main/java/com/netgrif/application/engine/auth/service/interfaces/IUserService.java index 01956bc2b8..989062f758 100644 --- a/src/main/java/com/netgrif/application/engine/auth/service/interfaces/IUserService.java +++ b/src/main/java/com/netgrif/application/engine/auth/service/interfaces/IUserService.java @@ -49,6 +49,8 @@ public interface IUserService { void addDefaultAuthorities(IUser user); + boolean existsById(String id); + IUser assignAuthority(String userId, String authorityId); IUser getLoggedOrSystem(); diff --git a/src/main/java/com/netgrif/application/engine/configuration/ApplicationShutdownProvider.java b/src/main/java/com/netgrif/application/engine/configuration/ApplicationShutdownProvider.java new file mode 100644 index 0000000000..57c0c18a73 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/configuration/ApplicationShutdownProvider.java @@ -0,0 +1,31 @@ +package com.netgrif.application.engine.configuration; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class ApplicationShutdownProvider { + + private final ApplicationContext applicationContext; + + public void shutdown(Class<?> calledBy, int exitCode) { + String className = calledBy == null ? "unknown" : calledBy.getSimpleName(); + log.info("Application was signalled by {} to shutdown; exit code: {}", className, exitCode); + int ec = SpringApplication.exit(applicationContext, () -> exitCode); + System.exit(ec); + } + + public void shutdown(Class<?> calledBy) { + shutdown(calledBy, 0); + } + + public void shutdown() { + shutdown(null, 0); + } + +} diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/TaskService.java b/src/main/java/com/netgrif/application/engine/workflow/service/TaskService.java index 58c7db6f9d..910e759000 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/TaskService.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/TaskService.java @@ -772,12 +772,9 @@ public List<Task> save(List<Task> tasks) { } @Override - public void resolveUserRef(Case useCase) { - useCase.getTasks().forEach(taskPair -> { - Optional<Task> taskOptional = taskRepository.findById(taskPair.getTask()); - taskOptional.ifPresent(task -> resolveUserRef(task, useCase)); - }); - + public List<Task> resolveUserRef(Case useCase) { + List<Task> tasks = taskRepository.findAllBy_idIn(useCase.getTasks().stream().map(TaskPair::getTask).collect(Collectors.toList())); + return tasks.stream().map(task -> resolveUserRef(task, useCase)).collect(Collectors.toList()); } @Override @@ -786,9 +783,9 @@ public Task resolveUserRef(Task task, Case useCase) { task.getNegativeViewUsers().clear(); task.getUserRefs().forEach((id, permission) -> { List<String> userIds = getExistingUsers((UserListFieldValue) useCase.getDataSet().get(id).getValue()); - if (userIds != null && userIds.size() != 0 && permission.containsKey("view") && !permission.get("view")) { + if (userIds != null && !userIds.isEmpty() && permission.containsKey("view") && !permission.get("view")) { task.getNegativeViewUsers().addAll(userIds); - } else if (userIds != null && userIds.size() != 0) { + } else if (userIds != null && !userIds.isEmpty()) { task.addUsers(new HashSet<>(userIds), permission); } }); diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/WorkflowService.java b/src/main/java/com/netgrif/application/engine/workflow/service/WorkflowService.java index 3d9ac2dd2b..2cae7ed920 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/WorkflowService.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/WorkflowService.java @@ -228,7 +228,7 @@ public Case resolveUserRef(Case useCase) { private void resolveUserRefPermissions(Case useCase, String userListId, Map<String, Boolean> permission) { List<String> userIds = getExistingUsers((UserListFieldValue) useCase.getDataSet().get(userListId).getValue()); - if (userIds != null && userIds.size() != 0) { + if (userIds != null && !userIds.isEmpty()) { if (permission.containsKey("view") && !permission.get("view")) { useCase.getNegativeViewUsers().addAll(userIds); } else { @@ -241,7 +241,7 @@ private List<String> getExistingUsers(UserListFieldValue userListValue) { if (userListValue == null) return null; return userListValue.getUserValues().stream().map(UserFieldValue::getId) - .filter(id -> userService.resolveById(id, false) != null) + .filter(id -> userService.existsById(id)) .collect(Collectors.toList()); } diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/ITaskService.java b/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/ITaskService.java index f7b867eb79..691b2de4a9 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/ITaskService.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/ITaskService.java @@ -15,7 +15,6 @@ import com.netgrif.application.engine.workflow.web.responsebodies.TaskReference; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Locale; @@ -106,7 +105,7 @@ public interface ITaskService { DelegateTaskEventOutcome delegateTask(LoggedUser loggedUser, String delegatedId, String taskId, Map<String, String> params) throws TransitionNotExecutableException; - void resolveUserRef(Case useCase); + List<Task> resolveUserRef(Case useCase); Task resolveUserRef(Task task, Case useCase); @@ -127,4 +126,4 @@ public interface ITaskService { List<Task> save(List<Task> tasks); SetDataEventOutcome getMainOutcome(Map<String, SetDataEventOutcome> outcomes, String taskId); -} \ No newline at end of file +} From 938d94c1f2aa86f20c75ceb3296087d97acf6070 Mon Sep 17 00:00:00 2001 From: Milan Mladoniczky <6153201+tuplle@users.noreply.github.com> Date: Mon, 9 Mar 2026 14:55:12 +0100 Subject: [PATCH 77/92] Add `MigrationProperties` to configure migration behavior - Introduced `MigrationProperties` class to define configurable properties for migration, including skip list, cache eviction, and shutdown control. - Enhanced `MigrationOrderedCommandLineRunner` to respect `MigrationProperties` settings. - Improved logging for migration operations with additional conditions. --- .../MigrationOrderedCommandLineRunner.groovy | 15 ++++++- .../properties/MigrationProperties.java | 39 +++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/netgrif/application/engine/configuration/properties/MigrationProperties.java diff --git a/src/main/groovy/com/netgrif/application/engine/migration/MigrationOrderedCommandLineRunner.groovy b/src/main/groovy/com/netgrif/application/engine/migration/MigrationOrderedCommandLineRunner.groovy index ff4044873d..a3f2a0b7b9 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/MigrationOrderedCommandLineRunner.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/MigrationOrderedCommandLineRunner.groovy @@ -1,6 +1,7 @@ package com.netgrif.application.engine.migration import com.netgrif.application.engine.configuration.ApplicationShutdownProvider +import com.netgrif.application.engine.configuration.properties.MigrationProperties import com.netgrif.application.engine.petrinet.service.interfaces.IPetriNetService import com.netgrif.application.engine.startup.AbstractOrderedCommandLineRunner import org.slf4j.Logger @@ -19,6 +20,9 @@ abstract class MigrationOrderedCommandLineRunner extends AbstractOrderedCommandL @Autowired private MigrationRepository repository + @Autowired + private MigrationProperties migrationProperties + @Autowired private IPetriNetService service @@ -31,13 +35,20 @@ abstract class MigrationOrderedCommandLineRunner extends AbstractOrderedCommandL log.info("Migration ${title} was already applied") return } + if (migrationProperties.getSkip().contains(title)) { + log.info("Migration ${title} is skipped according to property nae.migration.skip") + return + } log.info("Applying migration ${title}") migrate() repository.save(new Migration(title)) - service.evictAllCaches() + if (migrationProperties.isEvictCaches()) { + log.info("Evicting all caches after migration") + service.evictAllCaches() + } log.info("Migration ${title} applied") - if (shutdownAfterFinish) { + if (shutdownAfterFinish || migrationProperties.isShutdownAfterMigration()) { sleep(100) shutdownProvider.shutdown(this.class) } diff --git a/src/main/java/com/netgrif/application/engine/configuration/properties/MigrationProperties.java b/src/main/java/com/netgrif/application/engine/configuration/properties/MigrationProperties.java new file mode 100644 index 0000000000..aeba1845e0 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/configuration/properties/MigrationProperties.java @@ -0,0 +1,39 @@ +package com.netgrif.application.engine.configuration.properties; + + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.LinkedHashSet; +import java.util.Set; + +@Data +@Configuration +@ConfigurationProperties(prefix = "nae.migration") +public class MigrationProperties { + + /** + * A list of migration process identifiers or names that should be skipped when applying migration logic. + * This property allows you to configure specific migrations that should be ignored, + * typically useful for excluding unnecessary or problematic migrations. + */ + private Set<String> skip = new LinkedHashSet<>(); + + /** + * Indicates whether caches should be evicted as part of the migration process. + * This property allows enabling or disabling the cache eviction mechanism, which + * is useful in ensuring consistency and up-to-date data during migration operations. + * Default value is {@code true}. + */ + private boolean evictCaches = true; + + /** + * Specifies whether the application should automatically shut down once the migration process is completed. + * This property can be used to terminate the application after the migration, ensuring a clean exit + * if no further operations are intended post-migration. + * Default value is {@code false}. + */ + private boolean shutdownAfterMigration = false; + +} From 815e2f6cb31b41d8430ccd07ff716ff2b43b22e4 Mon Sep 17 00:00:00 2001 From: Milan Mladoniczky <6153201+tuplle@users.noreply.github.com> Date: Mon, 9 Mar 2026 23:07:15 +0100 Subject: [PATCH 78/92] Increased `sleep` duration to allow elastic executor and related processes to flush work after migration. --- .../migration/MigrationOrderedCommandLineRunner.groovy | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/groovy/com/netgrif/application/engine/migration/MigrationOrderedCommandLineRunner.groovy b/src/main/groovy/com/netgrif/application/engine/migration/MigrationOrderedCommandLineRunner.groovy index a3f2a0b7b9..7c0c52d9bc 100644 --- a/src/main/groovy/com/netgrif/application/engine/migration/MigrationOrderedCommandLineRunner.groovy +++ b/src/main/groovy/com/netgrif/application/engine/migration/MigrationOrderedCommandLineRunner.groovy @@ -49,18 +49,21 @@ abstract class MigrationOrderedCommandLineRunner extends AbstractOrderedCommandL } log.info("Migration ${title} applied") if (shutdownAfterFinish || migrationProperties.isShutdownAfterMigration()) { - sleep(100) + // sleep is for elastic executor and other things to flush their work after migration. + // the number was chosen arbitrary by feeling 😅 + sleep(333) shutdownProvider.shutdown(this.class) } } - protected enableShutdownAfterFinish() { + protected void enableShutdownAfterFinish() { this.shutdownAfterFinish = true; } - protected disableShutdownAfterFinish() { + protected void disableShutdownAfterFinish() { this.shutdownAfterFinish = false; } abstract void migrate() + } From 00d04bbd2d74d3ad42ee8383edde5df941e91abf Mon Sep 17 00:00:00 2001 From: Milan Mladoniczky <6153201+tuplle@users.noreply.github.com> Date: Tue, 10 Mar 2026 11:08:16 +0100 Subject: [PATCH 79/92] Update GitHub Actions dependencies and services - Upgraded `actions/checkout` from v3 to v6 across workflows for feature improvements and compatibility. - Updated `actions/setup-java` from v3 to v5 and switched JDK distribution to `temurin`, adding Maven caching where applicable. - Upgraded MongoDB image to `mongo:6` and Elasticsearch image to `elasticsearch:7.17.28` in workflows. - Removed deprecated Maven cache setup and unused steps to simplify workflows. --- .github/workflows/master-build.yml | 12 +++++------ .github/workflows/pr-build.yml | 33 +++++++++++------------------ .github/workflows/release-build.yml | 22 +++++++++---------- 3 files changed, 29 insertions(+), 38 deletions(-) diff --git a/.github/workflows/master-build.yml b/.github/workflows/master-build.yml index 79c87fd6a6..d528df3bb0 100644 --- a/.github/workflows/master-build.yml +++ b/.github/workflows/master-build.yml @@ -9,12 +9,12 @@ jobs: name: Build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: java-version: 11 distribution: 'adopt' @@ -67,12 +67,12 @@ jobs: echo $ELASTIC_SEARCH_URL curl -fsSL "$ELASTIC_SEARCH_URL/_cat/health?h=status" - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: java-version: 11 distribution: 'adopt' @@ -113,13 +113,13 @@ jobs: needs: test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 with: token: ${{ secrets.PUSH_DOCS }} fetch-depth: 0 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: java-version: 11 distribution: 'adopt' diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index 0005231f06..4547940746 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -8,22 +8,19 @@ jobs: name: Build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: java-version: 11 - distribution: 'adopt' + distribution: 'temurin' + cache: maven - - name: Cache Maven packages - uses: actions/cache@v3 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 + - name: Temporary remove corrupted jars + run: rm -rf ~/.m2/repository/xml-apis - name: Build run: mvn clean verify -DskipTests=true @@ -35,7 +32,7 @@ jobs: timeout-minutes: 200 services: mongo: - image: mongo:4.4 + image: mongo:6 ports: - 27017:27017 @@ -45,7 +42,7 @@ jobs: - 6379:6379 elasticsearch: - image: elasticsearch:7.17.3 + image: elasticsearch:7.17.28 ports: - 9200:9200 - 9300:9300 @@ -66,15 +63,16 @@ jobs: echo $ELASTIC_SEARCH_URL curl -fsSL "$ELASTIC_SEARCH_URL/_cat/health?h=status" - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: java-version: 11 - distribution: 'adopt' + distribution: 'temurin' + cache: maven # - name: Cache SonarCloud packages # uses: actions/cache@v3 @@ -83,13 +81,6 @@ jobs: # key: ${{ runner.os }}-sonar # restore-keys: ${{ runner.os }}-sonar - - name: Cache Maven packages - uses: actions/cache@v3 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 - - name: Generate certificates run: cd src/main/resources/certificates && openssl genrsa -out keypair.pem 4096 && openssl rsa -in keypair.pem -pubout -out public.crt && openssl pkcs8 -topk8 -inform PEM -outform DER -nocrypt -in keypair.pem -out private.der && cd ../../../.. diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index b5fdc3067e..90d3265ebc 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -9,12 +9,12 @@ jobs: name: Build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: java-version: 11 distribution: 'adopt' @@ -71,12 +71,12 @@ jobs: echo $ELASTIC_SEARCH_URL curl -fsSL "$ELASTIC_SEARCH_URL/_cat/health?h=status" - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: java-version: 11 distribution: 'adopt' @@ -108,7 +108,7 @@ jobs: contents: read packages: write steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 - id: install-secret-key name: Install gpg secret key @@ -117,7 +117,7 @@ jobs: gpg --list-secret-keys --keyid-format LONG - name: Set up Maven Central Repository - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: java-version: 11 distribution: 'adopt' @@ -141,10 +141,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v6 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: java-version: 11 distribution: 'adopt' @@ -190,9 +190,9 @@ jobs: contents: read packages: write steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v5 with: java-version: '11' distribution: 'adopt' @@ -219,7 +219,7 @@ jobs: id-token: write security-events: write steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 - name: Build run: mvn clean package install -DskipTests=true From 384252c18b74cbd66e94c56f1e244e10367edf3a Mon Sep 17 00:00:00 2001 From: Milan Mladoniczky <6153201+tuplle@users.noreply.github.com> Date: Tue, 10 Mar 2026 11:45:40 +0100 Subject: [PATCH 80/92] Update Maven repositories and workflows - Added new Maven repositories for central and snapshot artifacts in `pom.xml`. - Switched JDK distribution to `temurin` and enabled Maven caching in GitHub workflows. - Upgraded MongoDB service to `mongo:6` and Elasticsearch to `elasticsearch:7.17.28`. - Updated dependencies including `xml-apis-ext` and various GitHub Actions versions for compatibility. --- .github/workflows/master-build.yml | 36 ++++++--------------- .github/workflows/pr-build.yml | 3 -- .github/workflows/release-build.yml | 49 ++++++++--------------------- pom.xml | 26 +++++++++++++++ 4 files changed, 48 insertions(+), 66 deletions(-) diff --git a/.github/workflows/master-build.yml b/.github/workflows/master-build.yml index d528df3bb0..a6400af651 100644 --- a/.github/workflows/master-build.yml +++ b/.github/workflows/master-build.yml @@ -17,14 +17,8 @@ jobs: uses: actions/setup-java@v5 with: java-version: 11 - distribution: 'adopt' - - - name: Cache Maven packages - uses: actions/cache@v3 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 + distribution: 'temurin' + cache: maven - name: Build run: mvn clean verify -DskipTests=true @@ -36,7 +30,7 @@ jobs: timeout-minutes: 90 services: mongo: - image: mongo:4.4 + image: mongo:6 ports: - 27017:27017 @@ -46,7 +40,7 @@ jobs: - 6379:6379 elasticsearch: - image: elasticsearch:7.17.3 + image: elasticsearch:7.17.28 ports: - 9200:9200 - 9300:9300 @@ -75,7 +69,8 @@ jobs: uses: actions/setup-java@v5 with: java-version: 11 - distribution: 'adopt' + distribution: 'temurin' + cache: maven # - name: Cache SonarCloud packages # uses: actions/cache@v3 @@ -84,13 +79,6 @@ jobs: # key: ${{ runner.os }}-sonar # restore-keys: ${{ runner.os }}-sonar - - name: Cache Maven packages - uses: actions/cache@v3 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 - - name: Generate certificates run: cd src/main/resources/certificates && openssl genrsa -out keypair.pem 4096 && openssl rsa -in keypair.pem -pubout -out public.crt && openssl pkcs8 -topk8 -inform PEM -outform DER -nocrypt -in keypair.pem -out private.der && cd ../../../.. @@ -122,14 +110,8 @@ jobs: uses: actions/setup-java@v5 with: java-version: 11 - distribution: 'adopt' - - - name: Cache Maven packages - uses: actions/cache@v3 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 + distribution: 'temurin' + cache: maven - name: Build run: mvn clean package install -DskipTests=true @@ -139,7 +121,7 @@ jobs: mvn javadoc:javadoc cp -r ./target/apidocs/* ./docs/javadoc/ - - uses: EndBug/add-and-commit@v8 + - uses: EndBug/add-and-commit@v9 with: add: docs pathspec_error_handling: exitImmediately diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index 4547940746..ff3edb08ca 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -19,9 +19,6 @@ jobs: distribution: 'temurin' cache: maven - - name: Temporary remove corrupted jars - run: rm -rf ~/.m2/repository/xml-apis - - name: Build run: mvn clean verify -DskipTests=true diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 90d3265ebc..64f8d110c2 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -17,19 +17,13 @@ jobs: uses: actions/setup-java@v5 with: java-version: 11 - distribution: 'adopt' + distribution: 'temurin' + cache: maven - name: Get Project Version from pom.xml uses: entimaniac/read-pom-version-action@1.0.0 id: getVersion - - name: Cache Maven packages - uses: actions/cache@v3 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 - - name: Build run: mvn clean verify -DskipTests=true @@ -79,14 +73,8 @@ jobs: uses: actions/setup-java@v5 with: java-version: 11 - distribution: 'adopt' - - - name: Cache Maven packages - uses: actions/cache@v3 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 + distribution: 'temurin' + cache: maven - name: Generate certificates run: cd src/main/resources/certificates && openssl genrsa -out keypair.pem 4096 && openssl rsa -in keypair.pem -pubout -out public.crt && openssl pkcs8 -topk8 -inform PEM -outform DER -nocrypt -in keypair.pem -out private.der && cd ../../../.. @@ -120,7 +108,8 @@ jobs: uses: actions/setup-java@v5 with: java-version: 11 - distribution: 'adopt' + distribution: 'temurin' + cache: maven server-id: central server-username: MAVEN_USERNAME server-password: MAVEN_PASSWORD @@ -147,20 +136,14 @@ jobs: uses: actions/setup-java@v5 with: java-version: 11 - distribution: 'adopt' - - - name: Cache Maven packages - uses: actions/cache@v3 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 + distribution: 'temurin' + cache: maven - name: Build run: mvn -P docker-build clean package install -DskipTests=true - name: Log in to Docker Hub - uses: docker/login-action@v1 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_AUTH_TOKEN }} @@ -170,14 +153,14 @@ jobs: id: getVersion - name: Push Version ${{ steps.getVersion.outputs.version }} - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v7 with: push: true tags: netgrif/application-engine:${{ steps.getVersion.outputs.version }} - name: Push Latest if: ${{ !contains(steps.getVersion.outputs.version, '-SNAPSHOT') }} - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v7 with: push: true tags: netgrif/application-engine:latest @@ -195,14 +178,8 @@ jobs: - uses: actions/setup-java@v5 with: java-version: '11' - distribution: 'adopt' - - - name: Cache Maven packages - uses: actions/cache@v3 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 + distribution: 'temurin' + cache: maven - name: Publish artifact on GitHub Packages run: mvn -B -P github-publish clean deploy -DskipTests diff --git a/pom.xml b/pom.xml index e57436ea5c..470b48b79f 100644 --- a/pom.xml +++ b/pom.xml @@ -91,6 +91,27 @@ <!-- <id>mulesoft</id>--> <!-- <url>https://repository.mulesoft.org/nexus/content/repositories/public/</url>--> <!-- </repository>--> + <repository> + <id>central</id> + <url>https://repo.maven.apache.org/maven2</url> + <releases> + <enabled>true</enabled> + </releases> + <snapshots> + <enabled>false</enabled> + </snapshots> + </repository> + <repository> + <name>Central Portal Snapshots</name> + <id>central-portal-snapshots</id> + <url>https://central.sonatype.com/repository/maven-snapshots/</url> + <releases> + <enabled>false</enabled> + </releases> + <snapshots> + <enabled>true</enabled> + </snapshots> + </repository> <repository> <id>jitpack.io</id> <url>https://jitpack.io</url> @@ -315,6 +336,11 @@ <artifactId>batik-all</artifactId> <version>1.17</version> </dependency> + <dependency> + <groupId>xml-apis</groupId> + <artifactId>xml-apis-ext</artifactId> + <version>1.3.04</version> + </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> From 9f90ce5964c94854e901810e2c5cbdc94789fd5c Mon Sep 17 00:00:00 2001 From: Milan Mladoniczky <6153201+tuplle@users.noreply.github.com> Date: Tue, 10 Mar 2026 12:21:17 +0100 Subject: [PATCH 81/92] Add UserServiceTest and improve graceful shutdown handling - Introduced `UserServiceTest` to validate user existence and ID checks. - Enhanced `ApplicationShutdownProvider` to properly shut down thread pool executors for graceful application termination. --- .../ApplicationShutdownProvider.java | 8 ++++ .../engine/auth/service/UserServiceTest.java | 41 +++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 src/test/java/com/netgrif/application/engine/auth/service/UserServiceTest.java diff --git a/src/main/java/com/netgrif/application/engine/configuration/ApplicationShutdownProvider.java b/src/main/java/com/netgrif/application/engine/configuration/ApplicationShutdownProvider.java index 57c0c18a73..db24485e5d 100644 --- a/src/main/java/com/netgrif/application/engine/configuration/ApplicationShutdownProvider.java +++ b/src/main/java/com/netgrif/application/engine/configuration/ApplicationShutdownProvider.java @@ -4,6 +4,8 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.context.ApplicationContext; +import org.springframework.core.task.TaskExecutor; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.stereotype.Component; @Slf4j @@ -12,10 +14,16 @@ public class ApplicationShutdownProvider { private final ApplicationContext applicationContext; + private final TaskExecutor taskExecutor; public void shutdown(Class<?> calledBy, int exitCode) { String className = calledBy == null ? "unknown" : calledBy.getSimpleName(); log.info("Application was signalled by {} to shutdown; exit code: {}", className, exitCode); + if (taskExecutor != null && taskExecutor instanceof ThreadPoolTaskExecutor) { + log.info("Shutting down thread pool executor"); + ThreadPoolTaskExecutor executor = (ThreadPoolTaskExecutor) taskExecutor; + executor.shutdown(); + } int ec = SpringApplication.exit(applicationContext, () -> exitCode); System.exit(ec); } diff --git a/src/test/java/com/netgrif/application/engine/auth/service/UserServiceTest.java b/src/test/java/com/netgrif/application/engine/auth/service/UserServiceTest.java new file mode 100644 index 0000000000..874e29fd24 --- /dev/null +++ b/src/test/java/com/netgrif/application/engine/auth/service/UserServiceTest.java @@ -0,0 +1,41 @@ +package com.netgrif.application.engine.auth.service; + +import com.netgrif.application.engine.TestHelper; +import com.netgrif.application.engine.auth.domain.IUser; +import com.netgrif.application.engine.auth.service.interfaces.IUserService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SpringBootTest +@ActiveProfiles({"test"}) +@ExtendWith(SpringExtension.class) +public class UserServiceTest { + + @Autowired + private IUserService service; + + @Autowired + private TestHelper testHelper; + + @BeforeEach + void before() { + testHelper.truncateDbs(); + } + + @Test + public void shouldUserExist() { + IUser user = service.findByEmail("super@netgrif.com", true); + assertNotNull(user); + boolean userExists = service.existsById(user.getStringId()); + assertTrue(userExists); + } + +} From b2f8a0bcc090b5d7f06f9516f0841be05a94c773 Mon Sep 17 00:00:00 2001 From: Milan Mladoniczky <6153201+tuplle@users.noreply.github.com> Date: Wed, 1 Apr 2026 10:14:59 +0200 Subject: [PATCH 82/92] [NAE-2401] Timestamp of case dataSet change - Add endpoint to reload tasks of all cases --- .../workflow/web/WorkflowController.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/main/java/com/netgrif/application/engine/workflow/web/WorkflowController.java b/src/main/java/com/netgrif/application/engine/workflow/web/WorkflowController.java index 23bfb683f9..d063e4ca27 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/web/WorkflowController.java +++ b/src/main/java/com/netgrif/application/engine/workflow/web/WorkflowController.java @@ -31,6 +31,7 @@ import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.Resource; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.querydsl.binding.QuerydslPredicate; import org.springframework.data.web.PagedResourcesAssembler; @@ -191,6 +192,37 @@ public MessageResource reloadTasks(@PathVariable("id") String caseId) { } } + @PreAuthorize("@authorizationService.hasAuthority('ADMIN')") + @Operation(summary = "Reload tasks of all cases", + description = "Caller must have the ADMIN role", + security = {@SecurityRequirement(name = "BasicAuth")}) + @GetMapping(value = "/case/reload_all", produces = MediaTypes.HAL_JSON_VALUE) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "OK"), + @ApiResponse(responseCode = "403", description = "Caller doesn't fulfill the authorisation requirements"), + }) + public MessageResource reloadTasksOfAllCases(Authentication auth, Locale locale) { + log.info("Starting reload tasks of all cases to repair database integrity."); + try { + long caseCount = workflowService.count(Map.of(), (LoggedUser) auth.getPrincipal(), locale); + log.info("Number of cases: {}", caseCount); + long pageCount = Double.valueOf(Math.ceil((double) caseCount / 1000)).longValue(); + log.info("Calculated number of pages: {}", pageCount); + + for (int i = 0; i < pageCount; i++) { + PageRequest pageRequest = PageRequest.of(i, 1000); + Page<Case> cases = workflowService.getAll(pageRequest); + log.info("Processing page {} of {}", i + 1, pageCount); + cases.forEach(aCase -> taskService.reloadTasks(aCase)); + } + + return MessageResource.successMessage("Task reloaded for " + caseCount + " cases"); + } catch (Exception e) { + log.error("Reloading tasks of cases has failed:", e); + return MessageResource.errorMessage("Reloading tasks in cases has failed!"); + } + } + @Deprecated @PreAuthorize("@authorizationService.hasAuthority('ADMIN')") @Operation(summary = "Get all case data", security = {@SecurityRequirement(name = "BasicAuth")}) From 952f4c01e491041415cdf577073067853e0c77e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kov=C3=A1=C4=8Dik?= <kovacik@netgrif.com> Date: Thu, 2 Apr 2026 11:07:20 +0200 Subject: [PATCH 83/92] [NAE-2401] Timestamp of case dataSet change - add lastModifiedDataSet property to Case and transient property changed to DataField, which is set when value, choices or options on DataField are set --- .../engine/elastic/domain/ElasticCase.java | 4 ++++ .../engine/elastic/service/ElasticIndexService.java | 3 +++ .../application/engine/workflow/domain/Case.java | 4 ++++ .../engine/workflow/domain/DataField.java | 13 +++++++++++++ .../engine/workflow/service/DataService.java | 3 ++- .../engine/workflow/service/WorkflowService.java | 13 +++++++++++++ 6 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticCase.java b/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticCase.java index 89dda11f55..bcd17cae11 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticCase.java +++ b/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticCase.java @@ -40,6 +40,8 @@ public class ElasticCase { private Long lastModified; + private Long lastModifiedDataSet; + @Field(type = Keyword) private String stringId; @@ -112,6 +114,7 @@ public ElasticCase(Case useCase) { uriNodeId = useCase.getUriNodeId(); mongoId = useCase.getStringId(); //TODO: Duplication lastModified = Timestamp.valueOf(useCase.getLastModified()).getTime(); + lastModifiedDataSet = Timestamp.valueOf(useCase.getLastModifiedDataSet()).getTime(); processIdentifier = useCase.getProcessIdentifier(); processId = useCase.getPetriNetId(); visualId = useCase.getVisualId(); @@ -137,6 +140,7 @@ public ElasticCase(Case useCase) { public void update(ElasticCase useCase) { version++; lastModified = useCase.getLastModified(); + lastModifiedDataSet = useCase.getLastModifiedDataSet(); if (useCase.getUriNodeId() != null) { uriNodeId = useCase.getUriNodeId(); } diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java index 0b89b7da00..816822841a 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java @@ -486,6 +486,9 @@ private void prepareCase(Case useCase) { if (useCase.getLastModified() == null) { useCase.setLastModified(LocalDateTime.now()); } + if (useCase.getLastModifiedDataSet() == null) { + useCase.setLastModifiedDataSet(LocalDateTime.now()); + } } /** diff --git a/src/main/java/com/netgrif/application/engine/workflow/domain/Case.java b/src/main/java/com/netgrif/application/engine/workflow/domain/Case.java index 1559cbe001..4fc1ecf018 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/domain/Case.java +++ b/src/main/java/com/netgrif/application/engine/workflow/domain/Case.java @@ -42,6 +42,10 @@ public class Case implements Serializable { @Setter private LocalDateTime lastModified; + @Getter + @Setter + private LocalDateTime lastModifiedDataSet; + @Getter private String visualId; diff --git a/src/main/java/com/netgrif/application/engine/workflow/domain/DataField.java b/src/main/java/com/netgrif/application/engine/workflow/domain/DataField.java index b37ebe82b3..6ed0a5e915 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/domain/DataField.java +++ b/src/main/java/com/netgrif/application/engine/workflow/domain/DataField.java @@ -14,6 +14,7 @@ import lombok.Getter; import lombok.Setter; import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.annotation.Transient; import java.io.Serializable; import java.time.LocalDateTime; @@ -65,6 +66,11 @@ public class DataField implements Referencable, Serializable { @Setter private Component component; + @Transient + @Getter + @Setter + private boolean changed = false; + public DataField() { behavior = new HashMap<>(); dataRefComponents = new HashMap<>(); @@ -83,11 +89,13 @@ public void setBehavior(Map<String, Set<FieldBehavior>> behavior) { public void setValue(Object value) { this.value = value; update(); + changed(); } public void setChoices(Set<I18nString> choices) { this.choices = choices; update(); + changed(); } public void setAllowedNets(List<String> allowedNets) { @@ -103,6 +111,7 @@ public void setFilterMetadata(Map<String, Object> filterMetadata) { public void setOptions(Map<String, I18nString> options) { this.options = options; update(); + changed(); } public void setValidations(List<Validation> validations) { @@ -210,6 +219,10 @@ private void update() { version++; } + private void changed() { + changed = true; + } + public boolean isNewerThen(DataField other) { return version > other.getVersion(); } diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java b/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java index c97e5e0546..548ee926a2 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java @@ -692,7 +692,8 @@ public SetDataEventOutcome saveFiles(String taskId, String fieldId, MultipartFil } private List<EventOutcome> getChangedFieldByFileFieldContainer(String fieldId, Task referencingTask, Case useCase, Map<String, String> params) { - List<EventOutcome> outcomes = new ArrayList<>(resolveDataEvents(useCase.getPetriNet().getField(fieldId).get(), DataEventType.SET, + List<EventOutcome> outcomes = new ArrayList<>(); + outcomes.addAll( resolveDataEvents(useCase.getPetriNet().getField(fieldId).get(), DataEventType.SET, EventPhase.PRE, useCase, referencingTask, params)); outcomes.addAll(resolveDataEvents(useCase.getPetriNet().getField(fieldId).get(), DataEventType.SET, EventPhase.POST, useCase, referencingTask, params)); diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/WorkflowService.java b/src/main/java/com/netgrif/application/engine/workflow/service/WorkflowService.java index 2cae7ed920..130beb3090 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/WorkflowService.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/WorkflowService.java @@ -122,10 +122,14 @@ public Case save(Case useCase) { if (useCase.getPetriNet() == null) { setPetriNet(useCase); } + checkChangedDataSet(useCase); encryptDataSet(useCase); useCase = repository.save(useCase); try { setImmediateDataFields(useCase); + if (useCase.getLastModifiedDataSet() == null) { + useCase.setLastModifiedDataSet(LocalDateTime.now()); + } elasticCaseService.indexNow(this.caseMappingService.transform(useCase)); } catch (Exception e) { log.error("Indexing failed [" + useCase.getStringId() + "]", e); @@ -598,4 +602,13 @@ private EventOutcome addMessageToOutcome(PetriNet net, CaseEventType type, Event } return outcome; } + + private void checkChangedDataSet(Case useCase) { + for (DataField data : useCase.getDataSet().values()) { + if (data.isChanged()) { + useCase.setLastModifiedDataSet(LocalDateTime.now()); + return; + } + }; + }; } From bf22124a5d0f09be4239235e5eadcbe2d9d91f01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kov=C3=A1=C4=8Dik?= <kovacik@netgrif.com> Date: Thu, 2 Apr 2026 12:43:58 +0200 Subject: [PATCH 84/92] [NAE-2101] Release 6.4.2 - change changelog and version --- CHANGELOG.md | 10 ++++++++++ pom.xml | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c250d2f1b..3b9dc8f6ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,9 +12,19 @@ Full Changelog: [https://github.com/netgrif/application-engine/commits/v6.4.2](h ### Fixed - [NAE-2225] Not possible to set empty options using setData - [NAE-2231] Unable to change behavior of taskRef on finish event without error message +- [NAE-2100] Case view export button as NAE feature +- [NAE-2136] Speed up Elasticsearch reindex +- Refactor ObjectMapper configuration for Elasticsearch +- Remove custom serializers for startDate in ElasticTask +- [NAE-2342] Improve Quartz configuration +- Minor improvements to manage migrations ### Added - [NAE-2100] Case view export button as NAE feature +- [NAE-2136] Speed up Elasticsearch reindex +- [NAE-2303] TaskRef Security Improvements +- [NAE-2310] Elasticsearch fulltext query input sanitation +- [NAE-2246] - Enable Redis TLS & Configure Redis Sentinel ## [6.4.1](https://github.com/netgrif/application-engine/releases/tag/v6.4.1) (2025-03-19) diff --git a/pom.xml b/pom.xml index 470b48b79f..a83717a9cf 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ <groupId>com.netgrif</groupId> <artifactId>application-engine</artifactId> - <version>6.4.2-SNAPSHOT</version> + <version>6.4.2</version> <packaging>jar</packaging> <name>NETGRIF Application Engine</name> From daba15a12f3d3102632c59270c08dbe14d5bac58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kov=C3=A1=C4=8Dik?= <kovacik@netgrif.com> Date: Thu, 2 Apr 2026 12:59:41 +0200 Subject: [PATCH 85/92] [NAE-2101] Release 6.4.2 - fix duplicity --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b9dc8f6ec..27538131e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,16 +10,16 @@ Full Changelog: [https://github.com/netgrif/application-engine/commits/v6.4.2](h ## [6.4.2](https://github.com/netgrif/application-engine/releases/tag/v6.4.2) (2025-05-16) ### Fixed + - [NAE-2225] Not possible to set empty options using setData - [NAE-2231] Unable to change behavior of taskRef on finish event without error message -- [NAE-2100] Case view export button as NAE feature -- [NAE-2136] Speed up Elasticsearch reindex - Refactor ObjectMapper configuration for Elasticsearch - Remove custom serializers for startDate in ElasticTask - [NAE-2342] Improve Quartz configuration - Minor improvements to manage migrations ### Added + - [NAE-2100] Case view export button as NAE feature - [NAE-2136] Speed up Elasticsearch reindex - [NAE-2303] TaskRef Security Improvements From f87e406417fab6b5cb538c8cacbc5f2f88fcbed4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kov=C3=A1=C4=8Dik?= <kovacik@netgrif.com> Date: Thu, 2 Apr 2026 15:59:41 +0200 Subject: [PATCH 86/92] [NAE-2401] Timestamp of case dataSet change - changes according to PR --- .../engine/elastic/domain/ElasticCase.java | 4 +++- .../elastic/service/ElasticIndexService.java | 3 --- .../engine/workflow/domain/Case.java | 10 ++++---- .../engine/workflow/domain/DataField.java | 24 ++++++++++++++++--- .../workflow/service/WorkflowService.java | 15 ++++++------ 5 files changed, 37 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticCase.java b/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticCase.java index bcd17cae11..3f5c1996af 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticCase.java +++ b/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticCase.java @@ -114,7 +114,9 @@ public ElasticCase(Case useCase) { uriNodeId = useCase.getUriNodeId(); mongoId = useCase.getStringId(); //TODO: Duplication lastModified = Timestamp.valueOf(useCase.getLastModified()).getTime(); - lastModifiedDataSet = Timestamp.valueOf(useCase.getLastModifiedDataSet()).getTime(); + if (useCase.getLastModifiedDataSet() != null) { + lastModifiedDataSet = Timestamp.valueOf(useCase.getLastModifiedDataSet()).getTime(); + } processIdentifier = useCase.getProcessIdentifier(); processId = useCase.getPetriNetId(); visualId = useCase.getVisualId(); diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java index 816822841a..0b89b7da00 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java @@ -486,9 +486,6 @@ private void prepareCase(Case useCase) { if (useCase.getLastModified() == null) { useCase.setLastModified(LocalDateTime.now()); } - if (useCase.getLastModifiedDataSet() == null) { - useCase.setLastModifiedDataSet(LocalDateTime.now()); - } } /** diff --git a/src/main/java/com/netgrif/application/engine/workflow/domain/Case.java b/src/main/java/com/netgrif/application/engine/workflow/domain/Case.java index 4fc1ecf018..039f6db8b7 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/domain/Case.java +++ b/src/main/java/com/netgrif/application/engine/workflow/domain/Case.java @@ -243,10 +243,10 @@ public void populateDataSet(IInitValueExpressionEvaluator initValueExpressionEva this.dataSet.get(key).setComponent(field.getComponent()); } if (field instanceof UserField) { - this.dataSet.get(key).setChoices(((UserField) field).getRoles().stream().map(I18nString::new).collect(Collectors.toSet())); + this.dataSet.get(key).setChoices(((UserField) field).getRoles().stream().map(I18nString::new).collect(Collectors.toSet()), false); } if (field instanceof UserListField) { - this.dataSet.get(key).setChoices(((UserListField) field).getRoles().stream().map(I18nString::new).collect(Collectors.toSet())); + this.dataSet.get(key).setChoices(((UserListField) field).getRoles().stream().map(I18nString::new).collect(Collectors.toSet()), false); } if (field instanceof FieldWithAllowedNets) { this.dataSet.get(key).setAllowedNets(((FieldWithAllowedNets) field).getAllowedNets()); @@ -261,9 +261,9 @@ public void populateDataSet(IInitValueExpressionEvaluator initValueExpressionEva dynamicChoicesFields.add((ChoiceField<?>) field); } }); - dynamicInitFields.forEach(field -> this.dataSet.get(field.getImportId()).setValue(initValueExpressionEvaluator.evaluate(this, field, params))); - dynamicChoicesFields.forEach(field -> this.dataSet.get(field.getImportId()).setChoices(initValueExpressionEvaluator.evaluateChoices(this, field, params))); - dynamicOptionsFields.forEach(field -> this.dataSet.get(field.getImportId()).setOptions(initValueExpressionEvaluator.evaluateOptions(this, field, params))); + dynamicInitFields.forEach(field -> this.dataSet.get(field.getImportId()).setValue(initValueExpressionEvaluator.evaluate(this, field, params), false)); + dynamicChoicesFields.forEach(field -> this.dataSet.get(field.getImportId()).setChoices(initValueExpressionEvaluator.evaluateChoices(this, field, params), false)); + dynamicOptionsFields.forEach(field -> this.dataSet.get(field.getImportId()).setOptions(initValueExpressionEvaluator.evaluateOptions(this, field, params), false)); populateDataSetBehaviorAndComponents(); } diff --git a/src/main/java/com/netgrif/application/engine/workflow/domain/DataField.java b/src/main/java/com/netgrif/application/engine/workflow/domain/DataField.java index 6ed0a5e915..5734a9b3e9 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/domain/DataField.java +++ b/src/main/java/com/netgrif/application/engine/workflow/domain/DataField.java @@ -87,15 +87,27 @@ public void setBehavior(Map<String, Set<FieldBehavior>> behavior) { } public void setValue(Object value) { + setValue(value, true); + } + + public void setValue(Object value, boolean trackChange) { this.value = value; update(); - changed(); + if (trackChange) { + changed(); + } } public void setChoices(Set<I18nString> choices) { + setChoices(choices, true); + } + + public void setChoices(Set<I18nString> choices, boolean trackChange) { this.choices = choices; update(); - changed(); + if (trackChange) { + changed(); + } } public void setAllowedNets(List<String> allowedNets) { @@ -109,9 +121,15 @@ public void setFilterMetadata(Map<String, Object> filterMetadata) { } public void setOptions(Map<String, I18nString> options) { + setOptions(options, true); + } + + public void setOptions(Map<String, I18nString> options, boolean trackChange) { this.options = options; update(); - changed(); + if (trackChange) { + changed(); + } } public void setValidations(List<Validation> validations) { diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/WorkflowService.java b/src/main/java/com/netgrif/application/engine/workflow/service/WorkflowService.java index 130beb3090..87bcb62a02 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/WorkflowService.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/WorkflowService.java @@ -127,9 +127,6 @@ public Case save(Case useCase) { useCase = repository.save(useCase); try { setImmediateDataFields(useCase); - if (useCase.getLastModifiedDataSet() == null) { - useCase.setLastModifiedDataSet(LocalDateTime.now()); - } elasticCaseService.indexNow(this.caseMappingService.transform(useCase)); } catch (Exception e) { log.error("Indexing failed [" + useCase.getStringId() + "]", e); @@ -480,7 +477,7 @@ private void resolveTaskRefs(Case useCase) { useCase.getPetriNet().getDataSet().values().stream().filter(f -> f instanceof TaskField).map(TaskField.class::cast).forEach(field -> { if (field.getDefaultValue() != null && !field.getDefaultValue().isEmpty() && useCase.getDataField(field.getStringId()).getValue() != null && useCase.getDataField(field.getStringId()).getValue().equals(field.getDefaultValue())) { - useCase.getDataField(field.getStringId()).setValue(new ArrayList<>()); + useCase.getDataField(field.getStringId()).setValue(new ArrayList<>(), false); List<TaskPair> taskPairList = useCase.getTasks().stream().filter(t -> (field.getDefaultValue().contains(t.getTransition()))).collect(Collectors.toList()); if (!taskPairList.isEmpty()) { @@ -568,7 +565,7 @@ private void applyCryptoMethodOnDataSet(Case useCase, Function<Pair<String, Stri if (value == null) continue; - dataField.setValue(method.apply(Pair.of(value, encryption))); + dataField.setValue(method.apply(Pair.of(value, encryption)), false); } } @@ -604,11 +601,15 @@ private EventOutcome addMessageToOutcome(PetriNet net, CaseEventType type, Event } private void checkChangedDataSet(Case useCase) { + boolean changed = false; for (DataField data : useCase.getDataSet().values()) { if (data.isChanged()) { - useCase.setLastModifiedDataSet(LocalDateTime.now()); - return; + changed = true; + data.setChanged(false); } }; + if (changed) { + useCase.setLastModifiedDataSet(LocalDateTime.now()); + } }; } From 2eba5228c969a07b9dbbdf611628a964b16fd226 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kov=C3=A1=C4=8Dik?= <kovacik@netgrif.com> Date: Sat, 4 Apr 2026 11:58:42 +0200 Subject: [PATCH 87/92] [NAE-2401] Timestamp of case dataSet change - changes according to PR --- .../application/engine/elastic/domain/ElasticCase.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticCase.java b/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticCase.java index 3f5c1996af..630ca0c403 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticCase.java +++ b/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticCase.java @@ -142,7 +142,9 @@ public ElasticCase(Case useCase) { public void update(ElasticCase useCase) { version++; lastModified = useCase.getLastModified(); - lastModifiedDataSet = useCase.getLastModifiedDataSet(); + if (useCase.getLastModifiedDataSet() != null) { + lastModifiedDataSet = useCase.getLastModifiedDataSet(); + } if (useCase.getUriNodeId() != null) { uriNodeId = useCase.getUriNodeId(); } From 0f554e3ba31df10157e321517a2aec5708b6c10d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kov=C3=A1=C4=8Dik?= <kovacik@netgrif.com> Date: Tue, 7 Apr 2026 11:22:47 +0200 Subject: [PATCH 88/92] [NAE-2401] Timestamp of case dataSet change - add task to changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27538131e4..7706b72c5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ Full Changelog: [https://github.com/netgrif/application-engine/commits/v6.4.2](h - [NAE-2303] TaskRef Security Improvements - [NAE-2310] Elasticsearch fulltext query input sanitation - [NAE-2246] - Enable Redis TLS & Configure Redis Sentinel +- [NAE-2401] Timestamp of case dataSet change ## [6.4.1](https://github.com/netgrif/application-engine/releases/tag/v6.4.1) (2025-03-19) From d043145394f9863f3eccc0694ffde292ab8c15ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kov=C3=A1=C4=8Dik?= <kovacik@netgrif.com> Date: Tue, 7 Apr 2026 13:57:11 +0200 Subject: [PATCH 89/92] [NAE-2101] Release 6.4.2 - fix vulnerability --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a83717a9cf..d9f4b8ffac 100644 --- a/pom.xml +++ b/pom.xml @@ -478,7 +478,7 @@ <dependency> <groupId>com.netgrif</groupId> <artifactId>quartz-mongodb-connector</artifactId> - <version>1.0.0</version> + <version>1.0.1</version> </dependency> <!-- OpenAPI 3 / Swagger Docs --> From a43357c8eb5b4b8806da3e5bd0b5f8132954847b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kov=C3=A1=C4=8Dik?= <kovacik@netgrif.com> Date: Tue, 7 Apr 2026 14:44:04 +0200 Subject: [PATCH 90/92] [NAE-2101] Release 6.4.2 - fix null pointer --- .../application/engine/export/service/XlsExportService.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/com/netgrif/application/engine/export/service/XlsExportService.java b/src/main/java/com/netgrif/application/engine/export/service/XlsExportService.java index 43b7fff4f9..39fda32e16 100644 --- a/src/main/java/com/netgrif/application/engine/export/service/XlsExportService.java +++ b/src/main/java/com/netgrif/application/engine/export/service/XlsExportService.java @@ -235,6 +235,9 @@ private Object resolveMetaFieldValue(Case caze, ExportedField field) { } private String getProcessIdentifierFromFilteredRequest(FilteredCasesRequest request) { + if (request.getQuery() == null || request.getQuery().isEmpty() || request.getQuery().get(0).query == null || request.getQuery().get(0).query.isBlank()) { + return ""; + } return Arrays.stream(request.getQuery().get(0).query.split("\\s+")) .filter(part -> part.startsWith("processIdentifier:")) .map(part -> part.split(":", 2)[1]) From 6eb1126ee12fac464a0fcc9a518a47041a338802 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kov=C3=A1=C4=8Dik?= <kovacik@netgrif.com> Date: Tue, 7 Apr 2026 14:49:00 +0200 Subject: [PATCH 91/92] [NAE-2101] Release 6.4.2 - fix null pointer --- .../application/engine/export/service/XlsExportService.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/netgrif/application/engine/export/service/XlsExportService.java b/src/main/java/com/netgrif/application/engine/export/service/XlsExportService.java index 39fda32e16..40b358b018 100644 --- a/src/main/java/com/netgrif/application/engine/export/service/XlsExportService.java +++ b/src/main/java/com/netgrif/application/engine/export/service/XlsExportService.java @@ -235,7 +235,11 @@ private Object resolveMetaFieldValue(Case caze, ExportedField field) { } private String getProcessIdentifierFromFilteredRequest(FilteredCasesRequest request) { - if (request.getQuery() == null || request.getQuery().isEmpty() || request.getQuery().get(0).query == null || request.getQuery().get(0).query.isBlank()) { + if (request.getQuery() == null || + request.getQuery().isEmpty() || + request.getQuery().get(0) == null || + request.getQuery().get(0).query == null || + request.getQuery().get(0).query.isBlank()) { return ""; } return Arrays.stream(request.getQuery().get(0).query.split("\\s+")) From fe4cda6fafad4bb5cb26dc61a03f46b8dcf061fd Mon Sep 17 00:00:00 2001 From: palajsamuel <palaj@netgrif.com> Date: Thu, 30 Apr 2026 12:25:57 +0200 Subject: [PATCH 92/92] [NAE-2060] Merge 6.2.10, 6.4.2 into 6.5.0 - after merge fixes --- .../workflow/domain/menu/MenuItemBody.java | 268 -- .../domain/menu/MenuItemConstants.java | 66 - .../menu/tabbed_case_view_configuration.xml | 19 + .../engine-processes/preference_item.xml | 2686 ----------------- .../export/service/XlsExportServiceTest.java | 4 +- 5 files changed, 21 insertions(+), 3022 deletions(-) delete mode 100644 src/main/java/com/netgrif/application/engine/workflow/domain/menu/MenuItemBody.java delete mode 100644 src/main/java/com/netgrif/application/engine/workflow/domain/menu/MenuItemConstants.java delete mode 100644 src/main/resources/petriNets/engine-processes/preference_item.xml diff --git a/src/main/java/com/netgrif/application/engine/workflow/domain/menu/MenuItemBody.java b/src/main/java/com/netgrif/application/engine/workflow/domain/menu/MenuItemBody.java deleted file mode 100644 index 5f59e15ccd..0000000000 --- a/src/main/java/com/netgrif/application/engine/workflow/domain/menu/MenuItemBody.java +++ /dev/null @@ -1,268 +0,0 @@ -package com.netgrif.application.engine.workflow.domain.menu; - -import com.netgrif.application.engine.petrinet.domain.I18nString; -import com.netgrif.application.engine.petrinet.domain.dataset.FieldType; -import com.netgrif.application.engine.petrinet.domain.dataset.logic.action.ActionDelegate; -import com.netgrif.application.engine.workflow.domain.Case; -import lombok.Data; -import lombok.NoArgsConstructor; - -import javax.annotation.Nullable; -import java.text.Normalizer; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -/** - * Class, that holds configurable attributes of menu item. In case of attribute addition, please update also - * {@link MenuItemBody#toDataSet(String, String, boolean)} method. - */ -@Data -@NoArgsConstructor -public class MenuItemBody { - - // generic attributes - private I18nString menuName; - private I18nString tabName; - private String menuIcon = "filter_none"; - private String tabIcon; - private String uri; - private String identifier; - private Case filter; - private Map<String, I18nString> allowedRoles; - private Map<String, I18nString> bannedRoles; - private boolean useTabIcon = true; - private boolean useCustomView = false; - private String customViewSelector; - - // case view attributes - private String caseViewSearchType = "fulltext_advanced"; - private String createCaseButtonTitle; - private String createCaseButtonIcon = "add"; - private boolean caseRequireTitleInCreation = true; - private boolean showCreateCaseButton = true; - private String bannedNetsInCreation; - private boolean caseShowMoreMenu = false; - private boolean caseAllowHeaderTableMode = true; - private List<String> caseHeadersMode = new ArrayList<>(List.of("sort", "edit", "search")); - private String caseHeadersDefaultMode = "sort"; - private List<String> caseDefaultHeaders; - private boolean caseIsHeaderModeChangeable = true; - private boolean caseUseDefaultHeaders = true; - private boolean caseAllowExport = false; - - // task view attributes - private Case additionalFilter; - private boolean mergeFilters = true; - private String taskViewSearchType = "fulltext_advanced"; - private List<String> taskHeadersMode = new ArrayList<>(List.of("sort", "edit")); - private String taskHeadersDefaultMode = "sort"; - private boolean taskIsHeaderModeChangeable = true; - private boolean taskAllowHeaderTableMode = true; - private boolean taskUseDefaultHeaders = true; - private List<String> taskDefaultHeaders; - private boolean taskShowMoreMenu = true; - - public MenuItemBody(I18nString name, String icon) { - this.menuName = name; - this.tabName = name; - this.menuIcon = icon; - this.tabIcon = icon; - } - - public MenuItemBody(I18nString menuName, I18nString tabName, String menuIcon, String tabIcon) { - this.menuName = menuName; - this.tabName = tabName; - this.menuIcon = menuIcon; - this.tabIcon = tabIcon; - } - - public MenuItemBody(String uri, String identifier, I18nString name, String icon) { - this.uri = uri; - this.identifier = identifier; - this.menuName = name; - this.tabName = name; - this.menuIcon = icon; - this.tabIcon = icon; - } - - public MenuItemBody(String uri, String identifier, I18nString menuName, I18nString tabName, String menuIcon, String tabIcon) { - this.uri = uri; - this.identifier = identifier; - this.menuName = menuName; - this.tabName = tabName; - this.menuIcon = menuIcon; - this.tabIcon = tabIcon; - } - - public MenuItemBody(String uri, String identifier, String name, String icon) { - this.uri = uri; - this.identifier = identifier; - this.menuName = new I18nString(name); - this.tabName = new I18nString(name); - this.menuIcon = icon; - this.tabIcon = icon; - } - - public MenuItemBody(String uri, String identifier, String menuName, String tabName, String menuIcon, String tabIcon) { - this.uri = uri; - this.identifier = identifier; - this.menuName = new I18nString(menuName); - this.tabName = new I18nString(tabName); - this.menuIcon = menuIcon; - this.tabIcon = tabIcon; - } - - private static void putDataSetEntry(Map<String, Map<String, Object>> dataSet, MenuItemConstants fieldId, FieldType fieldType, - @Nullable Object fieldValue) { - Map<String, Object> fieldMap = new LinkedHashMap<>(); - fieldMap.put("type", fieldType.getName()); - fieldMap.put("value", fieldValue); - dataSet.put(fieldId.getAttributeId(), fieldMap); - } - - private static String sanitize(String input) { - if (input == null) { - return null; - } - return Normalizer.normalize(input.trim(), Normalizer.Form.NFD) - .replaceAll("[^\\p{ASCII}]", "") - .replaceAll("\\p{InCombiningDiacriticalMarks}+", "") - .replaceAll("[\\W-]+", "-") - .toLowerCase(); - } - - public String getIdentifier() { - return sanitize(this.identifier); - } - - public void setMenuName(I18nString name) { - this.menuName = name; - } - - public void setMenuName(String name) { - this.menuName = new I18nString(name); - } - - public void setTabName(I18nString name) { - this.tabName = name; - } - - public void setTabName(String name) { - this.tabName = new I18nString(name); - } - - /** - * Transforms attributes into dataSet for {@link ActionDelegate#setData} - * - * @return created dataSet from attributes - */ - public Map<String, Map<String, Object>> toDataSet() { - return toDataSet(null, null, true); - } - - /** - * Transforms attributes into dataSet for {@link ActionDelegate#setData} - * - * @param parentId id of parent menu item instance - * @param nodePath uri, that represents the menu item (f.e.: "/myItem1/myItem2") - * @return created dataSet from attributes - */ - public Map<String, Map<String, Object>> toDataSet(String parentId, String nodePath) { - return toDataSet(parentId, nodePath, false); - } - - private Map<String, Map<String, Object>> toDataSet(String parentId, String nodePath, boolean ignoreParentId) { - Map<String, Map<String, Object>> dataSet = new LinkedHashMap<>(); - - // GENERIC - ArrayList<String> filterIdCaseRefValue = new ArrayList<>(); - if (this.filter != null) { - filterIdCaseRefValue.add(this.filter.getStringId()); - } - ArrayList<String> parentIdCaseRef = new ArrayList<>(); - if (parentId != null) { - parentIdCaseRef.add(parentId); - } - - if (nodePath != null) { - putDataSetEntry(dataSet, MenuItemConstants.PREFERENCE_ITEM_FIELD_NODE_PATH, FieldType.TEXT, nodePath); - } - if (!ignoreParentId) { - putDataSetEntry(dataSet, MenuItemConstants.PREFERENCE_ITEM_FIELD_PARENT_ID, FieldType.CASE_REF, parentIdCaseRef); - } - putDataSetEntry(dataSet, MenuItemConstants.PREFERENCE_ITEM_FIELD_MENU_NAME, FieldType.I18N, this.menuName); - putDataSetEntry(dataSet, MenuItemConstants.PREFERENCE_ITEM_FIELD_MENU_ICON, FieldType.TEXT, this.menuIcon); - putDataSetEntry(dataSet, MenuItemConstants.PREFERENCE_ITEM_FIELD_TAB_NAME, FieldType.I18N, this.tabName); - putDataSetEntry(dataSet, MenuItemConstants.PREFERENCE_ITEM_FIELD_TAB_ICON, FieldType.TEXT, this.tabIcon); - if (this.identifier != null) { - putDataSetEntry(dataSet, MenuItemConstants.PREFERENCE_ITEM_FIELD_IDENTIFIER, FieldType.TEXT, this.getIdentifier()); - } - putDataSetEntry(dataSet, MenuItemConstants.PREFERENCE_ITEM_FIELD_FILTER_CASE, FieldType.CASE_REF, filterIdCaseRefValue); - putDataSetEntry(dataSet, MenuItemConstants.PREFERENCE_ITEM_FIELD_USE_TAB_ICON, FieldType.BOOLEAN, this.useTabIcon); - putDataSetEntry(dataSet, MenuItemConstants.PREFERENCE_ITEM_FIELD_USE_CUSTOM_VIEW, FieldType.BOOLEAN, - this.useCustomView); - putDataSetEntry(dataSet, MenuItemConstants.PREFERENCE_ITEM_FIELD_CUSTOM_VIEW_SELECTOR, FieldType.TEXT, - this.customViewSelector); - - // CASE - putDataSetEntry(dataSet, MenuItemConstants.PREFERENCE_ITEM_FIELD_CASE_VIEW_SEARCH_TYPE, FieldType.ENUMERATION_MAP, - this.caseViewSearchType); - putDataSetEntry(dataSet, MenuItemConstants.PREFERENCE_ITEM_FIELD_CREATE_CASE_BUTTON_TITLE, FieldType.TEXT, - this.createCaseButtonTitle); - putDataSetEntry(dataSet, MenuItemConstants.PREFERENCE_ITEM_FIELD_CREATE_CASE_BUTTON_ICON, FieldType.TEXT, - this.createCaseButtonIcon); - putDataSetEntry(dataSet, MenuItemConstants.PREFERENCE_ITEM_FIELD_REQUIRE_TITLE_IN_CREATION, FieldType.BOOLEAN, - this.caseRequireTitleInCreation); - putDataSetEntry(dataSet, MenuItemConstants.PREFERENCE_ITEM_FIELD_SHOW_CREATE_CASE_BUTTON, FieldType.BOOLEAN, - this.showCreateCaseButton); - putDataSetEntry(dataSet, MenuItemConstants.PREFERENCE_ITEM_FIELD_BANNED_NETS_IN_CREATION, FieldType.TEXT, - this.bannedNetsInCreation); - putDataSetEntry(dataSet, MenuItemConstants.PREFERENCE_ITEM_FIELD_CASE_SHOW_MORE_MENU, FieldType.BOOLEAN, - this.caseShowMoreMenu); - putDataSetEntry(dataSet, MenuItemConstants.PREFERENCE_ITEM_FIELD_CASE_ALLOW_HEADER_TABLE_MODE, FieldType.BOOLEAN, - this.caseAllowHeaderTableMode); - putDataSetEntry(dataSet, MenuItemConstants.PREFERENCE_ITEM_FIELD_CASE_HEADERS_MODE, FieldType.MULTICHOICE_MAP, - this.caseHeadersMode == null ? new ArrayList<>() : this.caseHeadersMode); - putDataSetEntry(dataSet, MenuItemConstants.PREFERENCE_ITEM_FIELD_CASE_HEADERS_DEFAULT_MODE, FieldType.ENUMERATION_MAP, - this.caseHeadersDefaultMode); - putDataSetEntry(dataSet, MenuItemConstants.PREFERENCE_ITEM_FIELD_CASE_DEFAULT_HEADERS, FieldType.TEXT, - this.caseDefaultHeaders != null ? String.join(",", this.caseDefaultHeaders) : null); - putDataSetEntry(dataSet, MenuItemConstants.PREFERENCE_ITEM_FIELD_CASE_IS_HEADER_MODE_CHANGEABLE, FieldType.BOOLEAN, - this.caseIsHeaderModeChangeable); - putDataSetEntry(dataSet, MenuItemConstants.PREFERENCE_ITEM_FIELD_USE_CASE_DEFAULT_HEADERS, FieldType.BOOLEAN, - this.caseUseDefaultHeaders); - putDataSetEntry(dataSet, MenuItemConstants.PREFERENCE_ITEM_FIELD_CASE_ALLOW_EXPORT, FieldType.BOOLEAN, - this.caseAllowExport); - - // TASK - ArrayList<String> additionalFilterIdCaseRefValue = new ArrayList<>(); - if (this.additionalFilter != null) { - additionalFilterIdCaseRefValue.add(this.additionalFilter.getStringId()); - } - - putDataSetEntry(dataSet, MenuItemConstants.PREFERENCE_ITEM_FIELD_ADDITIONAL_FILTER_CASE, FieldType.CASE_REF, - additionalFilterIdCaseRefValue); - putDataSetEntry(dataSet, MenuItemConstants.PREFERENCE_ITEM_FIELD_MERGE_FILTERS, FieldType.BOOLEAN, - this.mergeFilters); - putDataSetEntry(dataSet, MenuItemConstants.PREFERENCE_ITEM_FIELD_TASK_VIEW_SEARCH_TYPE, FieldType.ENUMERATION_MAP, - this.taskViewSearchType); - putDataSetEntry(dataSet, MenuItemConstants.PREFERENCE_ITEM_FIELD_TASK_HEADERS_MODE, FieldType.MULTICHOICE_MAP, - this.taskHeadersMode == null ? new ArrayList<>() : this.taskHeadersMode); - putDataSetEntry(dataSet, MenuItemConstants.PREFERENCE_ITEM_FIELD_TASK_HEADERS_DEFAULT_MODE, FieldType.ENUMERATION_MAP, - this.taskHeadersDefaultMode); - putDataSetEntry(dataSet, MenuItemConstants.PREFERENCE_ITEM_FIELD_TASK_IS_HEADER_MODE_CHANGEABLE, FieldType.BOOLEAN, - this.taskIsHeaderModeChangeable); - putDataSetEntry(dataSet, MenuItemConstants.PREFERENCE_ITEM_FIELD_TASK_ALLOW_HEADER_TABLE_MODE, FieldType.BOOLEAN, - this.taskAllowHeaderTableMode); - putDataSetEntry(dataSet, MenuItemConstants.PREFERENCE_ITEM_FIELD_USE_TASK_DEFAULT_HEADERS, FieldType.BOOLEAN, - this.taskUseDefaultHeaders); - putDataSetEntry(dataSet, MenuItemConstants.PREFERENCE_ITEM_FIELD_TASK_DEFAULT_HEADERS, FieldType.TEXT, - this.taskDefaultHeaders != null ? String.join(",", this.taskDefaultHeaders) : null); - putDataSetEntry(dataSet, MenuItemConstants.PREFERENCE_ITEM_FIELD_TASK_SHOW_MORE_MENU, FieldType.BOOLEAN, - this.taskShowMoreMenu); - - return dataSet; - } -} diff --git a/src/main/java/com/netgrif/application/engine/workflow/domain/menu/MenuItemConstants.java b/src/main/java/com/netgrif/application/engine/workflow/domain/menu/MenuItemConstants.java deleted file mode 100644 index b449bcc809..0000000000 --- a/src/main/java/com/netgrif/application/engine/workflow/domain/menu/MenuItemConstants.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.netgrif.application.engine.workflow.domain.menu; - -import lombok.Getter; - -/** - * Enumeration for menu items. It contains any constants needed in application. - */ -public enum MenuItemConstants { - - // FIELDS - PREFERENCE_ITEM_FIELD_NEW_FILTER_ID("new_filter_id"), - PREFERENCE_ITEM_FIELD_FILTER_CASE("filter_case"), - PREFERENCE_ITEM_FIELD_PARENT_ID("parentId"), - PREFERENCE_ITEM_FIELD_CHILD_ITEM_IDS("childItemIds"), - PREFERENCE_ITEM_FIELD_HAS_CHILDREN("hasChildren"), - PREFERENCE_ITEM_FIELD_CASE_DEFAULT_HEADERS("case_default_headers"), - PREFERENCE_ITEM_FIELD_TASK_DEFAULT_HEADERS("task_default_headers"), - PREFERENCE_ITEM_FIELD_IDENTIFIER("menu_item_identifier"), - PREFERENCE_ITEM_FIELD_APPEND_MENU_ITEM("append_menu_item_stringId"), - PREFERENCE_ITEM_FIELD_ALLOWED_ROLES("allowed_roles"), - PREFERENCE_ITEM_FIELD_BANNED_ROLES("banned_roles"), - PREFERENCE_ITEM_FIELD_MENU_NAME("menu_name"), - PREFERENCE_ITEM_FIELD_MENU_ICON("menu_icon"), - PREFERENCE_ITEM_FIELD_TAB_NAME("tab_name"), - PREFERENCE_ITEM_FIELD_USE_TAB_ICON("use_tab_icon"), - PREFERENCE_ITEM_FIELD_TAB_ICON("tab_icon"), - PREFERENCE_ITEM_FIELD_NODE_PATH("nodePath"), - PREFERENCE_ITEM_FIELD_NODE_NAME("nodeName"), - PREFERENCE_ITEM_FIELD_DUPLICATE_TITLE("duplicate_new_title"), - PREFERENCE_ITEM_FIELD_DUPLICATE_IDENTIFIER("duplicate_view_identifier"), - PREFERENCE_ITEM_FIELD_DUPLICATE_RESET_CHILD_ITEM_IDS("duplicate_reset_childItemIds"), - PREFERENCE_ITEM_FIELD_REQUIRE_TITLE_IN_CREATION("case_require_title_in_creation"), - PREFERENCE_ITEM_FIELD_USE_CUSTOM_VIEW("use_custom_view"), - PREFERENCE_ITEM_FIELD_CUSTOM_VIEW_SELECTOR("custom_view_selector"), - PREFERENCE_ITEM_FIELD_CASE_VIEW_SEARCH_TYPE("case_view_search_type"), - PREFERENCE_ITEM_FIELD_CREATE_CASE_BUTTON_TITLE("create_case_button_title"), - PREFERENCE_ITEM_FIELD_CREATE_CASE_BUTTON_ICON("create_case_button_icon"), - PREFERENCE_ITEM_FIELD_BANNED_NETS_IN_CREATION("case_banned_nets_in_creation"), - PREFERENCE_ITEM_FIELD_SHOW_CREATE_CASE_BUTTON("show_create_case_button"), - PREFERENCE_ITEM_FIELD_CASE_SHOW_MORE_MENU("case_show_more_menu"), - PREFERENCE_ITEM_FIELD_CASE_ALLOW_HEADER_TABLE_MODE("case_allow_header_table_mode"), - PREFERENCE_ITEM_FIELD_CASE_HEADERS_MODE("case_headers_mode"), - PREFERENCE_ITEM_FIELD_CASE_HEADERS_DEFAULT_MODE("case_headers_default_mode"), - PREFERENCE_ITEM_FIELD_CASE_IS_HEADER_MODE_CHANGEABLE("case_is_header_mode_changeable"), - PREFERENCE_ITEM_FIELD_USE_CASE_DEFAULT_HEADERS("case_is_header_mode_changeable"), - PREFERENCE_ITEM_FIELD_CASE_ALLOW_EXPORT("case_allow_export"), - PREFERENCE_ITEM_FIELD_ADDITIONAL_FILTER_CASE("additional_filter_case"), - PREFERENCE_ITEM_FIELD_MERGE_FILTERS("merge_filters"), - PREFERENCE_ITEM_FIELD_TASK_VIEW_SEARCH_TYPE("task_view_search_type"), - PREFERENCE_ITEM_FIELD_TASK_HEADERS_MODE("task_headers_mode"), - PREFERENCE_ITEM_FIELD_TASK_HEADERS_DEFAULT_MODE("task_headers_default_mode"), - PREFERENCE_ITEM_FIELD_TASK_IS_HEADER_MODE_CHANGEABLE("task_is_header_mode_changeable"), - PREFERENCE_ITEM_FIELD_TASK_ALLOW_HEADER_TABLE_MODE("task_allow_header_table_mode"), - PREFERENCE_ITEM_FIELD_USE_TASK_DEFAULT_HEADERS("use_task_default_headers"), - PREFERENCE_ITEM_FIELD_TASK_SHOW_MORE_MENU("task_show_more_menu"), - - // TRANSITIONS - PREFERENCE_ITEM_SETTINGS_TRANS_ID("item_settings"), - PREFERENCE_ITEM_FIELD_INIT_TRANS_ID("initialize"); - @Getter - private final String attributeId; - - MenuItemConstants(String attributeId) { - this.attributeId = attributeId; - } -} diff --git a/src/main/resources/petriNets/engine-processes/menu/tabbed_case_view_configuration.xml b/src/main/resources/petriNets/engine-processes/menu/tabbed_case_view_configuration.xml index 3e1adad7aa..d1c28db279 100644 --- a/src/main/resources/petriNets/engine-processes/menu/tabbed_case_view_configuration.xml +++ b/src/main/resources/petriNets/engine-processes/menu/tabbed_case_view_configuration.xml @@ -393,6 +393,11 @@ <title/> </data> + <data type="boolean" immediate="true"> + <id>case_allow_export</id> + <title name="case_allow_export">Allow export? + + Názov tlačidla "Nová inštancia" @@ -428,6 +433,7 @@ Vybrať zobrazenie Nastavenie Asociované zobrazenie + Povoliť export? Schaltflächentitel "Neuer Fall" @@ -463,6 +469,7 @@ Wählen Sie einen Ansichtstyp Einstellungen zugehörige Ansicht + Erlaube Export? @@ -894,6 +901,18 @@ outline + + case_allow_export + + hidden + + + 0 + 999 + 1 + 1 + + diff --git a/src/main/resources/petriNets/engine-processes/preference_item.xml b/src/main/resources/petriNets/engine-processes/preference_item.xml deleted file mode 100644 index b3a4b55676..0000000000 --- a/src/main/resources/petriNets/engine-processes/preference_item.xml +++ /dev/null @@ -1,2686 +0,0 @@ - - preference_item - PRI - Preference Item - check_box_outline_blank - true - false - false - - system - - true - true - true - - - - admin - - true - true - true - - - - default - - false - false - true - - - - - preference_item_delete - - - removeItemChildCases(useCase) - - - - - - system - System - - - admin - Admin - - - { com.netgrif.application.engine.workflow.domain.Case useCase -> - - def childCaseIds = useCase.dataSet['childItemIds'].value - if (childCaseIds == null || childCaseIds.isEmpty()) { - return - } - - removeChildItemFromParent(useCase.dataSet['parentId'].value[0], useCase) - - def childCases = workflowService.findAllById(childCaseIds) - async.run { - childCases.each { - workflowService.deleteCase(it) - } - } - } - - - { - com.netgrif.application.engine.petrinet.domain.dataset.EnumerationMapField filterAutocomplete, - com.netgrif.application.engine.petrinet.domain.dataset.TaskField previewTaskRef, - com.netgrif.application.engine.petrinet.domain.dataset.CaseField selectedFilterRef, - com.netgrif.application.engine.petrinet.domain.dataset.ButtonField updateBtn, - com.netgrif.application.engine.petrinet.domain.Transition trans, - boolean taskTypeOnly - -> - if (filterAutocomplete.getOptions().containsKey(filterAutocomplete.value)) { - change previewTaskRef value { - return [findTask({it.caseId.eq(filterAutocomplete.value).and(it.transitionId.eq("view_filter"))}).stringId] - } - make updateBtn,editable on trans when { true } - } else { - change filterAutocomplete options { - def findAllPredicate - if (taskTypeOnly) { - findAllPredicate = { filterCase -> - !selectedFilterRef.value.contains(filterCase.stringId) && - filterCase.dataSet["filter_type"].value == "Task" - } - } else { - findAllPredicate = { filterCase -> !selectedFilterRef.value.contains(filterCase.stringId) } - } - return findFilters(filterAutocomplete.value != null ? filterAutocomplete.value : "") - .findAll(findAllPredicate) - .collectEntries({filterCase -> [filterCase.stringId, filterCase.title]}) - } - change previewTaskRef value { [] } - make updateBtn,visible on trans when { true } - } - } - - - { - com.netgrif.application.engine.petrinet.domain.dataset.EnumerationMapField toBeUpdated, - com.netgrif.application.engine.petrinet.domain.dataset.MultichoiceMapField valueSelector, - com.netgrif.application.engine.petrinet.domain.dataset.EnumerationMapField optionsHolder - -> - def existingOptions = optionsHolder.options - def selectedValues = valueSelector.value - def newOptions = [:] - - if (selectedValues != null) { - existingOptions.each { key, value -> - if (selectedValues.contains(key)) { - newOptions.put(key, value) - } - } - } - - if (!newOptions.containsKey(toBeUpdated.value)) { - change toBeUpdated value { null } - } - - change toBeUpdated options { newOptions } - } - - - - parentId - - <allowedNets> - <allowedNet>preference_item</allowedNet> - </allowedNets> - </data> - <data type="text"> - <id>move_previous_dest_uri</id> - <title/> - </data> - <data type="multichoice_map"> - <id>move_dest_uri</id> - <title name="move_dest_uri">Destination URI - List of nodes representing destination URI - - autocomplete - - - moveDestUri: f.move_dest_uri; - - String uriNodeId = elasticCaseService.findUriNodeId(useCase) - def node = uriService.findById(uriNodeId) - updateMultichoiceWithCurrentNode(moveDestUri, node) - - - prevDestUri: f.move_previous_dest_uri, - moveDestUri: f.move_dest_uri; - - String newUri = moveDestUri.value.join("/") - newUri = newUri.replace("//","/") - - String corrected = getCorrectedUri(newUri) - - if (corrected == newUri) { - def node = uriService.findByUri(newUri) - change moveDestUri options { findOptionsBasedOnSelectedNode(node) } - } else { - change moveDestUri value { splitUriPath(corrected) } - } - - - - move_dest_uri_new_node - New node to be added - Enter new node name - - - move_add_node - - <placeholder name="move_add_node">Add</placeholder> - <component> - <name>raised</name> - </component> - <action trigger="set"> - newNodeName: f.move_dest_uri_new_node, - selectedUri: f.move_dest_uri; - - if (newNodeName.value == null || newNodeName.value == "") { - return - } - - String prefixUri = selectedUri.value.join("/") - prefixUri = prefixUri.replace("//","/") - - String newUri = prefixUri + uriService.getUriSeparator() + newNodeName.value - def newNode = uriService.getOrCreate(newUri, com.netgrif.application.engine.petrinet.domain.UriContentType.CASE) - - change selectedUri value { splitUriPath(newNode.uriPath) } - - change newNodeName value { null } - </action> - </data> - <data type="i18n"> - <id>duplicate_new_title</id> - <title name="duplicate_new_title">Title of duplicated view - - - duplicate_view_identifier - View identifier - Must be unique - - - childItemIds - - <allowedNets> - <allowedNet>preference_item</allowedNet> - </allowedNets> - </data> - <data type="taskRef"> - <id>childItemForms</id> - <title/> - </data> - <data type="boolean" immediate="true"> - <id>hasChildren</id> - <title/> - </data> - <data type="button"> - <id>duplicate_reset_childItemIds</id> - <title/> - <action trigger="set"> - hasChildren: f.hasChildren, - childItemIds: f.childItemIds; - - change childItemIds value { [] } - change hasChildren value { false } - </action> - </data> - <data type="text" immediate="true"> - <id>menu_item_identifier</id> - <title name="menu_item_identifier">Menu item identifier - - - nodePath - Item URI - - 0 - - - nodePath: f.nodePath, - menu_item_identifier: f.menu_item_identifier; - - change menu_item_identifier value { - def idx = nodePath.value.lastIndexOf(uriService.getUriSeparator()) - return nodePath.value.substring(idx + 1) - } - - - - - - - - menu_icon - Menu icon identifier - Material icon identifier. List of icons with identifiers is available online. - - icon: f.this, - iconPreview: f.menu_icon_preview; - - changeCaseProperty "icon" about { icon.value; } - - if (icon.value == "") { - change iconPreview value {"""]]>} - return; - } - - change iconPreview value { - """]]> + icon.value + """]]> - } - - - - menu_icon_preview - Menu icon preview - - htmltextarea - - - - menu_name_as_visible - Name of the item - Is shown in the menu - - autocomplete - - - - menu_name - Name of the item - Will be shown in the menu - - menu_name_as_visible: f.menu_name_as_visible, - name: f.menu_name; - - changeCaseProperty "title" about { name.value } - change menu_name_as_visible choices { [name.value] } - change menu_name_as_visible value { name.value } - - - - tab_icon - Tab icon identifier - Material icon identifier. List of icons with identifiers is available online. - - icon: f.this, - iconPreview: f.tab_icon_preview; - - if (icon.value == "") { - change iconPreview value {"""]]>} - return; - } - - change iconPreview value { - """]]> + icon.value + """]]> - } - - - - tab_icon_preview - Tab icon preview - - htmltextarea - - - - use_tab_icon - Display tab icon? - true - - - tab_name - Name of the item - Will be shown in tab - - - add_allowed_roles - - <placeholder name="allow_roles">Allow view for roles</placeholder> - <action trigger="set"> - allowedRoles: f.allowed_roles, - processesAvailable: f.processes_available, - rolesAvailable: f.roles_available; - - change allowedRoles options {return configurableMenuService.addSelectedRoles(allowedRoles, processesAvailable, rolesAvailable)} - - change rolesAvailable value {[]} - change rolesAvailable options {[:]} - change processesAvailable value {null} - </action> - </data> - <data type="button"> - <id>remove_allowed_roles</id> - <title/> - <placeholder name="remove_from_allowed_roles">Remove from allowed roles</placeholder> - <action trigger="set"> - allowedRoles: f.allowed_roles, - processesAvailable: f.processes_available, - rolesAvailable: f.roles_available; - - change allowedRoles options {return configurableMenuService.removeSelectedRoles(allowedRoles)} - - change allowedRoles value {[]} - change rolesAvailable value {[]} - change rolesAvailable options {[:]} - change processesAvailable value {null} - </action> - </data> - <data type="button"> - <id>add_banned_roles</id> - <title/> - <placeholder name="ban_roles">Ban view for roles</placeholder> - <action trigger="set"> - bannedRoles: f.banned_roles, - processesAvailable: f.processes_available, - rolesAvailable: f.roles_available; - - change bannedRoles options {return configurableMenuService.addSelectedRoles(bannedRoles, processesAvailable, rolesAvailable)} - - change rolesAvailable value {[]} - change rolesAvailable options {[:]} - change processesAvailable value {null} - </action> - </data> - <data type="button"> - <id>remove_banned_roles</id> - <title/> - <placeholder name="remove_from_banned_roles">Remove from banned roles</placeholder> - <action trigger="set"> - bannedRoles: f.banned_roles, - processesAvailable: f.processes_available, - rolesAvailable: f.roles_available; - - change bannedRoles options { return configurableMenuService.removeSelectedRoles(bannedRoles) } - - change bannedRoles value { [] } - change rolesAvailable value { [] } - change rolesAvailable options { [:] } - change processesAvailable value { null } - </action> - </data> - <data type="enumeration_map" immediate="true"> - <id>processes_available</id> - <title name="available_processes">Your processes - Select a process containing roles you wish to add to allowed or banned roles lists. - - processes: f.this; - - change processes options { return configurableMenuService.getNetsByAuthorAsMapOptions(loggedUser(), org.springframework.context.i18n.LocaleContextHolder.locale) } - - - processes: f.this, - allowedRoles: f.allowed_roles, - bannedRoles: f.banned_roles, - rolesAvailable: f.roles_available; - - if (processes.value != null) { - change rolesAvailable options { return configurableMenuService.getAvailableRolesFromNet(processes, allowedRoles, bannedRoles) } - } else { - change rolesAvailable options { [:] } - } - change rolesAvailable value { [] } - - - - roles_available - Available roles from selected process - - - allowed_roles - Allowed roles - List of roles allowed to view this menu entry. - - [:] - - - - banned_roles - Banned roles - List of roles not allowed to view this menu entry. - - [:] - - - - selected_filter_preview - - </data> - <data type="taskRef"> - <id>current_filter_preview</id> - <title/> - </data> - <data type="i18n"> - <id>filter_header</id> - <title/> - <init name="filter_header">Current filter</init> - <component> - <name>divider</name> - </component> - </data> - <data type="text"> - <id>new_filter_id</id> - <title/> - </data> - <data type="enumeration_map"> - <id>filter_autocomplete_selection</id> - <title name="filter_autocomplete_selection">Select new filter - - autocomplete_dynamic - - - trans: t.item_settings, - useCustomView: f.use_custom_view, - filterAutocomplete: f.this, - filter_case: f.filter_case, - update_filter: f.update_filter, - previewTaskRef: f.selected_filter_preview; - - updateFilterAutocompleteOptions(filterAutocomplete, previewTaskRef, filter_case, update_filter, trans, false) - - make update_filter,hidden on trans when { useCustomView.value } - - - trans: t.item_settings, - useCustomView: f.use_custom_view, - filterAutocomplete: f.this, - filter_case: f.filter_case, - update_filter: f.update_filter, - previewTaskRef: f.selected_filter_preview; - - updateFilterAutocompleteOptions(filterAutocomplete, previewTaskRef, filter_case, update_filter, trans, false) - - make update_filter,hidden on trans when { useCustomView.value } - - - - update_filter - - <placeholder name="update_filter">Update view with selected filter</placeholder> - <component> - <name>raised</name> - </component> - <action trigger="set"> - trans: t.item_settings, - update_filter: f.update_filter, - filter_case: f.filter_case, - filterAutocomplete: f.filter_autocomplete_selection; - - change filter_case value { [filterAutocomplete.value] } - change filterAutocomplete value { "" } - make update_filter,visible on trans when { true } - </action> - </data> - <data type="caseRef"> - <id>filter_case</id> - <title/> - <action trigger="set"> - additionalFilterCase: f.additional_filter_case, - additionalAutocomplete: f.additional_filter_autocomplete_selection, - additionalUpdate: f.update_additional_filter, - additionalFilterPreview: f.selected_additional_filter_preview, - mergeFilters: f.merge_filters, - filterHeader: f.filter_header, - currentAdditionalFilterPreview: f.current_additional_filter_preview, - taskSettingsTrans: t.task_view_settings, - settingsTrans: t.item_settings, - caseViewHeader: f.case_view_header, - caseViewSettingsTaskRef: f.case_view_settings_taskRef, - taskViewHeader: f.task_view_header, - taskViewSettingsTaskRef: f.task_view_settings_taskRef, - filterTaskRef: f.current_filter_preview, - filterCaseRef: f.filter_case; - - if (filterCaseRef.value == null || filterCaseRef.value == []) { - return - } - - def filterCase = findCase({it._id.eq(filterCaseRef.value[0])}) - change filterTaskRef value {return [findTask({it.caseId.eq(filterCase.stringId).and(it.transitionId.eq("view_filter"))}).stringId]} - - if (filterCase.dataSet["filter_type"].value == "Case") { - make caseViewHeader,editable on settingsTrans when { true } - make caseViewSettingsTaskRef,editable on settingsTrans when { true } - - make additionalAutocomplete,editable on taskSettingsTrans when { true } - make additionalUpdate,visible on taskSettingsTrans when { true } - make additionalFilterPreview,visible on taskSettingsTrans when { true } - make mergeFilters,visible on taskSettingsTrans when { true } - make filterHeader,visible on taskSettingsTrans when { true } - make currentAdditionalFilterPreview,visible on taskSettingsTrans when { true } - } else { - make caseViewHeader,hidden on settingsTrans when { true } - make caseViewSettingsTaskRef,hidden on settingsTrans when { true } - - make additionalAutocomplete,hidden on taskSettingsTrans when { true } - make additionalUpdate,hidden on taskSettingsTrans when { true } - make additionalFilterPreview,hidden on taskSettingsTrans when { true } - make mergeFilters,hidden on taskSettingsTrans when { true } - make filterHeader,hidden on taskSettingsTrans when { true } - make currentAdditionalFilterPreview,hidden on taskSettingsTrans when { true } - } - make taskViewHeader,editable on settingsTrans when { true } - make taskViewSettingsTaskRef,editable on settingsTrans when { true } - - change additionalFilterCase value { [] } - </action> - <allowedNets> - <allowedNet>filter</allowedNet> - </allowedNets> - </data> - <data type="boolean" immediate="true"> - <id>use_custom_view</id> - <title name="use_custom_view">Use custom view? - false - - - custom_view_selector - Custom view configuration selector - Example: "demo-tabbed-views" - - - - - case_view_search_type - Search type for case view - - - - - - fulltext_advanced - - - create_case_button_title - "New case" button title - - - create_case_button_icon_preview - Icon preview - add]]> - - htmltextarea - - - - create_case_button_icon - "New case" button icon identifier - add - - create_case_button_icon_preview: f.create_case_button_icon_preview, - create_case_button_icon: f.create_case_button_icon; - - - if (create_case_button_icon.value == "") { - change create_case_button_icon_preview value {"""]]>} - return; - } - - change create_case_button_icon_preview value { - """]]> + create_case_button_icon.value + """]]> - } - - - - show_create_case_button - Show create case button? - true - - - case_require_title_in_creation - Require title input in case creation? - true - - - case_banned_nets_in_creation - Banned processes for creation - Write down process identifiers separated by comma. Example: mynet1,mynet2 - - bannedNets: f.this; - - String trimmed = bannedNets.value?.replaceAll("\\s","") - if (bannedNets.value != trimmed) { - change bannedNets value { trimmed } - } - - - - case_view_header - - <init name="case_view_header">Case view</init> - <component> - <name>divider</name> - </component> - </data> - <data type="taskRef"> - <id>case_view_settings_taskRef</id> - <title/> - <init>case_view_settings</init> - </data> - <data type="boolean" immediate="true"> - <id>case_show_more_menu</id> - <title name="case_show_more_menu">Show more menu for case item? - false - - - case_allow_header_table_mode - Allow table mode for headers? - true - - - case_headers_mode - Header mode - - - - - - sort,edit,search - - headersMode: f.case_headers_mode, - defaultMode: f.case_headers_default_mode, - holder: f.case_headers_options_holder; - - updateOptionsBasedOnValue(defaultMode, headersMode, holder) - - - headersMode: f.case_headers_mode, - defaultMode: f.case_headers_default_mode, - holder: f.case_headers_options_holder; - - updateOptionsBasedOnValue(defaultMode, headersMode, holder) - - - - case_headers_default_mode - Default header mode - - - - - - sort - - - case_headers_options_holder - - <options> - <option key="sort" name="sort">Sort</option> - <option key="search" name="search">Search</option> - <option key="edit" name="edit">Edit</option> - </options> - </data> - <data type="boolean" immediate="true"> - <id>case_is_header_mode_changeable</id> - <title name="is_header_mode_changeable">Can header mode be changed? - true - - - use_case_default_headers - Use custom default headers? - true - - - case_default_headers - Set default headers - Example: "meta-title,meta-visualId" - - defaultHeaders: f.this; - - String trimmed = defaultHeaders.value?.replaceAll("\\s","") - if (defaultHeaders.value != trimmed) { - change defaultHeaders value { trimmed } - } - - - - - - selected_additional_filter_preview - - </data> - <data type="taskRef"> - <id>current_additional_filter_preview</id> - <title/> - </data> - <data type="enumeration_map"> - <id>additional_filter_autocomplete_selection</id> - <title name="filter_autocomplete_selection">Select new filter - - autocomplete_dynamic - - - trans: t.task_view_settings, - filterAutocomplete: f.this, - filterCase: f.additional_filter_case, - updateFilter: f.update_additional_filter, - previewTaskRef: f.selected_additional_filter_preview; - - updateFilterAutocompleteOptions(filterAutocomplete, previewTaskRef, filterCase, updateFilter, trans, true) - - - trans: t.task_view_settings, - filterAutocomplete: f.this, - filterCase: f.additional_filter_case, - updateFilter: f.update_additional_filter, - previewTaskRef: f.selected_additional_filter_preview; - - updateFilterAutocompleteOptions(filterAutocomplete, previewTaskRef, filterCase, updateFilter, trans, true) - - - - update_additional_filter - - <placeholder name="update_filter">Update view with selected filter</placeholder> - <component> - <name>raised</name> - </component> - <action trigger="set"> - trans: t.task_view_settings, - updateFilter: f.update_additional_filter, - filterCase: f.additional_filter_case, - filterAutocomplete: f.additional_filter_autocomplete_selection; - - change filterCase value { [filterAutocomplete.value] } - change filterAutocomplete value { "" } - make updateFilter,visible on trans when { true } - </action> - </data> - <data type="button"> - <id>remove_additional_filter</id> - <title/> - <placeholder name="remove_additional_filter">Remove additional filter</placeholder> - <component> - <name>raised</name> - </component> - <action trigger="set"> - filterCase: f.additional_filter_case; - - change filterCase value { [] } - </action> - </data> - <data type="caseRef"> - <id>additional_filter_case</id> - <title/> - <action trigger="set"> - taskViewTrans: t.task_view_settings, - mergeFilters: f.merge_filters, - filterHeader: f.filter_header, - filterTaskRef: f.current_additional_filter_preview, - removeButton: f.remove_additional_filter, - filterCaseRef: f.additional_filter_case; - - if (filterCaseRef.value[0] == null) { - make mergeFilters,hidden on taskViewTrans when { true } - make filterHeader,hidden on taskViewTrans when { true } - make removeButton,hidden on taskViewTrans when { true } - change filterTaskRef value { [] } - return - } - - def filterCase = findCase({ it._id.eq(filterCaseRef.value[0]) }) - change filterTaskRef value { return [findTask({it.caseId.eq(filterCase.stringId).and(it.transitionId.eq("view_filter"))}).stringId] } - make mergeFilters,editable on taskViewTrans when { true } - make filterHeader,visible on taskViewTrans when { true } - make removeButton,editable on taskViewTrans when { true } - </action> - <allowedNets> - <allowedNet>filter</allowedNet> - </allowedNets> - </data> - <data type="boolean" immediate="true"> - <id>merge_filters</id> - <title name="merge_filters">Merge with base filter? - true - - - task_view_settings_taskRef - - <init>task_view_settings</init> - </data> - <data type="i18n"> - <id>task_view_header</id> - <title/> - <init name="task_view_header">Task view</init> - <component> - <name>divider</name> - </component> - </data> - <data type="enumeration_map" immediate="true"> - <id>task_view_search_type</id> - <title name="task_view_search_type">Search type for task view - - - - - - fulltext_advanced - - - task_headers_mode - Header mode - - - - - sort,edit - - headersMode: f.task_headers_mode, - defaultMode: f.task_headers_default_mode, - holder: f.task_headers_options_holder; - - updateOptionsBasedOnValue(defaultMode, headersMode, holder) - - - headersMode: f.case_headers_mode, - defaultMode: f.case_headers_default_mode, - holder: f.case_headers_options_holder; - - updateOptionsBasedOnValue(defaultMode, headersMode, holder) - - - - task_headers_default_mode - Default header mode - - - - - sort - - - task_headers_options_holder - - <options> - <option key="sort" name="sort">Sort</option> - <option key="edit" name="edit">Edit</option> - </options> - </data> - <data type="boolean" immediate="true"> - <id>task_is_header_mode_changeable</id> - <title name="is_header_mode_changeable">Can header mode be changed? - true - - - task_allow_header_table_mode - Allow table mode for headers? - true - - - use_task_default_headers - Use custom default headers? - true - - - task_default_headers - Set default headers - Example: "meta-title,meta-user" - - defaultHeaders: f.this; - - String trimmed = defaultHeaders.value?.replaceAll("\\s","") - if (defaultHeaders.value != trimmed) { - change defaultHeaders value { trimmed } - } - - - - task_show_more_menu - Show more menu for task item? - true - - - order_down - - <placeholder>south</placeholder> - <component> - <name>icon</name> - <property key="stretch">true</property> - </component> - <action trigger="set"> - parentId: f.parentId; - - def parentCase = workflowService.findOne(parentId.value[0]) - def taskId = useCase.tasks.find { it.transition == "row_for_ordering" }.task - def taskRefValue = parentCase.dataSet['childItemForms'].value - int taskRefValueSize = taskRefValue.size() - def caseRefValue = parentCase.dataSet['childItemIds'].value - int caseRefValueSize = caseRefValue.size() - - int idxInTaskRef = taskRefValue.indexOf(taskId) - if (idxInTaskRef < taskRefValueSize - 1) { - Collections.swap(taskRefValue, idxInTaskRef, idxInTaskRef + 1) - } - - int idxInCaseRef = caseRefValue.indexOf(useCase.stringId) - if (idxInCaseRef < caseRefValueSize - 1) { - Collections.swap(caseRefValue, idxInCaseRef, idxInCaseRef + 1) - } - - setData("children_order", parentCase, [ - "childItemForms" : [ - "value" : taskRefValue, - "type" : "taskRef" - ], - "childItemIds" : [ - "value" : caseRefValue, - "type" : "caseRef" - ] - ]) - </action> - </data> - <data type="button"> - <id>order_up</id> - <title/> - <placeholder>north</placeholder> - <component> - <name>icon</name> - <property key="stretch">true</property> - </component> - <action trigger="set"> - parentId: f.parentId; - - def parentCase = workflowService.findOne(parentId.value[0]) - def taskId = useCase.tasks.find { it.transition == "row_for_ordering" }.task - def taskRefValue = parentCase.dataSet['childItemForms'].value - def caseRefValue = parentCase.dataSet['childItemIds'].value - - int idxInTaskRef = taskRefValue.indexOf(taskId) - if (idxInTaskRef > 0) { - Collections.swap(taskRefValue, idxInTaskRef - 1, idxInTaskRef) - } else { - return - } - - int idxInCaseRef = caseRefValue.indexOf(useCase.stringId) - if (idxInCaseRef > 0) { - Collections.swap(caseRefValue, idxInCaseRef - 1, idxInCaseRef) - } else { - return - } - - setData("children_order", parentCase, [ - "childItemForms" : [ - "value" : taskRefValue, - "type" : "taskRef" - ], - "childItemIds" : [ - "value" : caseRefValue, - "type" : "caseRef" - ] - ]) - </action> - </data> - <data type="boolean" immediate="true"> - <id>case_allow_export</id> - <title name="case_allow_export">Allow export? - - - - - Náhľad ikony - Identifikátor ikony - Identifikátor Material ikony. Zoznam ikon s identifikátormi je dostupný online. - Pridaj k povoleným roliam - Odstráň z povolených rolí - Pridaj k zakázaným roliam - Odstráň zo zakázaných rolí - Vaše procesy - Vyberte proces obsahujúci roly ktoré chcete pridať do zoznamu povolených alebo zakázaných rolí. - Dostupné roly - Názov tlačidla "Nová inštancia" - Identifikátor ikony tlačidla "Nová inštancia" - Náhľad ikony - Predvolené hlavičky - Napríklad: "meta-title,meta-visualId" - Zvoľte nový filter - Cieľové URI - Názov duplikovanej položky - Identifikátor duplikovanej položky - Musí byť jedinečný - Názov položky - Bude zobrazený v menu - Identifikátor ikony v karte - Identifikátor Material ikony. Zoznam ikon s identifikátormi je dostupný online. - Zobraziť ikonu v karte? - Názov položky - Bude zobrazený v karte - Aktualizovať zobrazenie s vybraným filtrom - Použiť vlastné zobrazenie? - Konfiguračný identifikátor vlastného zobrazenia - Napríklad: "demo-tabbed-views" - Typ vyhľadávania prípadov - Skryté - Fulltext - Fulltext a rozšírené - Vyžadovať názov inštancii pri vytváraní? - Zakázané siete pri vytváraní - Uveďte identifikátory procesov oddelené čiarkou. Napríklad: mynet1,mynet2 - Zobraziť tlačidlo na vytvorenie prípadu? - Zobrazovať menu pre prípadovú položku? - Zoraďovanie - Vyhľadávanie - Upravovanie - Zjednotiť filter so základným filtrom? - Typ vyhľadávania úloh - Mód hlavičiek - Predvolený mód hlavičiek - Môže byť mód hlavičiek zmenený? - Povoliť tabuľkový mód pre hlavičky? - Použiť vlastné predvolené hlavičky? - Predvolené hlavičky - Napríklad: "meta-title,meta-user" - Zobrazovať menu pre úlohovú položku? - Nastavenie položky - Roly - Filter - Presunúť položku - Presunúť - Duplikovať položku - Duplikovať - Dodatočný filter - Súčasný filter - Zobrazenie prípadov - Zobrazenie úloh - Povolené roly - Zoznam povolených rolí, ktoré môžu zobraziť túto položku - Zakázané roly - Zoznam zakázaných rolí, ktoré nemôžu zobraziť túto položku - Všeobecné - Identifikátor položky - Odstrániť dodatočný filter - URI položky - Nový uzol - Uveďte názov uzlu, ktorý chcete pridať - Zoznam uzlov reprezentujúce cieľovú URI - Pridať - Povoliť export? - - - Ikonevorschau - Ikone ID - Material Ikone ID. Liste den Ikonen mit IDs ist online verfügbar. - Zu zulässigen Rollen hinzufügen - Aus zulässigen Rollen entfernen - Zu verbotenen Rollen hinzufügen - Aus verbotenen Rollen entfernen - Ihre Prozesse - Wählen Sie einen Prozess mit Rollen aus, die Sie zu Listen mit zulässigen oder verbotenen Rollen hinzufügen möchten. - Verfügbare Rollen - Schaltflächentitel "Neuer Fall" - Ikone ID - Ikonevorschau - Anzuzeigende Attributmenge auswählen - Neue Filter auswählen - Beispiel: "meta-title,meta-visualId" - Material Ikone ID. Liste den Ikonen mit IDs ist online verfügbar. - Beispiel: "demo-tabbed-views" - Versteckt - Einfacher Suchmodus - Sortieren - Suchen - Bearbeiten - Kopfzeilenmodus - Standardkopfzeilenmodus - Erlaube Änderung des Kopfzeilenmodus? - Erlaube Tabellenmodus? - Eigene Kopfzeilen verwenden? - Anzuzeigende Attributmenge auswählen - Beispiel: "meta-title,meta-user" - Rollen - Filter - Zusätzlicher Filter - Aktueller Filter - Zulässige Rollen - Allgemein - Identifikationsnummer des Menüeintrages - Zusatzfilter entfernen - Menüeintrag-URI - Neuer Knoten - Hinzufügen - Ziel URI - Titel der kopierten Ansicht - Identifikator der kopierten Ansicht - Muss einzigartig sein - Titel des Eintrages - Wird im Menü angezeigt - Ikonen Identifikator der Registerkarte - Zeige die Registerkarte Ikone an? - Titel der Registerkarte - Wird in der Registerkarte angezeigt - Aktualisiere die Ansicht mit dem ausgewählten Filter - Eigener Ansicht anwenden? - Konfigurationsidentifikator der eigenen Ansicht - Suchmodus im Fallansicht - Einfacher und erweiterter Suchmodus - Erforde den Titel beim erzeugen von Fällen? - Ausgeschlossene Prozesse - Trenne die Prozessidentifikatoren mit einer Komma. z.B.: netz1,netz2 - Schaltfläche „Fall erstellen“ anzeigen? - "Erweiterte Optionen" Taste bei einzelnen Fällen anzeigen - Mit dem Basisfilter kombinieren? - Suchmodus im Aufgabenansicht - "Erweiterte Optionen" Taste bei einzelnen Aufgaben anzeigen - Menüeintrageinstellungen - Menüeintrag verschieben - verschieben - Menüeintrag duplizieren - duplizieren - Fallansicht - Aufgabenansicht - Rollen mit Zugriff auf diesen Menüeintrag - Verbotene Rollen - Rollen, für die wird den Menüeintrag ausgeblendet - Nächste URI-Teil angeben - Teile der Ziel URI - Erlaube Export? - - - - - initialize - 340 - 220 - - hourglass_empty - - admin - - true - true - true - true - - - - view - - filter_case - - forbidden - - filterCaseRef: f.filter_case, - menu_name: f.menu_name; - - if (filterCaseRef.value == null || filterCaseRef.value == []) { - return - } - - def filterCase = findCase({it._id.eq(filterCaseRef.value[0])}) - if (!menu_name.value) { - change menu_name value {return filterCase.dataSet["i18n_filter_name"].value} - } - - - - - - - - item_settings - 460 - 100 - - settings - auto - - admin - - true - true - true - true - - - - pre_general - 4 - grid - - menu_item_identifier - - visible - - - 0 - 0 - 1 - 2 - - outline - - - - nodePath - - visible - - - 2 - 0 - 1 - 2 - - outline - - - - - general_0 - 4 - grid - General - - menu_name - - editable - - - 0 - 0 - 1 - 2 - - outline - - - - menu_icon - - editable - - - 2 - 0 - 1 - 1 - - outline - - - - menu_icon_preview - - visible - - - 3 - 0 - 1 - 1 - - standard - - - - tab_name - - editable - - - 0 - 1 - 1 - 1 - - outline - - - - use_tab_icon - - editable - - - 1 - 1 - 1 - 1 - 0 - - - - 0 - - - trans: t.this, - iconPreview: f.tab_icon_preview, - icon: f.tab_icon, - useIcon: f.use_tab_icon; - - make iconPreview,visible on trans when { useIcon.value } - make icon,editable on trans when { useIcon.value } - - make iconPreview,hidden on trans when { !useIcon.value } - make icon,hidden on trans when { !useIcon.value } - - - - - - tab_icon - - editable - - - 2 - 1 - 1 - 1 - - outline - - - - tab_icon_preview - - visible - - - 3 - 1 - 1 - 1 - - standard - - - - use_custom_view - - editable - - - 0 - 2 - 1 - 1 - - outline - - - 0 - - - trans: t.this, - caseHeader: f.case_view_header, - caseTaskRef: f.case_view_settings_taskRef, - taskHeader: f.task_view_header, - taskTaskRef: f.task_view_settings_taskRef, - - useTabIcon: f.use_tab_icon, - tabIconPreview: f.tab_icon_preview, - tabName: f.tab_name, - tabIcon: f.tab_icon, - filterSelection: f.filter_autocomplete_selection, - updateFilter: f.update_filter, - selectedFilterPreview: f.selected_filter_preview, - currentFilterHeader: f.filter_header, - currentFilterPreview: f.current_filter_preview, - - use: f.use_custom_view, - selector: f.custom_view_selector; - - make selector,editable on trans when { use.value } - make selector,visible on trans when { !use.value } - - make caseHeader,visible on trans when { !use.value } - make caseHeader,hidden on trans when { use.value } - make caseTaskRef,editable on trans when { !use.value } - make caseTaskRef,hidden on trans when { use.value } - - make taskHeader,visible on trans when { !use.value } - make taskHeader,hidden on trans when { use.value } - make taskTaskRef,editable on trans when { !use.value } - make taskTaskRef,hidden on trans when { use.value } - - make useTabIcon,editable on trans when { !use.value } - make useTabIcon,hidden on trans when { use.value } - make tabIconPreview,visible on trans when { !use.value } - make tabIconPreview,hidden on trans when { use.value } - make tabName,editable on trans when { !use.value } - make tabName,hidden on trans when { use.value } - make tabIcon,editable on trans when { !use.value } - make tabIcon,hidden on trans when { use.value } - make filterSelection,editable on trans when { !use.value } - make filterSelection,hidden on trans when { use.value } - make updateFilter,visible on trans when { !use.value } - make updateFilter,hidden on trans when { use.value } - make selectedFilterPreview,visible on trans when { !use.value } - make selectedFilterPreview,hidden on trans when { use.value } - make currentFilterHeader,visible on trans when { !use.value } - make currentFilterHeader,hidden on trans when { use.value } - make currentFilterPreview,visible on trans when { !use.value } - make currentFilterPreview,hidden on trans when { use.value } - - - - - - custom_view_selector - - visible - - - 1 - 2 - 1 - 3 - - outline - - - - - roles_management - 5 - grid - Roles - - processes_available - - editable - - - 0 - 0 - 2 - 1 - 0 - - outline - - - - roles_available - - editable - - - 1 - 0 - 2 - 1 - 0 - - outline - - - - add_allowed_roles - - editable - - - 2 - 0 - 1 - 1 - 0 - - - - - allowed_roles - - editable - - - 3 - 0 - 1 - 1 - 0 - - outline - - - - remove_allowed_roles - - editable - - - 4 - 0 - 1 - 1 - 0 - - - - - add_banned_roles - - editable - - - 2 - 1 - 1 - 1 - 0 - - - - - banned_roles - - editable - - - 3 - 1 - 1 - 1 - 0 - - outline - - - - remove_banned_roles - - editable - - - 4 - 1 - 1 - 1 - 0 - - - - - - filter_update - 4 - grid - Filter - - filter_autocomplete_selection - - editable - - - 0 - 0 - 1 - 3 - - outline - - - - update_filter - - visible - - - 3 - 0 - 1 - 1 - - standard - - - - selected_filter_preview - - visible - - - 0 - 1 - 1 - 4 - - standard - - - - - current_filter - 4 - grid - - filter_header - - visible - - - 0 - 0 - 1 - 4 - - outline - - - - current_filter_preview - - visible - - - 0 - 1 - 1 - 4 - - standard - - - - - case_view_settings_dataGroup - 4 - grid - - case_view_header - - hidden - - - 0 - 0 - 1 - 4 - - outline - - - - case_view_settings_taskRef - - hidden - - - 0 - 1 - 1 - 4 - - outline - - - - - task_view_settings_dataGroup - 4 - grid - - task_view_header - - hidden - - - 0 - 0 - 1 - 4 - - outline - - - - task_view_settings_taskRef - - hidden - - - 0 - 1 - 1 - 4 - - outline - - - - - - - move_item - 580 - 100 - - move_down - auto - - admin - - true - true - true - true - - - - move - 4 - grid - - move_dest_uri - - editable - required - - - 0 - 0 - 1 - 2 - - outline - - - - move_dest_uri_new_node - - editable - - - 2 - 0 - 1 - 1 - - outline - - - - move_add_node - - editable - - - 3 - 0 - 1 - 1 - - outline - - - - - finish - - - dest: f.move_dest_uri; - - if (dest.value == null || dest.value == []) { - throw new IllegalArgumentException("URI must not be empty!") - } - - String newUri = dest.value.join("/") - newUri = newUri.replace("//","/") - - changeMenuItem useCase uri { newUri } - - - Move - - - - - duplicate_item - 580 - 340 - - content_copy - auto - - admin - - true - true - true - true - - - - duplicate - 4 - grid - - duplicate_new_title - - editable - required - - - 0 - 0 - 1 - 4 - - outline - - - - duplicate_view_identifier - - editable - required - - - 0 - 1 - 1 - 4 - - outline - - - - - finish - - - identifier: f.duplicate_view_identifier, - title: f.duplicate_new_title; - - duplicateMenuItem(useCase, title.value, identifier.value) - - - Duplicate - - - - - change_filter - 460 - 340 - - - system - - true - - - - new_filter_id - - editable - required - - - set_event_0 - - - new_filter_id: f.new_filter_id, - filterTaskRef: f.current_filter_preview, - filterCaseRef: f.filter_case; - - change filterCaseRef value { [new_filter_id.value] } - def filterCase = findCase({it._id.eq(filterCaseRef.value[0])}) - change filterTaskRef value {return [findTask({it.caseId.eq(filterCase.stringId).and(it.transitionId.eq("view_filter"))}).stringId]} - - - - - - - - case_view_settings - 340 - 100 - - - system - - true - - - - case_view_dataGroup - 4 - grid - - case_view_search_type - - editable - required - - - 0 - 0 - 1 - 1 - - outline - - - - case_allow_export - - editable - required - - - 1 - 0 - 1 - 1 - - outline - - - - show_create_case_button - - editable - required - - - 2 - 0 - 1 - 1 - - outline - - - - case_show_more_menu - - editable - required - - - 3 - 0 - 1 - 1 - - outline - - - - create_case_button_title - - editable - - - 0 - 1 - 1 - 1 - 0 - - outline - - - - create_case_button_icon - - editable - - - 1 - 1 - 1 - 1 - 0 - - outline - - - - create_case_button_icon_preview - - visible - - - 2 - 1 - 1 - 1 - 0 - - standard - - - - case_require_title_in_creation - - editable - - - 3 - 1 - 1 - 1 - 0 - - standard - - - - case_banned_nets_in_creation - - editable - - - 0 - 2 - 1 - 4 - 0 - - outline - - - - - case_view_headers - 5 - grid - - case_is_header_mode_changeable - - editable - required - - trans: t.this, - isChangeable: f.case_is_header_mode_changeable, - mode: f.case_headers_mode, - defaultMode: f.case_headers_default_mode; - - make mode,editable on trans when { isChangeable.value } - make mode,required on trans when { isChangeable.value } - make defaultMode,editable on trans when { isChangeable.value } - make defaultMode,required on trans when { isChangeable.value } - - make mode,hidden on trans when { !isChangeable.value } - make mode,optional on trans when { !isChangeable.value } - make defaultMode,hidden on trans when { !isChangeable.value } - make defaultMode,optional on trans when { !isChangeable.value } - - - - 0 - 0 - 1 - 1 - 0 - - outline - - - - case_allow_header_table_mode - - editable - required - - - 1 - 0 - 1 - 1 - 0 - - outline - - - - case_headers_mode - - editable - required - - - 2 - 0 - 1 - 2 - 0 - - outline - - - - case_headers_default_mode - - editable - required - - - 4 - 0 - 1 - 1 - 0 - - outline - - - - use_case_default_headers - - editable - - trans: t.this, - use: f.use_case_default_headers, - headers: f.case_default_headers; - - make headers,editable on trans when { use.value } - make headers,visible on trans when { !use.value } - - - - 0 - 1 - 1 - 1 - 0 - - outline - - - - case_default_headers - - editable - - - 1 - 1 - 1 - 4 - 0 - - outline - - - - - - - task_view_settings - 340 - 340 - - - system - - true - - - - task_view_dataGroup - 4 - grid - - task_view_search_type - - editable - required - - - 0 - 0 - 1 - 3 - - outline - - - - task_show_more_menu - - editable - required - - - 3 - 0 - 1 - 1 - - outline - - - - - task_view_headers - 5 - grid - - task_is_header_mode_changeable - - editable - required - - trans: t.this, - isChangeable: f.task_is_header_mode_changeable, - mode: f.task_headers_mode, - defaultMode: f.task_headers_default_mode; - - make mode,editable on trans when { isChangeable.value } - make mode,required on trans when { isChangeable.value } - make defaultMode,editable on trans when { isChangeable.value } - make defaultMode,required on trans when { isChangeable.value } - - make mode,hidden on trans when { !isChangeable.value } - make mode,optional on trans when { !isChangeable.value } - make defaultMode,hidden on trans when { !isChangeable.value } - make defaultMode,optional on trans when { !isChangeable.value } - - - - 0 - 0 - 1 - 1 - 0 - - outline - - - - task_allow_header_table_mode - - editable - required - - - 1 - 0 - 1 - 1 - - outline - - - - task_headers_mode - - editable - required - - - 2 - 0 - 1 - 2 - - outline - - - - task_headers_default_mode - - editable - required - - - 4 - 0 - 1 - 1 - - outline - - - - use_task_default_headers - - editable - - trans: t.this, - use: f.use_task_default_headers, - headers: f.task_default_headers; - - make headers,editable on trans when { use.value } - make headers,visible on trans when { !use.value } - - - - 0 - 1 - 1 - 1 - 0 - - outline - - - - task_default_headers - - editable - - - 1 - 1 - 1 - 4 - 0 - - outline - - - - - additional_filter_update - 4 - grid - Additional filter - - additional_filter_autocomplete_selection - - editable - - - 0 - 0 - 1 - 3 - - outline - - - - update_additional_filter - - editable - - - 3 - 0 - 1 - 1 - - standard - - - - selected_additional_filter_preview - - visible - - - 0 - 1 - 1 - 4 - - standard - - - - - current_additional_filter - 4 - grid - - filter_header - - hidden - - - 0 - 0 - 1 - 4 - - outline - - - - current_additional_filter_preview - - visible - - - 0 - 1 - 1 - 4 - - standard - - - - merge_filters - - hidden - - - 0 - 2 - 1 - 1 - - standard - - - - remove_additional_filter - - hidden - - - 1 - 2 - 1 - 1 - - standard - - - - - - children_order - 580 - 220 - - low_priority - auto - - admin - - true - true - true - true - - - - children_order_0 - 4 - grid - - childItemForms - - editable - - forms: f.childItemForms, - ids: f.childItemIds; - - def orderedTaskIds = ids.value?.collect { id -> workflowService.findOne(id).tasks.find { it.transition == "row_for_ordering" }.task } - change forms value { orderedTaskIds } - - - - 0 - 0 - 1 - 4 - - outline - - - - - - row_for_ordering - 741 - 219 - - - system - - true - true - true - true - - - - row_for_ordering_0 - 6 - grid - - menu_item_identifier - - visible - - - 0 - 0 - 1 - 2 - - outline - - - - menu_name_as_visible - - visible - - - 2 - 0 - 1 - 2 - - outline - - - - order_down - - editable - - - 4 - 0 - 1 - 1 - 1 - - outline - - - - order_up - - editable - - - 5 - 0 - 1 - 1 - 1 - - outline - - - - - finish - - - - delegate - - - - - - - uninitialized - 220 - 220 - - 1 - false - - - initialized - 460 - 220 - - 0 - false - - - - - a1 - regular - uninitialized - initialize - 1 - - - a7 - read - initialized - item_settings - 1 - - - a8 - regular - initialize - initialized - 1 - - - a9 - read - initialized - move_item - 1 - - - a10 - read - initialized - duplicate_item - 1 - - - a12 - read - initialized - change_filter - 1 - - - a13 - read - initialized - children_order - 1 - - diff --git a/src/test/groovy/com/netgrif/application/engine/export/service/XlsExportServiceTest.java b/src/test/groovy/com/netgrif/application/engine/export/service/XlsExportServiceTest.java index 392d9b1f06..4abac8a482 100644 --- a/src/test/groovy/com/netgrif/application/engine/export/service/XlsExportServiceTest.java +++ b/src/test/groovy/com/netgrif/application/engine/export/service/XlsExportServiceTest.java @@ -42,7 +42,7 @@ public class XlsExportServiceTest { void shouldCreateXlsxFile() throws Exception { LoggedUser superUser = superCreator.getSuperUser().transformToLoggedUser(); - IntStream.range(0,5).forEach(idx -> workflowService.createCaseByIdentifier(FilterRunner.PREFERRED_ITEM_NET_IDENTIFIER, "Test case", "", superUser)); + IntStream.range(0,5).forEach(idx -> workflowService.createCaseByIdentifier(FilterRunner.MENU_NET_IDENTIFIER, "Test case", "", superUser)); FilteredCasesRequest request = getTestRequest(); File excel = xlsExportService.getExportFilteredCasesFile(request, superUser, Locale.ENGLISH); @@ -59,7 +59,7 @@ FilteredCasesRequest getTestRequest() { FilteredCasesRequest request = new FilteredCasesRequest(); request.setQuery(List.of( CaseSearchRequest.builder() - .query("processIdentifier:" + FilterRunner.PREFERRED_ITEM_NET_IDENTIFIER) + .query("processIdentifier:" + FilterRunner.MENU_NET_IDENTIFIER) .build())); request.setSelectedDataFieldNames(List.of("Menu Item Identifier", "Item URI", "Menu icon identifier", "Name of the item", "Tab icon identifier", "Name of the item")); request.setSelectedDataFieldIds(List.of("menu_item_identifier", "nodePath", "menu_icon", "menu_name", "tab_icon", "tab_name"));