From f8a2451f12a870017d7b624103f3020695b0bc0d Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Wed, 15 Jul 2026 15:16:21 +0200 Subject: [PATCH 01/21] Update parent POM to version `7.0.0-RC10.6` and configure migration helpers to use a dedicated MongoTemplate bean (`migrationMongoTemplate`) with fallback logic in multi-database setups. --- application-engine/pom.xml | 2 +- .../helpers/CaseMigrationHelper.groovy | 3 +- .../helpers/PetriNetMigrationHelper.groovy | 3 +- .../helpers/TaskMigrationHelper.groovy | 3 +- .../MigrationMongoTemplateConfiguration.java | 47 +++++++++++++++++++ .../properties/MigrationProperties.java | 13 ++++- nae-object-library/pom.xml | 2 +- nae-spring-core-adapter/pom.xml | 2 +- nae-user-ce/pom.xml | 2 +- nae-user-common/pom.xml | 2 +- pom.xml | 2 +- 11 files changed, 71 insertions(+), 10 deletions(-) create mode 100644 application-engine/src/main/java/com/netgrif/application/engine/configuration/MigrationMongoTemplateConfiguration.java diff --git a/application-engine/pom.xml b/application-engine/pom.xml index 05d931347a..1173880a61 100644 --- a/application-engine/pom.xml +++ b/application-engine/pom.xml @@ -6,7 +6,7 @@ com.netgrif application-engine-parent - 7.0.0-RC10.5 + 7.0.0-RC10.6 application-engine diff --git a/application-engine/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy b/application-engine/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy index f64df517c1..1282a60143 100644 --- a/application-engine/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy +++ b/application-engine/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy @@ -16,6 +16,7 @@ import com.netgrif.application.engine.petrinet.service.interfaces.IPetriNetServi import com.querydsl.core.types.Predicate import groovy.util.logging.Slf4j import org.bson.types.ObjectId +import org.springframework.beans.factory.annotation.Qualifier import org.springframework.data.mongodb.core.BulkOperations import org.springframework.data.mongodb.core.FindAndReplaceOptions import org.springframework.data.mongodb.core.MongoTemplate @@ -65,7 +66,7 @@ class CaseMigrationHelper extends AbstractMigrationHelper { * @param mongoTemplate MongoTemplate to interact with MongoDB. * @param migrationConfigurationProperties Properties for migration configuration, including cases. */ - CaseMigrationHelper(MongoTemplate mongoTemplate, + CaseMigrationHelper(@Qualifier("migrationMongoTemplate") MongoTemplate mongoTemplate, MigrationProperties migrationProperties, IPetriNetService petriNetService, IElasticCaseService elasticCaseService, diff --git a/application-engine/src/main/groovy/com/netgrif/application/engine/migration/helpers/PetriNetMigrationHelper.groovy b/application-engine/src/main/groovy/com/netgrif/application/engine/migration/helpers/PetriNetMigrationHelper.groovy index 81f96a7b8e..a03fc955b0 100644 --- a/application-engine/src/main/groovy/com/netgrif/application/engine/migration/helpers/PetriNetMigrationHelper.groovy +++ b/application-engine/src/main/groovy/com/netgrif/application/engine/migration/helpers/PetriNetMigrationHelper.groovy @@ -20,6 +20,7 @@ import com.netgrif.application.engine.petrinet.service.interfaces.IPetriNetServi import groovy.util.logging.Slf4j import org.apache.tomcat.util.http.fileupload.IOUtils import org.springframework.beans.factory.ObjectFactory +import org.springframework.beans.factory.annotation.Qualifier import org.springframework.core.io.ClassPathResource import org.springframework.core.io.Resource import org.springframework.data.domain.Pageable @@ -83,7 +84,7 @@ class PetriNetMigrationHelper extends AbstractMigrationHelper { * @param importerProvider the {@link ObjectFactory} that supplies {@link Importer} instances for importing Petri Net models from various sources * @param userService the {@link UserService} for managing user-related operations, including retrieving system user for Petri Net imports */ - PetriNetMigrationHelper(MongoTemplate mongoTemplate, + PetriNetMigrationHelper(@Qualifier("migrationMongoTemplate") MongoTemplate mongoTemplate, MigrationProperties migrationProperties, IPetriNetService petriNetService, ProcessRoleRepository processRoleRepository, diff --git a/application-engine/src/main/groovy/com/netgrif/application/engine/migration/helpers/TaskMigrationHelper.groovy b/application-engine/src/main/groovy/com/netgrif/application/engine/migration/helpers/TaskMigrationHelper.groovy index dbb369f424..ef116d5845 100644 --- a/application-engine/src/main/groovy/com/netgrif/application/engine/migration/helpers/TaskMigrationHelper.groovy +++ b/application-engine/src/main/groovy/com/netgrif/application/engine/migration/helpers/TaskMigrationHelper.groovy @@ -15,6 +15,7 @@ import com.netgrif.application.engine.petrinet.service.interfaces.IPetriNetServi import com.netgrif.application.engine.workflow.service.interfaces.ITaskService import com.querydsl.core.types.Predicate import groovy.util.logging.Slf4j +import org.springframework.beans.factory.annotation.Qualifier import org.springframework.data.mongodb.core.BulkOperations import org.springframework.data.mongodb.core.MongoTemplate import org.springframework.data.mongodb.core.query.Criteria @@ -70,7 +71,7 @@ class TaskMigrationHelper extends AbstractMigrationHelper { * * @param mongoTemplate the {@link MongoTemplate} to use for interacting with MongoDB */ - TaskMigrationHelper(MongoTemplate mongoTemplate, + TaskMigrationHelper(@Qualifier("migrationMongoTemplate") MongoTemplate mongoTemplate, MigrationProperties migrationProperties, IPetriNetService petriNetService, ITaskService taskService, diff --git a/application-engine/src/main/java/com/netgrif/application/engine/configuration/MigrationMongoTemplateConfiguration.java b/application-engine/src/main/java/com/netgrif/application/engine/configuration/MigrationMongoTemplateConfiguration.java new file mode 100644 index 0000000000..9b0b8fe6d4 --- /dev/null +++ b/application-engine/src/main/java/com/netgrif/application/engine/configuration/MigrationMongoTemplateConfiguration.java @@ -0,0 +1,47 @@ +package com.netgrif.application.engine.configuration; + +import com.netgrif.application.engine.configuration.properties.MigrationProperties; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.util.StringUtils; + +@Slf4j +@Configuration +@RequiredArgsConstructor +public class MigrationMongoTemplateConfiguration { + + private static final String DEFAULT_TEMPLATE = "mongoTemplate"; + private final BeanFactory beanFactory; + private final MigrationProperties migrationProperties; + + @Bean(name = "migrationMongoTemplate") + public MongoTemplate getMongoTemplate() { + String requested = migrationProperties.getMongoTemplateBeanName(); + + if (!StringUtils.hasText(requested)) { + requested = DEFAULT_TEMPLATE; + } else { + requested = requested.trim(); + } + + if (!beanFactory.containsBean(requested)) { + log.warn("Migration MongoTemplate bean [{}] not found, falling back to [{}]", requested, DEFAULT_TEMPLATE); + return beanFactory.getBean(DEFAULT_TEMPLATE, MongoTemplate.class); + } + + try { + MongoTemplate template = beanFactory.getBean(requested, MongoTemplate.class); + log.info("Using MongoTemplate bean [{}] for migration helpers", requested); + return template; + } catch (BeansException e) { + log.warn("Migration MongoTemplate bean [{}] is not usable, falling back to [{}]", + requested, DEFAULT_TEMPLATE, e); + return beanFactory.getBean(DEFAULT_TEMPLATE, MongoTemplate.class); + } + } +} \ No newline at end of file diff --git a/application-engine/src/main/java/com/netgrif/application/engine/configuration/properties/MigrationProperties.java b/application-engine/src/main/java/com/netgrif/application/engine/configuration/properties/MigrationProperties.java index 7716fec460..424e07f103 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/configuration/properties/MigrationProperties.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/configuration/properties/MigrationProperties.java @@ -18,9 +18,20 @@ */ @Data @Configuration -@ConfigurationProperties(prefix = "nae.migration") +@ConfigurationProperties(prefix = "netgrif.engine.migration") public class MigrationProperties { + /** + * The name of the MongoTemplate bean to be used for migration operations. + * This property allows specifying a custom MongoTemplate bean from the application context + * that will be used by migration helpers to interact with the MongoDB database. + * If the specified bean is not found or is not usable, the system will fall back to the default + * "mongoTemplate" bean. This is particularly useful in multi-database scenarios where different + * MongoTemplate instances are configured for different databases. + * Default value is {@code "mongoTemplate"}. + */ + private String mongoTemplateBeanName = "mongoTemplate"; + /** * 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, diff --git a/nae-object-library/pom.xml b/nae-object-library/pom.xml index 4673ae95fd..259f629b1f 100644 --- a/nae-object-library/pom.xml +++ b/nae-object-library/pom.xml @@ -7,7 +7,7 @@ com.netgrif application-engine-parent - 7.0.0-RC10.5 + 7.0.0-RC10.6 nae-object-library diff --git a/nae-spring-core-adapter/pom.xml b/nae-spring-core-adapter/pom.xml index 339e5b7c12..8a3967af6b 100644 --- a/nae-spring-core-adapter/pom.xml +++ b/nae-spring-core-adapter/pom.xml @@ -7,7 +7,7 @@ com.netgrif application-engine-parent - 7.0.0-RC10.5 + 7.0.0-RC10.6 nae-spring-core-adapter diff --git a/nae-user-ce/pom.xml b/nae-user-ce/pom.xml index 8d4f5663fc..d859944d0b 100644 --- a/nae-user-ce/pom.xml +++ b/nae-user-ce/pom.xml @@ -6,7 +6,7 @@ com.netgrif application-engine-parent - 7.0.0-RC10.5 + 7.0.0-RC10.6 nae-user-ce diff --git a/nae-user-common/pom.xml b/nae-user-common/pom.xml index 584a6309ea..5d42e68e64 100644 --- a/nae-user-common/pom.xml +++ b/nae-user-common/pom.xml @@ -6,7 +6,7 @@ com.netgrif application-engine-parent - 7.0.0-RC10.5 + 7.0.0-RC10.6 nae-user-common diff --git a/pom.xml b/pom.xml index 42894ad3d9..437964e2c0 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.netgrif application-engine-parent - 7.0.0-RC10.5 + 7.0.0-RC10.6 pom NETGRIF Application Engine parent From 780c506ca3757f662d4196f5159f8ee9377ed4f0 Mon Sep 17 00:00:00 2001 From: Machac Date: Thu, 16 Jul 2026 15:51:59 +0200 Subject: [PATCH 02/21] Release 7.0.0-rc10.5 --- application-engine/pom.xml | 2 +- nae-object-library/pom.xml | 2 +- nae-spring-core-adapter/pom.xml | 2 +- nae-user-ce/pom.xml | 2 +- nae-user-common/pom.xml | 2 +- pom.xml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/application-engine/pom.xml b/application-engine/pom.xml index 1173880a61..05d931347a 100644 --- a/application-engine/pom.xml +++ b/application-engine/pom.xml @@ -6,7 +6,7 @@ com.netgrif application-engine-parent - 7.0.0-RC10.6 + 7.0.0-RC10.5 application-engine diff --git a/nae-object-library/pom.xml b/nae-object-library/pom.xml index 259f629b1f..4673ae95fd 100644 --- a/nae-object-library/pom.xml +++ b/nae-object-library/pom.xml @@ -7,7 +7,7 @@ com.netgrif application-engine-parent - 7.0.0-RC10.6 + 7.0.0-RC10.5 nae-object-library diff --git a/nae-spring-core-adapter/pom.xml b/nae-spring-core-adapter/pom.xml index 8a3967af6b..339e5b7c12 100644 --- a/nae-spring-core-adapter/pom.xml +++ b/nae-spring-core-adapter/pom.xml @@ -7,7 +7,7 @@ com.netgrif application-engine-parent - 7.0.0-RC10.6 + 7.0.0-RC10.5 nae-spring-core-adapter diff --git a/nae-user-ce/pom.xml b/nae-user-ce/pom.xml index d859944d0b..8d4f5663fc 100644 --- a/nae-user-ce/pom.xml +++ b/nae-user-ce/pom.xml @@ -6,7 +6,7 @@ com.netgrif application-engine-parent - 7.0.0-RC10.6 + 7.0.0-RC10.5 nae-user-ce diff --git a/nae-user-common/pom.xml b/nae-user-common/pom.xml index 5d42e68e64..584a6309ea 100644 --- a/nae-user-common/pom.xml +++ b/nae-user-common/pom.xml @@ -6,7 +6,7 @@ com.netgrif application-engine-parent - 7.0.0-RC10.6 + 7.0.0-RC10.5 nae-user-common diff --git a/pom.xml b/pom.xml index 437964e2c0..42894ad3d9 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.netgrif application-engine-parent - 7.0.0-RC10.6 + 7.0.0-RC10.5 pom NETGRIF Application Engine parent From 131e77ef47f89070af10bfed935a536f79f74a53 Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Thu, 23 Jul 2026 15:51:28 +0200 Subject: [PATCH 03/21] Update `ProcessRoleRepository` with `findByNetworkIdentifierAndObjectId` and deprecate `findByNetworkIdAndObjectId`. --- .../petrinet/domain/roles/ProcessRoleRepository.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/application-engine/src/main/java/com/netgrif/application/engine/petrinet/domain/roles/ProcessRoleRepository.java b/application-engine/src/main/java/com/netgrif/application/engine/petrinet/domain/roles/ProcessRoleRepository.java index 236885d141..c399280b5f 100755 --- a/application-engine/src/main/java/com/netgrif/application/engine/petrinet/domain/roles/ProcessRoleRepository.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/petrinet/domain/roles/ProcessRoleRepository.java @@ -139,9 +139,20 @@ default Optional findByCompositeId(String compositeId) { * @param objectId the object ID * @return an {@link Optional} containing the found {@link ProcessRole}, if any */ + @Deprecated(since = "7.0.2") @Query("{ '_id.shortProcessId': ?0, '_id.objectId': ?1 }") Optional findByNetworkIdAndObjectId(String networkId, ObjectId objectId); + /** + * Finds a {@link ProcessRole} by a network ID and object ID. + * + * @param networkIdentifier the short process ID + * @param objectId the object ID + * @return an {@link Optional} containing the found {@link ProcessRole}, if any + */ + @Query("{ '_id.shortProcessIdentifier': ?0, '_id.objectId': ?1 }") + Optional findByNetworkIdentifierAndObjectId(String networkIdentifier, ObjectId objectId); + /** * Finds all {@link ProcessRole} entities by a collection of composite resource IDs. * From 149671c2517d511647c84964567e52cf1b502a3a Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Mon, 27 Jul 2026 09:33:19 +0200 Subject: [PATCH 04/21] Introduce asynchronous execution support in `ActionDelegate` with state management and enhanced executor configuration. - Added methods to `ActionDelegate` to manage asynchronous execution lifecycle (`retainForAsyncExecution`, `releaseAfterAsyncExecution`, `clearAfterExecution`). - Enhanced `AsyncRunner` with delegate state tracking and custom `actionsExecutor`. - Added `async_run.xml` test Petri net and corresponding test cases to validate asynchronous action handling. --- .../application/engine/AsyncRunner.groovy | 67 +++++++++++++++++-- .../logic/action/ActionDelegate.groovy | 31 ++++++++- .../TaskExecutionConfiguration.java | 8 +++ .../engine/action/ActionDelegateTest.groovy | 39 +++++++++-- .../test/resources/petriNets/async_run.xml | 25 +++++++ 5 files changed, 159 insertions(+), 11 deletions(-) create mode 100644 application-engine/src/test/resources/petriNets/async_run.xml diff --git a/application-engine/src/main/groovy/com/netgrif/application/engine/AsyncRunner.groovy b/application-engine/src/main/groovy/com/netgrif/application/engine/AsyncRunner.groovy index 86e5bd7e1d..e7ed22a207 100644 --- a/application-engine/src/main/groovy/com/netgrif/application/engine/AsyncRunner.groovy +++ b/application-engine/src/main/groovy/com/netgrif/application/engine/AsyncRunner.groovy @@ -1,18 +1,73 @@ package com.netgrif.application.engine -import org.springframework.scheduling.annotation.Async +import com.netgrif.application.engine.petrinet.domain.dataset.logic.action.ActionDelegate +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.context.annotation.Bean +import org.springframework.core.task.TaskExecutor +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor import org.springframework.stereotype.Service +import java.util.concurrent.atomic.AtomicBoolean + @Service class AsyncRunner { - @Async + private final TaskExecutor actionsExecutor + + AsyncRunner(@Qualifier("actionsExecutor") TaskExecutor actionsExecutor) { + this.actionsExecutor = actionsExecutor + } + void run(Closure closure) { - closure() + ActionDelegate actionDelegate = findActionDelegate(closure) + actionDelegate?.retainForAsyncExecution() + AtomicBoolean released = new AtomicBoolean() + + Runnable task = { + try { + closure() + } finally { + release(actionDelegate, released) + } + } as Runnable + + try { + execute(task) + } catch (Throwable throwable) { + release(actionDelegate, released) + throw throwable + } } - @Async void execute(final Runnable runnable) { - runnable.run() + actionsExecutor.execute(runnable) + } + + private static void release(ActionDelegate actionDelegate, AtomicBoolean released) { + if (actionDelegate != null && released.compareAndSet(false, true)) { + actionDelegate.releaseAfterAsyncExecution() + } + } + + private static ActionDelegate findActionDelegate(Closure closure) { + Set visited = Collections.newSetFromMap(new IdentityHashMap<>()) + return findActionDelegate(closure, visited) + } + + private static ActionDelegate findActionDelegate(Object candidate, Set visited) { + if (candidate == null || !visited.add(candidate)) { + return null + } + if (candidate instanceof ActionDelegate) { + return candidate + } + if (!(candidate instanceof Closure)) { + return null + } + + Closure nestedClosure = (Closure) candidate + return findActionDelegate(nestedClosure.delegate, visited) + ?: findActionDelegate(nestedClosure.owner, visited) + ?: findActionDelegate(nestedClosure.thisObject, visited) } -} \ No newline at end of file +} diff --git a/application-engine/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy b/application-engine/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy index 33e14ce22b..e68be143fa 100644 --- a/application-engine/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy +++ b/application-engine/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy @@ -236,6 +236,10 @@ class ActionDelegate extends DelegateExpando { FieldActionsRunner actionsRunner List outcomes + private int pendingAsyncExecutions + private boolean executionFinished + private boolean executionStateCleared + def init(Action action, Case useCase, Optional task, FieldActionsRunner actionsRunner, Map params = [:]) { this.action = action this.useCase = useCase @@ -250,7 +254,32 @@ class ActionDelegate extends DelegateExpando { this.Plugin = new PluginHolder() } - void clearAfterExecution() { + synchronized void retainForAsyncExecution() { + if (executionStateCleared) { + throw new IllegalStateException("Action execution state has already been cleared") + } + pendingAsyncExecutions++ + } + + synchronized void releaseAfterAsyncExecution() { + if (pendingAsyncExecutions == 0) { + throw new IllegalStateException("No asynchronous action execution is pending") + } + pendingAsyncExecutions-- + clearExecutionStateIfPossible() + } + + synchronized void clearAfterExecution() { + executionFinished = true + clearExecutionStateIfPossible() + } + + private void clearExecutionStateIfPossible() { + if (!executionFinished || pendingAsyncExecutions != 0 || executionStateCleared) { + return + } + executionStateCleared = true + this.action = null this.useCase = null this.task = null diff --git a/application-engine/src/main/java/com/netgrif/application/engine/configuration/TaskExecutionConfiguration.java b/application-engine/src/main/java/com/netgrif/application/engine/configuration/TaskExecutionConfiguration.java index c261d1e4f8..a8db07dc1a 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/configuration/TaskExecutionConfiguration.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/configuration/TaskExecutionConfiguration.java @@ -16,4 +16,12 @@ public TaskExecutor taskExecutor() { executor.setCorePoolSize(5); return executor; } + + @Bean("actionsExecutor") + TaskExecutor actionsExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(5); + executor.setThreadNamePrefix("ActionsExecutor-"); + return executor; + } } \ No newline at end of file diff --git a/application-engine/src/test/groovy/com/netgrif/application/engine/action/ActionDelegateTest.groovy b/application-engine/src/test/groovy/com/netgrif/application/engine/action/ActionDelegateTest.groovy index a64e41e756..03174bb4a1 100644 --- a/application-engine/src/test/groovy/com/netgrif/application/engine/action/ActionDelegateTest.groovy +++ b/application-engine/src/test/groovy/com/netgrif/application/engine/action/ActionDelegateTest.groovy @@ -4,13 +4,22 @@ import com.icegreen.greenmail.configuration.GreenMailConfiguration import com.icegreen.greenmail.util.GreenMail import com.icegreen.greenmail.util.ServerSetup import com.netgrif.application.engine.TestHelper -import com.netgrif.application.engine.configuration.properties.SecurityConfigurationProperties import com.netgrif.application.engine.auth.service.UserService import com.netgrif.application.engine.auth.web.requestbodies.NewUserRequest +import com.netgrif.application.engine.configuration.properties.SecurityConfigurationProperties import com.netgrif.application.engine.objects.auth.domain.AbstractUser +import com.netgrif.application.engine.objects.petrinet.domain.VersionType +import com.netgrif.application.engine.objects.workflow.domain.eventoutcomes.caseoutcomes.CreateCaseEventOutcome +import com.netgrif.application.engine.objects.workflow.domain.eventoutcomes.petrinetoutcomes.ImportPetriNetEventOutcome import com.netgrif.application.engine.petrinet.domain.dataset.logic.action.ActionDelegate +import com.netgrif.application.engine.petrinet.params.ImportPetriNetParams +import com.netgrif.application.engine.petrinet.service.interfaces.IPetriNetService +import com.netgrif.application.engine.startup.runner.SuperCreatorRunner +import com.netgrif.application.engine.workflow.params.CreateCaseParams import com.netgrif.application.engine.workflow.service.interfaces.IFilterImportExportService +import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService import com.netgrif.application.engine.workflow.web.responsebodies.MessageResource +import jakarta.mail.internet.MimeMessage import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test @@ -20,9 +29,7 @@ import org.springframework.boot.test.context.SpringBootTest import org.springframework.test.context.ActiveProfiles import org.springframework.test.context.junit.jupiter.SpringExtension -import jakarta.mail.internet.MimeMessage - -import static java.util.Base64.* +import static java.util.Base64.getEncoder @SpringBootTest @ActiveProfiles(["test"]) @@ -44,6 +51,15 @@ class ActionDelegateTest { @Autowired private SecurityConfigurationProperties.WebProperties webProperties + @Autowired + private IPetriNetService petriNetService + + @Autowired + private IWorkflowService workflowService + + @Autowired + private SuperCreatorRunner superCreator + @BeforeEach void before() { testHelper.truncateDbs() @@ -114,4 +130,19 @@ class ActionDelegateTest { assert actionDelegate.makeUrl(webProperties.publicWeb.url, identifier) == url assert actionDelegate.makeUrl("test.netgrif.com/public", "identifier") == "test.netgrif.com/public/${getEncoder().encodeToString(identifier.bytes)}" } + + @Test + void testAsyncRunAction() { + ImportPetriNetEventOutcome net = petriNetService.importPetriNet(ImportPetriNetParams.with() + .xmlFile(new FileInputStream("src/test/resources/petriNets/async_run.xml")) + .releaseType(VersionType.MAJOR) + .author(superCreator.getLoggedSuper()) + .build()) + assert net.getNet() != null + CreateCaseEventOutcome outcome = workflowService.createCase(CreateCaseParams.with() + .processId(net.getNet().getStringId()) + .title("Test title") + .build()) + assert outcome.getCase() != null + } } diff --git a/application-engine/src/test/resources/petriNets/async_run.xml b/application-engine/src/test/resources/petriNets/async_run.xml new file mode 100644 index 0000000000..8e4a5d6c48 --- /dev/null +++ b/application-engine/src/test/resources/petriNets/async_run.xml @@ -0,0 +1,25 @@ + + async_run + 1.0.0 + ASR + Async Run + device_hub + true + true + false + + + async_run_create + + + + + + + + \ No newline at end of file From cf8ec2d022eac51bb7b29c894a7f34df0d91ff33 Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Mon, 27 Jul 2026 10:09:44 +0200 Subject: [PATCH 05/21] Introduce asynchronous execution support in `ActionDelegate` with state management and enhanced executor configuration. - Added methods to `ActionDelegate` to manage asynchronous execution lifecycle (`retainForAsyncExecution`, `releaseAfterAsyncExecution`, `clearAfterExecution`). - Enhanced `AsyncRunner` with delegate state tracking and custom `actionsExecutor`. - Added `async_run.xml` test Petri net and corresponding test cases to validate asynchronous action handling. --- .../configuration/MongoClientConfiguration.java | 14 +++++++++++--- .../engine/action/ActionDelegateTest.groovy | 1 + 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/application-engine/src/main/java/com/netgrif/application/engine/configuration/MongoClientConfiguration.java b/application-engine/src/main/java/com/netgrif/application/engine/configuration/MongoClientConfiguration.java index 305789a7e5..b4fb59da1b 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/configuration/MongoClientConfiguration.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/configuration/MongoClientConfiguration.java @@ -4,11 +4,12 @@ import com.mongodb.connection.*; import com.netgrif.application.engine.configuration.properties.DataConfigurationProperties; import org.jetbrains.annotations.NotNull; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.FilterType; +import org.springframework.context.annotation.*; +import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.convert.MappingMongoConverter; import org.springframework.data.mongodb.repository.config.EnableMongoRepositories; import org.springframework.util.StringUtils; @@ -32,6 +33,13 @@ public MongoClientConfiguration(DataConfigurationProperties.MongoProperties mong this.mongoProperties = mongoProperties; } + @Bean + @Primary + @Override + public @NotNull MongoTemplate mongoTemplate(@NotNull MongoDatabaseFactory databaseFactory, @NotNull MappingMongoConverter converter) { + return super.mongoTemplate(databaseFactory, converter); + } + @Override protected void configureClientSettings(MongoClientSettings.Builder builder) { builder diff --git a/application-engine/src/test/groovy/com/netgrif/application/engine/action/ActionDelegateTest.groovy b/application-engine/src/test/groovy/com/netgrif/application/engine/action/ActionDelegateTest.groovy index 03174bb4a1..410f6a9422 100644 --- a/application-engine/src/test/groovy/com/netgrif/application/engine/action/ActionDelegateTest.groovy +++ b/application-engine/src/test/groovy/com/netgrif/application/engine/action/ActionDelegateTest.groovy @@ -142,6 +142,7 @@ class ActionDelegateTest { CreateCaseEventOutcome outcome = workflowService.createCase(CreateCaseParams.with() .processId(net.getNet().getStringId()) .title("Test title") + .author(userService.getLoggedOrSystem()) .build()) assert outcome.getCase() != null } From b3a0def68c13cd06e89f25b1196fc9cf6603639a Mon Sep 17 00:00:00 2001 From: Machac Date: Mon, 27 Jul 2026 10:51:09 +0200 Subject: [PATCH 06/21] [NAE-2417] Fix dependency vulnerabilities - Update `server-patterns` to replace `/manage/**` with `/manage/health` - Configure `management.endpoint.shutdown.enabled` as `false` across properties - Expand `management.endpoints.web.exposure.include` for additional actuator endpoints --- .../src/main/resources/application-old-dev.properties | 5 +++-- .../src/main/resources/application-old.properties | 8 ++++---- application-engine/src/main/resources/application.yaml | 6 +++++- .../src/test/resources/application-test.yaml | 2 +- docker-compose.yml | 7 ++++--- 5 files changed, 17 insertions(+), 11 deletions(-) diff --git a/application-engine/src/main/resources/application-old-dev.properties b/application-engine/src/main/resources/application-old-dev.properties index 7f749349c7..cb334ce57d 100644 --- a/application-engine/src/main/resources/application-old-dev.properties +++ b/application-engine/src/main/resources/application-old-dev.properties @@ -25,7 +25,7 @@ nae.storage.clean=true nae.admin.password=password springdoc.swagger-ui.enabled=true -nae.security.server-patterns=/api/auth/signup,/api/auth/token/verify,/api/auth/reset,/api/auth/recover,/v3/api-docs,/v3/api-docs/**,/swagger-ui.html,/swagger-ui/**,/api/public/**,/manage/** +nae.security.server-patterns=/api/auth/signup,/api/auth/token/verify,/api/auth/reset,/api/auth/recover,/v3/api-docs,/v3/api-docs/**,/swagger-ui.html,/swagger-ui/**,/api/public/**,/manage/health logging.level.org.springframework.data.elasticsearch.core=info logging.level.com.netgrif.application.engine.elastic.service=info @@ -43,6 +43,7 @@ nae.cache.petriNetCache=petriNetCache # Actuator management.health.ldap.enabled=false management.health.mail.enabled=false +management.endpoint.shutdown.enabled=false #logging.level.root=debug @@ -50,4 +51,4 @@ management.health.mail.enabled=false netgrif.engine.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 diff --git a/application-engine/src/main/resources/application-old.properties b/application-engine/src/main/resources/application-old.properties index d7b4f6662c..63efcfc252 100644 --- a/application-engine/src/main/resources/application-old.properties +++ b/application-engine/src/main/resources/application-old.properties @@ -88,8 +88,8 @@ nae.security.limits.email-block-time-type=DAYS nae.security.jwt.expiration=900000 nae.security.jwt.algorithm=RSA nae.security.jwt.private-key=file:src/main/resources/certificates/private.der -nae.security.server-patterns=/api/auth/signup,/api/auth/token/verify,/api/auth/reset,/api/auth/recover,/api/public/**,/v3/api-docs/public,/manage/** -nae.security.anonymous-exceptions=/api/auth/signup,/api/auth/token/verify,/api/auth/reset,/api/auth/recover,/manage/** +nae.security.server-patterns=/api/auth/signup,/api/auth/token/verify,/api/auth/reset,/api/auth/recover,/api/public/**,/v3/api-docs/public,/manage/health +nae.security.anonymous-exceptions=/api/auth/signup,/api/auth/token/verify,/api/auth/reset,/api/auth/recover,/manage/health springdoc.swagger-ui.enabled=false # Quartz (this config overwrites quartz.properties resource file) @@ -180,8 +180,8 @@ netgrif.engine.storage.minio.enabled=false # Actuator management.endpoints.web.base-path=/manage management.endpoints.web.path-mapping.prometheus=metric -management.endpoints.web.exposure.exclude=shutdown -management.endpoints.web.exposure.include=* +management.endpoints.web.exposure.include=health,info,metrics,prometheus,beans,env,threaddump,loggers,logfile,logfiles,caches,conditions,configprops,mappings,scheduledtasks,httpexchanges,startup,nodeinfo,elasticnodes,netgriftracing +management.endpoint.shutdown.enabled=false management.endpoint.status.up-statuses=paused,maintenance,running management.endpoint.loggers.enabled=true management.endpoint.health.show-details=when_authorized diff --git a/application-engine/src/main/resources/application.yaml b/application-engine/src/main/resources/application.yaml index 30df7a68c5..545d60b497 100644 --- a/application-engine/src/main/resources/application.yaml +++ b/application-engine/src/main/resources/application.yaml @@ -121,8 +121,12 @@ netgrif: web: base-path: /manage exposure: - include: "health,info,metrics,loggers,env,beans,threaddump,heapdump,mappings,conditions,configprops,scheduledtasks,caches,flyway,liquibase,prometheus" + include: "health,info,metrics,prometheus,beans,env,threaddump,heapdump,loggers,logfile,logfiles,caches,conditions,configprops,mappings,scheduledtasks,httpexchanges,startup,nodeinfo,elasticnodes,netgriftracing" endpoint: + shutdown: + enabled: false + heapdump: + access: unrestricted health: show-details: when_authorized show-components: when_authorized diff --git a/application-engine/src/test/resources/application-test.yaml b/application-engine/src/test/resources/application-test.yaml index 7bca9e86cb..a67af46bdc 100644 --- a/application-engine/src/test/resources/application-test.yaml +++ b/application-engine/src/test/resources/application-test.yaml @@ -32,7 +32,7 @@ netgrif: server: port: 0 security: - server-patterns: /api/auth/signup,/api/auth/token/verify,/api/auth/reset,/api/auth/recover,/v3/api-docs,/v3/api-docs/**,/swagger-ui.html,/swagger-ui/**,/api/public/**,/manage/** + server-patterns: /api/auth/signup,/api/auth/token/verify,/api/auth/reset,/api/auth/recover,/v3/api-docs,/v3/api-docs/**,/swagger-ui.html,/swagger-ui/**,/api/public/**,/manage/health limits: login-attempts: 3 login-timeout: 3 diff --git a/docker-compose.yml b/docker-compose.yml index 382b9b7f06..04d9110c94 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,7 +16,8 @@ services: # NETGRIF_WORKER_NODE_NODE_TYPE: ENGINE_ROOT # netgrif.engine.data.redis.namespace: netgrif # netgrif.engine.logging.file.path: log -# netgrif.engine.management.endpoints.web.exposure.include: "*" +# netgrif.engine.management.endpoints.web.exposure.include: "health,info,metrics,prometheus,beans,env,threaddump,loggers,logfile,logfiles,caches,conditions,configprops,mappings,scheduledtasks,httpexchanges,startup,nodeinfo,elasticnodes,netgriftracing" +# netgrif.engine.management.endpoint.shutdown.enabled: 'false' # netgrif.engine.management.health.ldap.enabled: 'false' # netgrif.engine.main.allow-bean-definition-overriding: 'true' # netgrif.engine.management.health.mail.enabled: 'false' @@ -27,8 +28,8 @@ services: # netgrif.engine.management.endpoint.health.show-details: always # netgrif.engine.management.metrics.export.simple: enabled # netgrif.engine.management.endpoints.web.base-path: /manage -# netgrif.engine.security.server-patterns: /api/auth/signup,/api/auth/token/verify,/api/auth/reset,/api/auth/recover,/v3/api-docs,/v3/api-docs/**,/swagger-ui.html,/swagger-ui/**,/api/public/**,/manage/** -# netgrif.engine.security.anonymous-exceptions: /api/auth/signup,/api/auth/token/verify,/api/auth/reset,/api/auth/recover,/manage/** +# netgrif.engine.security.server-patterns: /api/auth/signup,/api/auth/token/verify,/api/auth/reset,/api/auth/recover,/v3/api-docs,/v3/api-docs/**,/swagger-ui.html,/swagger-ui/**,/api/public/**,/manage/health +# netgrif.engine.security.anonymous-exceptions: /api/auth/signup,/api/auth/token/verify,/api/auth/reset,/api/auth/recover,/manage/health # depends_on: # nae-mongodb: # condition: service_started From 16c3d08159fd7735bd2625e579dcf0377982bc4b Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Mon, 27 Jul 2026 11:17:34 +0200 Subject: [PATCH 07/21] Remove `MigrationMongoTemplateConfiguration` and associated fallback logic; consolidate MongoTemplate usage to default bean. --- .../helpers/CaseMigrationHelper.groovy | 2 +- .../helpers/PetriNetMigrationHelper.groovy | 2 +- .../helpers/TaskMigrationHelper.groovy | 2 +- .../MigrationMongoTemplateConfiguration.java | 47 ------------------- .../MongoClientConfiguration.java | 10 ---- .../application/engine/TestHelper.groovy | 4 +- 6 files changed, 5 insertions(+), 62 deletions(-) delete mode 100644 application-engine/src/main/java/com/netgrif/application/engine/configuration/MigrationMongoTemplateConfiguration.java diff --git a/application-engine/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy b/application-engine/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy index 1282a60143..941d57b241 100644 --- a/application-engine/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy +++ b/application-engine/src/main/groovy/com/netgrif/application/engine/migration/helpers/CaseMigrationHelper.groovy @@ -66,7 +66,7 @@ class CaseMigrationHelper extends AbstractMigrationHelper { * @param mongoTemplate MongoTemplate to interact with MongoDB. * @param migrationConfigurationProperties Properties for migration configuration, including cases. */ - CaseMigrationHelper(@Qualifier("migrationMongoTemplate") MongoTemplate mongoTemplate, + CaseMigrationHelper(@Qualifier("mongoTemplate") MongoTemplate mongoTemplate, MigrationProperties migrationProperties, IPetriNetService petriNetService, IElasticCaseService elasticCaseService, diff --git a/application-engine/src/main/groovy/com/netgrif/application/engine/migration/helpers/PetriNetMigrationHelper.groovy b/application-engine/src/main/groovy/com/netgrif/application/engine/migration/helpers/PetriNetMigrationHelper.groovy index a03fc955b0..3cc7935e92 100644 --- a/application-engine/src/main/groovy/com/netgrif/application/engine/migration/helpers/PetriNetMigrationHelper.groovy +++ b/application-engine/src/main/groovy/com/netgrif/application/engine/migration/helpers/PetriNetMigrationHelper.groovy @@ -84,7 +84,7 @@ class PetriNetMigrationHelper extends AbstractMigrationHelper { * @param importerProvider the {@link ObjectFactory} that supplies {@link Importer} instances for importing Petri Net models from various sources * @param userService the {@link UserService} for managing user-related operations, including retrieving system user for Petri Net imports */ - PetriNetMigrationHelper(@Qualifier("migrationMongoTemplate") MongoTemplate mongoTemplate, + PetriNetMigrationHelper(@Qualifier("mongoTemplate") MongoTemplate mongoTemplate, MigrationProperties migrationProperties, IPetriNetService petriNetService, ProcessRoleRepository processRoleRepository, diff --git a/application-engine/src/main/groovy/com/netgrif/application/engine/migration/helpers/TaskMigrationHelper.groovy b/application-engine/src/main/groovy/com/netgrif/application/engine/migration/helpers/TaskMigrationHelper.groovy index ef116d5845..944aeeaec4 100644 --- a/application-engine/src/main/groovy/com/netgrif/application/engine/migration/helpers/TaskMigrationHelper.groovy +++ b/application-engine/src/main/groovy/com/netgrif/application/engine/migration/helpers/TaskMigrationHelper.groovy @@ -71,7 +71,7 @@ class TaskMigrationHelper extends AbstractMigrationHelper { * * @param mongoTemplate the {@link MongoTemplate} to use for interacting with MongoDB */ - TaskMigrationHelper(@Qualifier("migrationMongoTemplate") MongoTemplate mongoTemplate, + TaskMigrationHelper(@Qualifier("mongoTemplate") MongoTemplate mongoTemplate, MigrationProperties migrationProperties, IPetriNetService petriNetService, ITaskService taskService, diff --git a/application-engine/src/main/java/com/netgrif/application/engine/configuration/MigrationMongoTemplateConfiguration.java b/application-engine/src/main/java/com/netgrif/application/engine/configuration/MigrationMongoTemplateConfiguration.java deleted file mode 100644 index 9b0b8fe6d4..0000000000 --- a/application-engine/src/main/java/com/netgrif/application/engine/configuration/MigrationMongoTemplateConfiguration.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.netgrif.application.engine.configuration; - -import com.netgrif.application.engine.configuration.properties.MigrationProperties; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.mongodb.core.MongoTemplate; -import org.springframework.util.StringUtils; - -@Slf4j -@Configuration -@RequiredArgsConstructor -public class MigrationMongoTemplateConfiguration { - - private static final String DEFAULT_TEMPLATE = "mongoTemplate"; - private final BeanFactory beanFactory; - private final MigrationProperties migrationProperties; - - @Bean(name = "migrationMongoTemplate") - public MongoTemplate getMongoTemplate() { - String requested = migrationProperties.getMongoTemplateBeanName(); - - if (!StringUtils.hasText(requested)) { - requested = DEFAULT_TEMPLATE; - } else { - requested = requested.trim(); - } - - if (!beanFactory.containsBean(requested)) { - log.warn("Migration MongoTemplate bean [{}] not found, falling back to [{}]", requested, DEFAULT_TEMPLATE); - return beanFactory.getBean(DEFAULT_TEMPLATE, MongoTemplate.class); - } - - try { - MongoTemplate template = beanFactory.getBean(requested, MongoTemplate.class); - log.info("Using MongoTemplate bean [{}] for migration helpers", requested); - return template; - } catch (BeansException e) { - log.warn("Migration MongoTemplate bean [{}] is not usable, falling back to [{}]", - requested, DEFAULT_TEMPLATE, e); - return beanFactory.getBean(DEFAULT_TEMPLATE, MongoTemplate.class); - } - } -} \ No newline at end of file diff --git a/application-engine/src/main/java/com/netgrif/application/engine/configuration/MongoClientConfiguration.java b/application-engine/src/main/java/com/netgrif/application/engine/configuration/MongoClientConfiguration.java index b4fb59da1b..ed10630fc3 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/configuration/MongoClientConfiguration.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/configuration/MongoClientConfiguration.java @@ -6,10 +6,7 @@ import org.jetbrains.annotations.NotNull; import org.springframework.context.annotation.*; -import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration; -import org.springframework.data.mongodb.core.MongoTemplate; -import org.springframework.data.mongodb.core.convert.MappingMongoConverter; import org.springframework.data.mongodb.repository.config.EnableMongoRepositories; import org.springframework.util.StringUtils; @@ -33,13 +30,6 @@ public MongoClientConfiguration(DataConfigurationProperties.MongoProperties mong this.mongoProperties = mongoProperties; } - @Bean - @Primary - @Override - public @NotNull MongoTemplate mongoTemplate(@NotNull MongoDatabaseFactory databaseFactory, @NotNull MappingMongoConverter converter) { - return super.mongoTemplate(databaseFactory, converter); - } - @Override protected void configureClientSettings(MongoClientSettings.Builder builder) { builder diff --git a/application-engine/src/test/groovy/com/netgrif/application/engine/TestHelper.groovy b/application-engine/src/test/groovy/com/netgrif/application/engine/TestHelper.groovy index f6921558ab..076a5a8a08 100644 --- a/application-engine/src/test/groovy/com/netgrif/application/engine/TestHelper.groovy +++ b/application-engine/src/test/groovy/com/netgrif/application/engine/TestHelper.groovy @@ -24,7 +24,7 @@ class TestHelper { private SuperCreatorRunner superCreator @Autowired - private MongoTemplate template + private MongoTemplate mongoTemplate @Autowired private ElasticIndexService indexService @@ -84,7 +84,7 @@ class TestHelper { private DefaultRealmRunner defaultRealmRunner void truncateDbs() { - template.db.drop() + mongoTemplate.db.drop() indexService.deleteIndex(ElasticPetriNet.class) indexService.deleteIndex(ElasticCase.class) indexService.deleteIndex(ElasticTask.class) From aa359a80861a5bbd659db96d360087c93ee45ae9 Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Mon, 27 Jul 2026 12:17:05 +0200 Subject: [PATCH 08/21] Refactor imports and update test dependencies in `ActionDelegateTest` and `TestHelper` for consistency and clarity. --- .../application/engine/TestHelper.groovy | 2 +- .../engine/action/ActionDelegateTest.groovy | 22 +++++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/application-engine/src/test/groovy/com/netgrif/application/engine/TestHelper.groovy b/application-engine/src/test/groovy/com/netgrif/application/engine/TestHelper.groovy index 8eb179ffca..6e26c217b2 100644 --- a/application-engine/src/test/groovy/com/netgrif/application/engine/TestHelper.groovy +++ b/application-engine/src/test/groovy/com/netgrif/application/engine/TestHelper.groovy @@ -138,7 +138,7 @@ class TestHelper { } private List mongoCollections() { - return template.db.listCollectionNames() + return mongoTemplate.db.listCollectionNames() .into(new ArrayList()) .findAll { !it.startsWith("system.") } } diff --git a/application-engine/src/test/groovy/com/netgrif/application/engine/action/ActionDelegateTest.groovy b/application-engine/src/test/groovy/com/netgrif/application/engine/action/ActionDelegateTest.groovy index a43dac4b77..3b024edca5 100644 --- a/application-engine/src/test/groovy/com/netgrif/application/engine/action/ActionDelegateTest.groovy +++ b/application-engine/src/test/groovy/com/netgrif/application/engine/action/ActionDelegateTest.groovy @@ -4,14 +4,31 @@ import com.icegreen.greenmail.configuration.GreenMailConfiguration import com.icegreen.greenmail.util.GreenMail import com.icegreen.greenmail.util.ServerSetup import com.netgrif.application.engine.TestHelper +import com.netgrif.application.engine.adapter.spring.auth.domain.AuthorityImpl +import com.netgrif.application.engine.adapter.spring.workflow.domain.QCase import com.netgrif.application.engine.configuration.properties.SecurityConfigurationProperties import com.netgrif.application.engine.auth.service.UserService import com.netgrif.application.engine.auth.web.requestbodies.NewUserRequest +import com.netgrif.application.engine.objects.auth.constants.UserConstants import com.netgrif.application.engine.objects.auth.domain.AbstractUser +import com.netgrif.application.engine.objects.auth.domain.ActorTransformer +import com.netgrif.application.engine.objects.petrinet.domain.VersionType +import com.netgrif.application.engine.objects.petrinet.domain.dataset.FileFieldValue +import com.netgrif.application.engine.objects.workflow.domain.Case +import com.netgrif.application.engine.objects.workflow.domain.eventoutcomes.caseoutcomes.CreateCaseEventOutcome +import com.netgrif.application.engine.objects.workflow.domain.eventoutcomes.petrinetoutcomes.ImportPetriNetEventOutcome import com.netgrif.application.engine.petrinet.domain.dataset.logic.action.ActionDelegate +import com.netgrif.application.engine.petrinet.params.ImportPetriNetParams +import com.netgrif.application.engine.petrinet.service.interfaces.IPetriNetService +import com.netgrif.application.engine.startup.runner.DefaultFiltersRunner +import com.netgrif.application.engine.startup.runner.FilterRunner +import com.netgrif.application.engine.startup.runner.SuperCreatorRunner +import com.netgrif.application.engine.workflow.params.CreateCaseParams import com.netgrif.application.engine.workflow.service.interfaces.IFilterImportExportService import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService import com.netgrif.application.engine.workflow.web.responsebodies.MessageResource +import jakarta.mail.internet.MimeMessage +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -47,9 +64,6 @@ class ActionDelegateTest { @Autowired private DefaultFiltersRunner defaultFiltersRunner - @Autowired - private IWorkflowService workflowService - @Autowired private UserService userService @@ -170,7 +184,7 @@ class ActionDelegateTest { void testAsyncRunAction() { ImportPetriNetEventOutcome net = petriNetService.importPetriNet(ImportPetriNetParams.with() .xmlFile(new FileInputStream("src/test/resources/petriNets/async_run.xml")) - .releaseType(VersionType.MAJOR) + .releaseType(ØIVersionType.MAJOR) .author(superCreator.getLoggedSuper()) .build()) assert net.getNet() != null From 436032ae36ea43833ebebeec447b1dd0fcac4faf Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Tue, 28 Jul 2026 11:02:05 +0200 Subject: [PATCH 09/21] Refactor repository methods to use `findByNetworkIdentifierAndObjectId` and deprecate `findByNetworkIdAndObjectId`; update tests and enums accordingly. --- .../petrinet/domain/roles/ProcessRoleRepository.java | 2 +- .../workflow/domain/repositories/CaseRepository.java | 8 +++++++- .../application/engine/action/ActionDelegateTest.groovy | 2 +- .../application/engine/action/AssignRemoveTest.groovy | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/application-engine/src/main/java/com/netgrif/application/engine/petrinet/domain/roles/ProcessRoleRepository.java b/application-engine/src/main/java/com/netgrif/application/engine/petrinet/domain/roles/ProcessRoleRepository.java index c399280b5f..19fa7e1b7a 100755 --- a/application-engine/src/main/java/com/netgrif/application/engine/petrinet/domain/roles/ProcessRoleRepository.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/petrinet/domain/roles/ProcessRoleRepository.java @@ -126,7 +126,7 @@ default Optional findByCompositeId(String compositeId) { if (parts.length == 2) { String networkId = parts[0]; ObjectId objectId = new ObjectId(parts[1]); - return findByNetworkIdAndObjectId(networkId, objectId); + return findByNetworkIdentifierAndObjectId(networkId, objectId); } else { return findByIdObjectId(new ObjectId(compositeId)); } diff --git a/application-engine/src/main/java/com/netgrif/application/engine/workflow/domain/repositories/CaseRepository.java b/application-engine/src/main/java/com/netgrif/application/engine/workflow/domain/repositories/CaseRepository.java index 306bf0b83d..c23b56fd5e 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/workflow/domain/repositories/CaseRepository.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/workflow/domain/repositories/CaseRepository.java @@ -38,15 +38,21 @@ default Optional findById(String compositeId) { if (parts.length == 2) { String networkId = parts[0]; ObjectId objectId = new ObjectId(parts[1]); - return findByNetworkIdAndObjectId(networkId, objectId); + return findByNetworkIdentifierAndObjectId(networkId, objectId); } else { return findByIdObjectId(new ObjectId(compositeId)); } } + @Deprecated(since = "7.0.0") @Query("{ '_id.shortProcessId': ?0, '_id.objectId': ?1 }") Optional findByNetworkIdAndObjectId(String ProcessId, ObjectId objectId); + @Deprecated(since = "7.0.1") + @Query("{ '_id.shortProcessIdentifier': ?0, '_id.objectId': ?1 }") + Optional findByNetworkIdentifierAndObjectId(String ProcessId, ObjectId objectId); + + @Override default void customize(QuerydslBindings bindings, QCase qCase) { } diff --git a/application-engine/src/test/groovy/com/netgrif/application/engine/action/ActionDelegateTest.groovy b/application-engine/src/test/groovy/com/netgrif/application/engine/action/ActionDelegateTest.groovy index 3b024edca5..97aedd3daf 100644 --- a/application-engine/src/test/groovy/com/netgrif/application/engine/action/ActionDelegateTest.groovy +++ b/application-engine/src/test/groovy/com/netgrif/application/engine/action/ActionDelegateTest.groovy @@ -184,7 +184,7 @@ class ActionDelegateTest { void testAsyncRunAction() { ImportPetriNetEventOutcome net = petriNetService.importPetriNet(ImportPetriNetParams.with() .xmlFile(new FileInputStream("src/test/resources/petriNets/async_run.xml")) - .releaseType(ØIVersionType.MAJOR) + .releaseType(VersionType.MAJOR) .author(superCreator.getLoggedSuper()) .build()) assert net.getNet() != null diff --git a/application-engine/src/test/groovy/com/netgrif/application/engine/action/AssignRemoveTest.groovy b/application-engine/src/test/groovy/com/netgrif/application/engine/action/AssignRemoveTest.groovy index fb4f770ec4..5797a30936 100644 --- a/application-engine/src/test/groovy/com/netgrif/application/engine/action/AssignRemoveTest.groovy +++ b/application-engine/src/test/groovy/com/netgrif/application/engine/action/AssignRemoveTest.groovy @@ -80,7 +80,7 @@ class AssignRemoveTest { [userAuthorities.get("user")] as Authority[], [] as ProcessRole[]) def loggedUser = ActorTransformer.toLoggedUser(testUser) - auth = new UsernamePasswordAuthenticationToken(loggedUser, "password", loggedUser.authorities) + auth = new UsernamePasswordAuthenticationToken(loggedUser, "password", loggedUser.authoritySet) SecurityContextHolder.getContext().setAuthentication(auth) Set actionRoleIds = net.roles.values() From 6256f71ae69cec50a249fbb2e46db21bc6828fcb Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Wed, 29 Jul 2026 10:58:33 +0200 Subject: [PATCH 10/21] Refactor `ElasticCaseService` to improve full-text search logic and variable naming consistency. --- .../elastic/service/ElasticCaseService.java | 66 ++++++++++++++++--- 1 file changed, 58 insertions(+), 8 deletions(-) diff --git a/application-engine/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java b/application-engine/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java index 199b1251a7..60a7f9305f 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java @@ -59,7 +59,7 @@ public class ElasticCaseService extends ElasticViewPermissionService implements protected DataConfigurationProperties.ElasticsearchProperties elasticProperties; protected IPetriNetService petriNetService; protected IWorkflowService workflowService; - protected IElasticCasePrioritySearch iElasticCasePrioritySearch; + protected IElasticCasePrioritySearch elasticCasePrioritySearch; protected ApplicationEventPublisher publisher; protected ElasticQueueManager caseElasticIndexQueueManager; protected ElasticQueueManager caseElasticDeleteQueueManager; @@ -70,7 +70,7 @@ public ElasticCaseService(ElasticCaseRepository repository, DataConfigurationProperties.ElasticsearchProperties elasticProperties, @Lazy IPetriNetService petriNetService, @Lazy IWorkflowService workflowService, - IElasticCasePrioritySearch iElasticCasePrioritySearch, + IElasticCasePrioritySearch elasticCasePrioritySearch, ApplicationEventPublisher publisher, ElasticsearchClient elasticsearchClient) { this.repository = repository; @@ -79,7 +79,7 @@ public ElasticCaseService(ElasticCaseRepository repository, this.elasticProperties = elasticProperties; this.petriNetService = petriNetService; this.workflowService = workflowService; - this.iElasticCasePrioritySearch = iElasticCasePrioritySearch; + this.elasticCasePrioritySearch = elasticCasePrioritySearch; this.publisher = publisher; this.caseElasticIndexQueueManager = new ElasticQueueManager(elasticProperties, elasticsearchClient, publisher); this.caseElasticDeleteQueueManager = new ElasticQueueManager(elasticProperties, elasticsearchClient, publisher); @@ -413,14 +413,29 @@ protected void buildTagsQuery(CaseSearchRequest request, BoolQuery.Builder query } protected void buildFullTextQuery(CaseSearchRequest request, BoolQuery.Builder query) { - if (request.fullText == null || request.fullText.isEmpty()) { + if (request.fullText == null || request.fullText.isBlank()) { return; } - // TODO: improvement? wildcard does not scale good - //String searchText = elasticsearchProperties.isAnalyzerEnabled() ? request.fullText : "*" + request.fullText + "*"; - String searchText = "*" + request.fullText + "*"; - QueryStringQuery fullTextQuery = QueryStringQuery.of(builder -> builder.fields(iElasticCasePrioritySearch.fullTextFields()).query(searchText)); + List fullTextTerms = normalizeFullTextSearch(request.fullText); + if (fullTextTerms.isEmpty()) { + return; + } + + String searchText = fullTextTerms.stream() + .map(this::escapeQueryStringTerm) + .map(term -> "*" + term + "*") + .collect(Collectors.joining(" AND ")); + + QueryStringQuery fullTextQuery = QueryStringQuery.of(builder -> builder + .fields(elasticCasePrioritySearch.fullTextFields()) + .query(searchText) + .allowLeadingWildcard(true) + .analyzeWildcard(true) + .defaultOperator(co.elastic.clients.elasticsearch._types.query_dsl.Operator.And) + ); + + log.debug("Fulltext search input [{}] normalized to terms [{}] and query [{}]", request.fullText, fullTextTerms, searchText); query.must(fullTextQuery._toQuery()); } @@ -531,4 +546,39 @@ private BulkOperation createIndexOperation(ElasticCase useCase) { .id(useCase.getId()) .document(template.getElasticsearchConverter().mapObject(useCase)))); } + + private List normalizeFullTextSearch(String fullText) { + return Arrays.stream(fullText + .replaceAll("\\\\+(?=\\s)", "") + .replaceAll("\\s+", " ") + .trim() + .split("\\s+")) + .map(String::trim) + .filter(term -> !term.isBlank()) + .toList(); + } + + private String escapeQueryStringTerm(String term) { + StringBuilder escaped = new StringBuilder(); + + for (int i = 0; i < term.length(); i++) { + char character = term.charAt(i); + + if (isQueryStringReservedCharacter(character)) { + escaped.append('\\'); + } + + escaped.append(character); + } + + return escaped.toString(); + } + + private boolean isQueryStringReservedCharacter(char character) { + return switch (character) { + case '+', '-', '=', '>', '<', '!', '(', ')', '{', '}', '[', ']', + '^', '"', '~', '*', '?', ':', '\\', '/' -> true; + default -> false; + }; + } } From 56148a2e2f903479c92ebb829c294dea58932e3c Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Wed, 29 Jul 2026 12:07:29 +0200 Subject: [PATCH 11/21] Update key field in `CaseEventHandler` and simplify regex in `ElasticCaseService` --- .../application/engine/elastic/service/ElasticCaseService.java | 2 +- .../application/engine/workflow/service/CaseEventHandler.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/application-engine/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java b/application-engine/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java index 60a7f9305f..3416651bc3 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java @@ -549,7 +549,7 @@ private BulkOperation createIndexOperation(ElasticCase useCase) { private List normalizeFullTextSearch(String fullText) { return Arrays.stream(fullText - .replaceAll("\\\\+(?=\\s)", "") + .replaceAll("\\\\", "") .replaceAll("\\s+", " ") .trim() .split("\\s+")) diff --git a/application-engine/src/main/java/com/netgrif/application/engine/workflow/service/CaseEventHandler.java b/application-engine/src/main/java/com/netgrif/application/engine/workflow/service/CaseEventHandler.java index 70505e4c9a..0644139bad 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/workflow/service/CaseEventHandler.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/workflow/service/CaseEventHandler.java @@ -57,7 +57,7 @@ public void onAfterDelete(AfterDeleteEvent event) { return; } - String objectId = ((Document)document.get("_id")).get("shortProcessId") + "-" + ((Document)document.get("_id")).get("objectId").toString(); + String objectId = ((Document)document.get("_id")).get("shortProcessIdentifier") + "-" + ((Document)document.get("_id")).get("objectId").toString(); service.remove(objectId); } } From fac8492e5876c6b27dd60cea829713cf811f1b30 Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Wed, 29 Jul 2026 12:30:29 +0200 Subject: [PATCH 12/21] Refactor `ElasticCaseService` full-text search logic: replace `QueryStringQuery` with `BoolQuery`, introduce `FullTextField` model, and enhance wildcard handling. --- .../elastic/service/ElasticCaseService.java | 72 +++++++++++-------- .../elastic/service/model/FullTextField.java | 4 ++ 2 files changed, 46 insertions(+), 30 deletions(-) create mode 100644 application-engine/src/main/java/com/netgrif/application/engine/elastic/service/model/FullTextField.java diff --git a/application-engine/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java b/application-engine/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java index 3416651bc3..81877a7c06 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java @@ -10,6 +10,7 @@ import co.elastic.clients.elasticsearch.core.bulk.BulkOperation; import com.netgrif.application.engine.configuration.properties.DataConfigurationProperties; import com.netgrif.application.engine.elastic.domain.BulkOperationWrapper; +import com.netgrif.application.engine.elastic.service.model.FullTextField; import com.netgrif.application.engine.objects.auth.domain.LoggedUser; import com.netgrif.application.engine.objects.elastic.domain.ElasticCase; import com.netgrif.application.engine.elastic.domain.ElasticCaseRepository; @@ -422,21 +423,28 @@ protected void buildFullTextQuery(CaseSearchRequest request, BoolQuery.Builder q return; } - String searchText = fullTextTerms.stream() - .map(this::escapeQueryStringTerm) - .map(term -> "*" + term + "*") - .collect(Collectors.joining(" AND ")); - - QueryStringQuery fullTextQuery = QueryStringQuery.of(builder -> builder - .fields(elasticCasePrioritySearch.fullTextFields()) - .query(searchText) - .allowLeadingWildcard(true) - .analyzeWildcard(true) - .defaultOperator(co.elastic.clients.elasticsearch._types.query_dsl.Operator.And) - ); - - log.debug("Fulltext search input [{}] normalized to terms [{}] and query [{}]", request.fullText, fullTextTerms, searchText); - query.must(fullTextQuery._toQuery()); + List fullTextFields = elasticCasePrioritySearch.fullTextFields().stream() + .map(this::parseFullTextField) + .toList(); + + BoolQuery.Builder fullTextQuery = new BoolQuery.Builder(); + + fullTextTerms.forEach(term -> { + BoolQuery.Builder termQuery = new BoolQuery.Builder(); + String wildcardValue = "*" + escapeWildcardValue(term) + "*"; + + fullTextFields.forEach(fullTextField -> termQuery.should(QueryBuilders.wildcard(builder -> builder + .field(fullTextField.field()) + .value(wildcardValue) + .caseInsensitive(true) + .boost(fullTextField.boost()) + ))); + + termQuery.minimumShouldMatch("1"); + fullTextQuery.must(termQuery.build()._toQuery()); + }); + + query.must(fullTextQuery.build()._toQuery()); } /** @@ -554,31 +562,35 @@ private List normalizeFullTextSearch(String fullText) { .trim() .split("\\s+")) .map(String::trim) + .map(this::removeDanglingEscapeCharacters) .filter(term -> !term.isBlank()) .toList(); } - private String escapeQueryStringTerm(String term) { - StringBuilder escaped = new StringBuilder(); + private String removeDanglingEscapeCharacters(String term) { + return term.replaceAll("\\\\+$", ""); + } - for (int i = 0; i < term.length(); i++) { - char character = term.charAt(i); + private FullTextField parseFullTextField(String fieldDefinition) { + String[] parts = fieldDefinition.split("\\^", 2); + String field = parts[0].trim(); + float boost = 1.0f; - if (isQueryStringReservedCharacter(character)) { - escaped.append('\\'); + if (parts.length == 2 && !parts[1].isBlank()) { + try { + boost = Float.parseFloat(parts[1].trim()); + } catch (NumberFormatException e) { + log.warn("Invalid boost [{}] in fulltext field definition [{}]. Using default boost 1.0.", parts[1], fieldDefinition); } - - escaped.append(character); } - return escaped.toString(); + return new FullTextField(field, boost); } - private boolean isQueryStringReservedCharacter(char character) { - return switch (character) { - case '+', '-', '=', '>', '<', '!', '(', ')', '{', '}', '[', ']', - '^', '"', '~', '*', '?', ':', '\\', '/' -> true; - default -> false; - }; + private String escapeWildcardValue(String value) { + return value + .replace("\\", "\\\\") + .replace("*", "\\*") + .replace("?", "\\?"); } } diff --git a/application-engine/src/main/java/com/netgrif/application/engine/elastic/service/model/FullTextField.java b/application-engine/src/main/java/com/netgrif/application/engine/elastic/service/model/FullTextField.java new file mode 100644 index 0000000000..bcaddca2d1 --- /dev/null +++ b/application-engine/src/main/java/com/netgrif/application/engine/elastic/service/model/FullTextField.java @@ -0,0 +1,4 @@ +package com.netgrif.application.engine.elastic.service.model; + +public record FullTextField(String field, float boost) { +} From a4beb5dc60f7a67db3da5e514f751b0a44c0fc68 Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Wed, 29 Jul 2026 15:03:58 +0200 Subject: [PATCH 13/21] Refactor `GroovyShellFactory` to improve import handling logic: replace static imports with dynamic discovery, introduce `ACTION_IMPORT_PACKAGES`, and optimize class loading. --- .../engine/event/GroovyShellFactory.java | 105 +++++++++++++++--- 1 file changed, 89 insertions(+), 16 deletions(-) diff --git a/application-engine/src/main/java/com/netgrif/application/engine/event/GroovyShellFactory.java b/application-engine/src/main/java/com/netgrif/application/engine/event/GroovyShellFactory.java index 4da799201d..0403e1580d 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/event/GroovyShellFactory.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/event/GroovyShellFactory.java @@ -4,18 +4,29 @@ import org.codehaus.groovy.control.CompilerConfiguration; import org.codehaus.groovy.control.customizers.ImportCustomizer; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.core.type.classreading.CachingMetadataReaderFactory; +import org.springframework.core.type.classreading.MetadataReaderFactory; import org.springframework.stereotype.Service; -import java.io.BufferedReader; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @Service public class GroovyShellFactory implements IGroovyShellFactory { + private static final List ACTION_IMPORT_PACKAGES = List.of( + "com.netgrif.application.engine.objects.*", + "com.netgrif.application.engine.workflow.domain.*", + "com.netgrif.application.engine.adapter.spring.*" + ); + @Autowired private CompilerConfiguration configuration; @@ -30,7 +41,7 @@ public GroovyShell getGroovyShell() { if (local == null) { ImportCustomizer importCustomizer = new ImportCustomizer(); - Set classNames = findAllClassesUsingClassLoader("com.netgrif.application.engine.workflow.domain"); + Set classNames = findAllActionImportClasses(); importCustomizer.addImports(classNames.toArray(new String[0])); configuration.addCompilationCustomizers(importCustomizer); @@ -43,21 +54,83 @@ public GroovyShell getGroovyShell() { return local; } - private Set findAllClassesUsingClassLoader(String packageName) { - String path = packageName.replace(".", "/"); - InputStream stream = ClassLoader.getSystemClassLoader().getResourceAsStream(path); - if (stream == null) { - return Set.of(); + private Set findAllActionImportClasses() { + Set> classes = ACTION_IMPORT_PACKAGES.stream() + .flatMap(packageName -> findAllClassesUsingClassLoader(packageName).stream()) + .map(this::loadClass) + .collect(Collectors.toCollection(HashSet::new)); + + return classes.stream() + .collect(Collectors.groupingBy(Class::getSimpleName)) + .values().stream() + .map(this::selectActionImport) + .flatMap(Optional::stream) + .map(Class::getName) + .collect(Collectors.toSet()); + } + + private Optional> selectActionImport(List> candidates) { + int highestSpecificity = candidates.stream() + .mapToInt(candidate -> importSpecificity(candidate, candidates)) + .max() + .orElseThrow(); + + List> mostSpecificCandidates = candidates.stream() + .filter(candidate -> importSpecificity(candidate, candidates) == highestSpecificity) + .toList(); + + return mostSpecificCandidates.size() == 1 + ? Optional.of(mostSpecificCandidates.getFirst()) + : Optional.empty(); + } + + private int importSpecificity(Class candidate, List> candidates) { + return (int) candidates.stream() + .filter(other -> other != candidate && other.isAssignableFrom(candidate)) + .count(); + } + + private Class loadClass(String className) { + try { + return Class.forName(className, false, getClass().getClassLoader()); + } catch (ClassNotFoundException e) { + throw new IllegalStateException("Failed to load discovered action import class " + className, e); } + } - try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) { - return reader.lines() - .filter(line -> line.endsWith(".class")) - .map(line -> packageName + "." + line.substring(0, line.lastIndexOf('.'))) - .collect(Collectors.toSet()); + private Set findAllClassesUsingClassLoader(String packagePattern) { + boolean recursive = packagePattern.endsWith(".*"); + String packageName = recursive + ? packagePattern.substring(0, packagePattern.length() - 2) + : packagePattern; + return findAllClassesUsingClassLoader(packageName, recursive); + } + + private Set findAllClassesUsingClassLoader(String packageName, boolean recursive) { + String path = packageName.replace(".", "/"); + ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(getClass().getClassLoader()); + MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resolver); + String classPattern = recursive ? "/**/*.class" : "/*.class"; + + try { + Resource[] resources = resolver.getResources( + ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + path + classPattern + ); + Set classNames = new HashSet<>(); + for (Resource resource : resources) { + if (resource.getDescription().contains("test-classes")) { + continue; + } + String className = metadataReaderFactory.getMetadataReader(resource) + .getClassMetadata() + .getClassName(); + if (!className.contains("$") && !className.endsWith("package-info") && !className.endsWith("module-info")) { + classNames.add(className); + } + } + return classNames; } catch (IOException e) { - e.printStackTrace(); - return Set.of(); + throw new IllegalStateException("Failed to discover classes in package " + packageName, e); } } } From 8353e174db6a0c25ddcb7cf76efa2eb15f5a2ad6 Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Thu, 30 Jul 2026 12:02:49 +0200 Subject: [PATCH 14/21] Refactor `GroovyShellFactory` to improve import handling logic: replace static imports with dynamic discovery, introduce `ACTION_IMPORT_PACKAGES`, and optimize class loading. --- .../groovy/GroovyShellConfiguration.java | 6 +++++- .../engine/event/GroovyShellFactory.java | 17 +++++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/application-engine/src/main/java/com/netgrif/application/engine/configuration/groovy/GroovyShellConfiguration.java b/application-engine/src/main/java/com/netgrif/application/engine/configuration/groovy/GroovyShellConfiguration.java index 919ae67a91..0a7b930e79 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/configuration/groovy/GroovyShellConfiguration.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/configuration/groovy/GroovyShellConfiguration.java @@ -40,7 +40,11 @@ protected String[] getDefaultEngineImports() { return new String[]{ "com.netgrif.application.engine.objects", "com.netgrif.application.engine.adapter.spring", - "java.time" + "com.netgrif.application.engine.objects.petrinet.domain.dataset", + "org.bson.types", + "java.time", + "java.util", + "java.util.stream" }; } diff --git a/application-engine/src/main/java/com/netgrif/application/engine/event/GroovyShellFactory.java b/application-engine/src/main/java/com/netgrif/application/engine/event/GroovyShellFactory.java index 0403e1580d..d727234b49 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/event/GroovyShellFactory.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/event/GroovyShellFactory.java @@ -1,5 +1,6 @@ package com.netgrif.application.engine.event; +import com.netgrif.application.engine.configuration.properties.ActionsProperties; import groovy.lang.GroovyShell; import org.codehaus.groovy.control.CompilerConfiguration; import org.codehaus.groovy.control.customizers.ImportCustomizer; @@ -30,6 +31,9 @@ public class GroovyShellFactory implements IGroovyShellFactory { @Autowired private CompilerConfiguration configuration; + @Autowired + private ActionsProperties actionsProperties; + private volatile GroovyShell shell; @Override @@ -55,6 +59,10 @@ public GroovyShell getGroovyShell() { } private Set findAllActionImportClasses() { + Set configuredImportNames = actionsProperties.getImports().stream() + .map(this::simpleName) + .collect(Collectors.toSet()); + Set> classes = ACTION_IMPORT_PACKAGES.stream() .flatMap(packageName -> findAllClassesUsingClassLoader(packageName).stream()) .map(this::loadClass) @@ -62,13 +70,18 @@ private Set findAllActionImportClasses() { return classes.stream() .collect(Collectors.groupingBy(Class::getSimpleName)) - .values().stream() - .map(this::selectActionImport) + .entrySet().stream() + .filter(entry -> !configuredImportNames.contains(entry.getKey())) + .map(entry -> selectActionImport(entry.getValue())) .flatMap(Optional::stream) .map(Class::getName) .collect(Collectors.toSet()); } + private String simpleName(String className) { + return className.substring(className.lastIndexOf('.') + 1); + } + private Optional> selectActionImport(List> candidates) { int highestSpecificity = candidates.stream() .mapToInt(candidate -> importSpecificity(candidate, candidates)) From 56fbc83c7ea636ab99ed7e14cb0b877e75fbde25 Mon Sep 17 00:00:00 2001 From: Machac Date: Fri, 31 Jul 2026 14:00:47 +0200 Subject: [PATCH 15/21] Release 7.0.2 - Update version to 7.0.2 in all affected `pom.xml` files - Modify `Dockerfile` and `Dockerfile.multi-stage` to use version 7.0.2 - Adjust `docker-compose.yml` to reflect the new image version --- Dockerfile | 2 +- Dockerfile.multi-stage | 2 +- application-engine/pom.xml | 2 +- docker-compose.yml | 2 +- nae-object-library/pom.xml | 2 +- nae-spring-core-adapter/pom.xml | 2 +- nae-user-ce/pom.xml | 2 +- nae-user-common/pom.xml | 2 +- pom.xml | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index 63083238e1..76da5e5645 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM eclipse-temurin:21-jre -ARG VERSION="7.0.1" +ARG VERSION="7.0.2" LABEL authors="Netgrif " \ org.opencontainers.image.authors="NETGRIF " \ diff --git a/Dockerfile.multi-stage b/Dockerfile.multi-stage index aa72d96e9a..37d00a3225 100644 --- a/Dockerfile.multi-stage +++ b/Dockerfile.multi-stage @@ -31,7 +31,7 @@ RUN mvn -B -e -DskipTests -P docker-build clean install # prepare runtime FROM eclipse-temurin:21-jre-jammy -ARG VERSION="7.0.1" +ARG VERSION="7.0.2" LABEL authors="Netgrif " \ org.opencontainers.image.authors="NETGRIF " \ diff --git a/application-engine/pom.xml b/application-engine/pom.xml index 92e4c07fbe..98600ef1fb 100644 --- a/application-engine/pom.xml +++ b/application-engine/pom.xml @@ -6,7 +6,7 @@ com.netgrif application-engine-parent - 7.0.1 + 7.0.2 application-engine diff --git a/docker-compose.yml b/docker-compose.yml index 04d9110c94..14516e1441 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: "3.8" services: # eng: -# image: netgrif/application-engine:7.0.1 +# image: netgrif/application-engine:7.0.2 # ports: # - "8080:8080" # environment: diff --git a/nae-object-library/pom.xml b/nae-object-library/pom.xml index 03c840a36f..f9b71b3efe 100644 --- a/nae-object-library/pom.xml +++ b/nae-object-library/pom.xml @@ -7,7 +7,7 @@ com.netgrif application-engine-parent - 7.0.1 + 7.0.2 nae-object-library diff --git a/nae-spring-core-adapter/pom.xml b/nae-spring-core-adapter/pom.xml index d29da05493..162556131b 100644 --- a/nae-spring-core-adapter/pom.xml +++ b/nae-spring-core-adapter/pom.xml @@ -7,7 +7,7 @@ com.netgrif application-engine-parent - 7.0.1 + 7.0.2 nae-spring-core-adapter diff --git a/nae-user-ce/pom.xml b/nae-user-ce/pom.xml index 9dedda3727..4823d85744 100644 --- a/nae-user-ce/pom.xml +++ b/nae-user-ce/pom.xml @@ -6,7 +6,7 @@ com.netgrif application-engine-parent - 7.0.1 + 7.0.2 nae-user-ce diff --git a/nae-user-common/pom.xml b/nae-user-common/pom.xml index b9639bd891..a5358188d6 100644 --- a/nae-user-common/pom.xml +++ b/nae-user-common/pom.xml @@ -6,7 +6,7 @@ com.netgrif application-engine-parent - 7.0.1 + 7.0.2 nae-user-common diff --git a/pom.xml b/pom.xml index 01fef9a9f0..0183543883 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.netgrif application-engine-parent - 7.0.1 + 7.0.2 pom NETGRIF Application Engine parent From 1a816b260d27d17135d073aff4b83a9de01a732e Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Mon, 3 Aug 2026 11:07:50 +0200 Subject: [PATCH 16/21] Reuse the default task executor for asynchronous action execution Remove the dedicated actions executor bean Log ambiguous automatic Groovy action imports instead of silently skipping them Prevent dev profile from dropping MongoDB and Elasticsearch data by default Update deprecated case repository methods to version 7.0.2 Remove obsolete migration MongoTemplate bean configuration Validate Elasticsearch full-text field boost values before applying them --- .../application/engine/AsyncRunner.groovy | 2 +- .../TaskExecutionConfiguration.java | 8 -------- .../properties/MigrationProperties.java | 11 ----------- .../elastic/service/ElasticCaseService.java | 7 ++++--- .../engine/event/GroovyShellFactory.java | 17 ++++++++++++++--- .../domain/repositories/CaseRepository.java | 4 ++-- 6 files changed, 21 insertions(+), 28 deletions(-) diff --git a/application-engine/src/main/groovy/com/netgrif/application/engine/AsyncRunner.groovy b/application-engine/src/main/groovy/com/netgrif/application/engine/AsyncRunner.groovy index e7ed22a207..ea8600a5c9 100644 --- a/application-engine/src/main/groovy/com/netgrif/application/engine/AsyncRunner.groovy +++ b/application-engine/src/main/groovy/com/netgrif/application/engine/AsyncRunner.groovy @@ -14,7 +14,7 @@ class AsyncRunner { private final TaskExecutor actionsExecutor - AsyncRunner(@Qualifier("actionsExecutor") TaskExecutor actionsExecutor) { + AsyncRunner(@Qualifier("taskExecutor") TaskExecutor actionsExecutor) { this.actionsExecutor = actionsExecutor } diff --git a/application-engine/src/main/java/com/netgrif/application/engine/configuration/TaskExecutionConfiguration.java b/application-engine/src/main/java/com/netgrif/application/engine/configuration/TaskExecutionConfiguration.java index a8db07dc1a..c261d1e4f8 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/configuration/TaskExecutionConfiguration.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/configuration/TaskExecutionConfiguration.java @@ -16,12 +16,4 @@ public TaskExecutor taskExecutor() { executor.setCorePoolSize(5); return executor; } - - @Bean("actionsExecutor") - TaskExecutor actionsExecutor() { - ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); - executor.setCorePoolSize(5); - executor.setThreadNamePrefix("ActionsExecutor-"); - return executor; - } } \ No newline at end of file diff --git a/application-engine/src/main/java/com/netgrif/application/engine/configuration/properties/MigrationProperties.java b/application-engine/src/main/java/com/netgrif/application/engine/configuration/properties/MigrationProperties.java index 424e07f103..41adab2ab9 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/configuration/properties/MigrationProperties.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/configuration/properties/MigrationProperties.java @@ -21,17 +21,6 @@ @ConfigurationProperties(prefix = "netgrif.engine.migration") public class MigrationProperties { - /** - * The name of the MongoTemplate bean to be used for migration operations. - * This property allows specifying a custom MongoTemplate bean from the application context - * that will be used by migration helpers to interact with the MongoDB database. - * If the specified bean is not found or is not usable, the system will fall back to the default - * "mongoTemplate" bean. This is particularly useful in multi-database scenarios where different - * MongoTemplate instances are configured for different databases. - * Default value is {@code "mongoTemplate"}. - */ - private String mongoTemplateBeanName = "mongoTemplate"; - /** * 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, diff --git a/application-engine/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java b/application-engine/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java index 81877a7c06..6dde07a1c0 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java @@ -557,8 +557,8 @@ private BulkOperation createIndexOperation(ElasticCase useCase) { private List normalizeFullTextSearch(String fullText) { return Arrays.stream(fullText - .replaceAll("\\\\", "") - .replaceAll("\\s+", " ") + .replace("\\\\", "") + .replace("\\s+", " ") .trim() .split("\\s+")) .map(String::trim) @@ -568,7 +568,7 @@ private List normalizeFullTextSearch(String fullText) { } private String removeDanglingEscapeCharacters(String term) { - return term.replaceAll("\\\\+$", ""); + return term.replace("\\\\+$", ""); } private FullTextField parseFullTextField(String fieldDefinition) { @@ -579,6 +579,7 @@ private FullTextField parseFullTextField(String fieldDefinition) { if (parts.length == 2 && !parts[1].isBlank()) { try { boost = Float.parseFloat(parts[1].trim()); + boost = Float.isFinite(boost) && boost > 0 ? boost : 1.0f; } catch (NumberFormatException e) { log.warn("Invalid boost [{}] in fulltext field definition [{}]. Using default boost 1.0.", parts[1], fieldDefinition); } diff --git a/application-engine/src/main/java/com/netgrif/application/engine/event/GroovyShellFactory.java b/application-engine/src/main/java/com/netgrif/application/engine/event/GroovyShellFactory.java index d727234b49..c75fc1640b 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/event/GroovyShellFactory.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/event/GroovyShellFactory.java @@ -2,6 +2,7 @@ import com.netgrif.application.engine.configuration.properties.ActionsProperties; import groovy.lang.GroovyShell; +import lombok.extern.slf4j.Slf4j; import org.codehaus.groovy.control.CompilerConfiguration; import org.codehaus.groovy.control.customizers.ImportCustomizer; import org.springframework.beans.factory.annotation.Autowired; @@ -19,6 +20,7 @@ import java.util.Set; import java.util.stream.Collectors; +@Slf4j @Service public class GroovyShellFactory implements IGroovyShellFactory { @@ -92,9 +94,18 @@ private Optional> selectActionImport(List> candidates) { .filter(candidate -> importSpecificity(candidate, candidates) == highestSpecificity) .toList(); - return mostSpecificCandidates.size() == 1 - ? Optional.of(mostSpecificCandidates.getFirst()) - : Optional.empty(); + if (mostSpecificCandidates.size() != 1) { + List collidingClassNames = candidates.stream() + .map(Class::getName) + .sorted() + .toList(); + log.warn("Skipping automatic action import for ambiguous class name [{}]. " + + "Conflicting candidates: {}. Configure an explicit import to resolve the conflict.", + candidates.getFirst().getSimpleName(), collidingClassNames); + return Optional.empty(); + } + + return Optional.of(mostSpecificCandidates.getFirst()); } private int importSpecificity(Class candidate, List> candidates) { diff --git a/application-engine/src/main/java/com/netgrif/application/engine/workflow/domain/repositories/CaseRepository.java b/application-engine/src/main/java/com/netgrif/application/engine/workflow/domain/repositories/CaseRepository.java index c23b56fd5e..75e496e411 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/workflow/domain/repositories/CaseRepository.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/workflow/domain/repositories/CaseRepository.java @@ -44,11 +44,11 @@ default Optional findById(String compositeId) { } } - @Deprecated(since = "7.0.0") + @Deprecated(since = "7.0.2") @Query("{ '_id.shortProcessId': ?0, '_id.objectId': ?1 }") Optional findByNetworkIdAndObjectId(String ProcessId, ObjectId objectId); - @Deprecated(since = "7.0.1") + @Deprecated(since = "7.0.2") @Query("{ '_id.shortProcessIdentifier': ?0, '_id.objectId': ?1 }") Optional findByNetworkIdentifierAndObjectId(String ProcessId, ObjectId objectId); From 503ab242e0015de78a330337ee7a79918a4095e1 Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Mon, 3 Aug 2026 13:22:12 +0200 Subject: [PATCH 17/21] Refactor `ElasticCaseService`: replace `replace` with `replaceAll` for regex handling consistency in full-text search normalization methods. --- .../engine/elastic/service/ElasticCaseService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/application-engine/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java b/application-engine/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java index 6dde07a1c0..7f93512db6 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java @@ -557,8 +557,8 @@ private BulkOperation createIndexOperation(ElasticCase useCase) { private List normalizeFullTextSearch(String fullText) { return Arrays.stream(fullText - .replace("\\\\", "") - .replace("\\s+", " ") + .replaceAll("\\\\", "") + .replaceAll("\\s+", " ") .trim() .split("\\s+")) .map(String::trim) @@ -568,7 +568,7 @@ private List normalizeFullTextSearch(String fullText) { } private String removeDanglingEscapeCharacters(String term) { - return term.replace("\\\\+$", ""); + return term.replaceAll("\\\\+$", ""); } private FullTextField parseFullTextField(String fieldDefinition) { From c9928f19228cb14f7354479b408ee1de6c12b650 Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Mon, 3 Aug 2026 13:46:26 +0200 Subject: [PATCH 18/21] Add unit and integration tests for `ElasticCaseService` to validate full-text search behavior and edge cases. --- .../ElasticCaseServiceIntegrationTest.java | 233 ++++++++++ .../service/ElasticCaseServiceTest.java | 398 ++++++++++++++++++ 2 files changed, 631 insertions(+) create mode 100644 application-engine/src/test/java/com/netgrif/application/engine/elastic/service/ElasticCaseServiceIntegrationTest.java create mode 100644 application-engine/src/test/java/com/netgrif/application/engine/elastic/service/ElasticCaseServiceTest.java diff --git a/application-engine/src/test/java/com/netgrif/application/engine/elastic/service/ElasticCaseServiceIntegrationTest.java b/application-engine/src/test/java/com/netgrif/application/engine/elastic/service/ElasticCaseServiceIntegrationTest.java new file mode 100644 index 0000000000..69aff80379 --- /dev/null +++ b/application-engine/src/test/java/com/netgrif/application/engine/elastic/service/ElasticCaseServiceIntegrationTest.java @@ -0,0 +1,233 @@ +package com.netgrif.application.engine.elastic.service; + +import com.netgrif.application.engine.ApplicationEngine; +import com.netgrif.application.engine.TestHelper; +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.web.requestbodies.CaseSearchRequest; +import com.netgrif.application.engine.objects.petrinet.domain.PetriNet; +import com.netgrif.application.engine.objects.workflow.domain.Case; +import com.netgrif.application.engine.startup.ImportHelper; +import com.netgrif.application.engine.startup.runner.SuperCreatorRunner; +import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService; +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.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.time.Duration; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SpringBootTest(classes = ApplicationEngine.class) +@ActiveProfiles("test") +@ExtendWith(SpringExtension.class) +@TestPropertySource(locations = "classpath:application-test.yaml") +class ElasticCaseServiceIntegrationTest { + + private static final String FIRST_TERM = "TOTOK"; + private static final String SECOND_TERM = "Pistok"; + private static final Duration SEARCH_TIMEOUT = Duration.ofSeconds(15); + + @Autowired + private TestHelper testHelper; + + @Autowired + private ImportHelper importHelper; + + @Autowired + private IWorkflowService workflowService; + + @Autowired + private IElasticCaseService elasticCaseService; + + @Autowired + private IElasticCaseMappingService caseMappingService; + + @Autowired + private ElasticCaseRepository elasticCaseRepository; + + @Autowired + private SuperCreatorRunner superCreator; + + private PetriNet net; + + @BeforeEach + void before() { + testHelper.truncateDbs(); + net = importHelper.createNet("all_data.xml").orElseThrow(); + } + + @Test + void fullTextSearchIsCaseInsensitiveAndRequiresEveryTerm() throws InterruptedException { + Case matchingCase = createAndIndexCase(FIRST_TERM + " " + SECOND_TERM); + Case nonMatchingCase = createAndIndexCase(FIRST_TERM + " hentok"); + waitForIndexedCases(List.of(matchingCase.getStringId(), nonMatchingCase.getStringId())); + + CaseSearchRequest request = fullTextRequest( + FIRST_TERM.toLowerCase(Locale.ROOT) + " " + SECOND_TERM.toUpperCase(Locale.ROOT) + ); + Page result = waitForSearchResult(List.of(request), true, 1); + + assertEquals(1, result.getTotalElements()); + assertCaseIds(result, matchingCase); + assertEquals(1, elasticCaseService.count( + List.of(request), + superCreator.getLoggedSuper(), + Locale.ENGLISH, + true + )); + } + + @Test + void fullTextSearchNormalizesWhitespaceAndBackslashes() throws InterruptedException { + Case matchingCase = createAndIndexCase(FIRST_TERM + " " + SECOND_TERM); + waitForIndexedCases(List.of(matchingCase.getStringId())); + + CaseSearchRequest request = fullTextRequest( + " \\" + FIRST_TERM + "\\ \t\n " + SECOND_TERM + "\\ " + ); + Page result = waitForSearchResult(List.of(request), true, 1); + + assertEquals(1, result.getTotalElements()); + assertCaseIds(result, matchingCase); + } + + @Test + void fullTextSearchTreatsAsteriskAsLiteralCharacter() throws InterruptedException { + Case literalMatch = createAndIndexCase("Asterisk*Marker"); + Case wildcardLookalike = createAndIndexCase("AsteriskXMarker"); + waitForIndexedCases(List.of(literalMatch.getStringId(), wildcardLookalike.getStringId())); + + Page result = waitForSearchResult( + List.of(fullTextRequest("Asterisk*Marker")), + true, + 1 + ); + + assertEquals(1, result.getTotalElements()); + assertCaseIds(result, literalMatch); + } + + @Test + void fullTextSearchTreatsQuestionMarkAsLiteralCharacter() throws InterruptedException { + Case literalMatch = createAndIndexCase("Question?Marker"); + Case wildcardLookalike = createAndIndexCase("QuestionXMarker"); + waitForIndexedCases(List.of(literalMatch.getStringId(), wildcardLookalike.getStringId())); + + Page result = waitForSearchResult( + List.of(fullTextRequest("Question?Marker")), + true, + 1 + ); + + assertEquals(1, result.getTotalElements()); + assertCaseIds(result, literalMatch); + } + + @Test + void multipleFullTextRequestsSupportIntersectionAndUnion() throws InterruptedException { + Case matchingBoth = createAndIndexCase(FIRST_TERM + " " + SECOND_TERM); + Case matchingFirst = createAndIndexCase(FIRST_TERM + " OnlyFirst"); + Case matchingSecond = createAndIndexCase("OnlySecond " + SECOND_TERM); + waitForIndexedCases(List.of( + matchingBoth.getStringId(), + matchingFirst.getStringId(), + matchingSecond.getStringId() + )); + + List requests = List.of( + fullTextRequest(FIRST_TERM), + fullTextRequest(SECOND_TERM) + ); + + Page intersection = waitForSearchResult(requests, true, 1); + Page union = waitForSearchResult(requests, false, 3); + + assertEquals(1, intersection.getTotalElements()); + assertCaseIds(intersection, matchingBoth); + assertEquals(3, union.getTotalElements()); + assertCaseIds(union, matchingBoth, matchingFirst, matchingSecond); + assertEquals(1, elasticCaseService.count( + requests, + superCreator.getLoggedSuper(), + Locale.ENGLISH, + true + )); + assertEquals(3, elasticCaseService.count( + requests, + superCreator.getLoggedSuper(), + Locale.ENGLISH, + false + )); + } + + private Case createAndIndexCase(String title) { + Case useCase = importHelper.createCaseAsSuper(title, net); + Case savedCase = workflowService.save(useCase); + elasticCaseService.indexNow(caseMappingService.transform(savedCase)); + return savedCase; + } + + private CaseSearchRequest fullTextRequest(String fullText) { + return new CaseSearchRequest(Map.of("fullText", fullText)); + } + + private void assertCaseIds(Page result, Case... expectedCases) { + assertEquals( + List.of(expectedCases).stream() + .map(Case::getStringId) + .sorted() + .toList(), + result.getContent().stream() + .map(Case::getStringId) + .sorted() + .toList(), + "The search result contains unexpected case IDs" + ); + } + + private void waitForIndexedCases(List caseIds) throws InterruptedException { + long deadline = System.nanoTime() + SEARCH_TIMEOUT.toNanos(); + while (System.nanoTime() < deadline) { + if (caseIds.stream().allMatch(caseId -> elasticCaseRepository.findById(caseId).isPresent())) { + return; + } + Thread.sleep(100); + } + assertTrue(caseIds.stream().allMatch(caseId -> elasticCaseRepository.findById(caseId).isPresent()), + "The test cases were not indexed before the timeout"); + } + + private Page waitForSearchResult(List requests, + boolean intersection, + long expectedCount) throws InterruptedException { + long deadline = System.nanoTime() + SEARCH_TIMEOUT.toNanos(); + Page result; + do { + result = elasticCaseService.search( + requests, + superCreator.getLoggedSuper(), + PageRequest.of(0, 10), + Locale.ENGLISH, + intersection + ); + if (result.getTotalElements() == expectedCount) { + return result; + } + Thread.sleep(100); + } while (System.nanoTime() < deadline); + return result; + } +} diff --git a/application-engine/src/test/java/com/netgrif/application/engine/elastic/service/ElasticCaseServiceTest.java b/application-engine/src/test/java/com/netgrif/application/engine/elastic/service/ElasticCaseServiceTest.java new file mode 100644 index 0000000000..d2058e15a4 --- /dev/null +++ b/application-engine/src/test/java/com/netgrif/application/engine/elastic/service/ElasticCaseServiceTest.java @@ -0,0 +1,398 @@ +package com.netgrif.application.engine.elastic.service; + +import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery; +import co.elastic.clients.elasticsearch._types.query_dsl.Query; +import co.elastic.clients.elasticsearch._types.query_dsl.WildcardQuery; +import com.netgrif.application.engine.configuration.properties.DataConfigurationProperties; +import com.netgrif.application.engine.elastic.domain.BulkOperationWrapper; +import com.netgrif.application.engine.elastic.domain.ElasticCaseRepository; +import com.netgrif.application.engine.elastic.service.executors.Executor; +import com.netgrif.application.engine.elastic.service.interfaces.IElasticCasePrioritySearch; +import com.netgrif.application.engine.elastic.web.requestbodies.CaseSearchRequest; +import com.netgrif.application.engine.objects.auth.domain.LoggedUser; +import com.netgrif.application.engine.objects.elastic.domain.ElasticCase; +import com.netgrif.application.engine.objects.petrinet.domain.PetriNetSearch; +import com.netgrif.application.engine.petrinet.service.interfaces.IPetriNetService; +import com.netgrif.application.engine.petrinet.web.responsebodies.PetriNetReference; +import com.netgrif.application.engine.objects.workflow.domain.Case; +import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService; +import org.bson.types.ObjectId; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.elasticsearch.client.elc.ElasticsearchTemplate; +import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter; +import org.springframework.data.elasticsearch.core.document.Document; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class ElasticCaseServiceTest { + + @Test + void buildFullTextQueryIgnoresNullAndBlankInput() { + ElasticCaseService service = service(List.of("title")); + BoolQuery.Builder query = new BoolQuery.Builder(); + + service.buildFullTextQuery(new CaseSearchRequest(), query); + service.buildFullTextQuery(CaseSearchRequest.builder().fullText(" \t\n ").build(), query); + + assertTrue(query.build().must().isEmpty()); + } + + @Test + void buildFullTextQueryRequiresEveryTermInAtLeastOneConfiguredField() { + ElasticCaseService service = service(List.of("title^3", "dataSet.*.fulltextValue")); + CaseSearchRequest request = new CaseSearchRequest(Map.of("fullText", "Alpha beta")); + BoolQuery.Builder query = new BoolQuery.Builder(); + + service.buildFullTextQuery(request, query); + + BoolQuery fullTextQuery = onlyMustClause(query.build()).bool(); + assertEquals(2, fullTextQuery.must().size()); + assertTermQuery(fullTextQuery.must().get(0).bool(), "*Alpha*", 3.0f, 1.0f); + assertTermQuery(fullTextQuery.must().get(1).bool(), "*beta*", 3.0f, 1.0f); + } + + @Test + void buildFullTextQueryEscapesLiteralWildcardCharacters() { + ElasticCaseService service = service(List.of("title")); + CaseSearchRequest request = new CaseSearchRequest(Map.of("fullText", "star* question?")); + BoolQuery.Builder query = new BoolQuery.Builder(); + + service.buildFullTextQuery(request, query); + + BoolQuery fullTextQuery = onlyMustClause(query.build()).bool(); + assertEquals("*star\\**", wildcard(fullTextQuery.must().get(0).bool(), 0).value()); + assertEquals("*question\\?*", wildcard(fullTextQuery.must().get(1).bool(), 0).value()); + } + + @Test + void buildFullTextQueryUsesDefaultBoostForInvalidValues() { + ElasticCaseService service = service(List.of( + "valid^2.5", + "missing", + "empty^", + "text^invalid", + "zero^0", + "negative^-4", + "nan^NaN", + "infinity^Infinity" + )); + BoolQuery.Builder query = new BoolQuery.Builder(); + + service.buildFullTextQuery(CaseSearchRequest.builder().fullText("term").build(), query); + + BoolQuery termQuery = onlyMustClause(query.build()).bool().must().getFirst().bool(); + assertAll( + () -> assertEquals(2.5f, wildcard(termQuery, 0).boost()), + () -> assertEquals(1.0f, wildcard(termQuery, 1).boost()), + () -> assertEquals(1.0f, wildcard(termQuery, 2).boost()), + () -> assertEquals(1.0f, wildcard(termQuery, 3).boost()), + () -> assertEquals(1.0f, wildcard(termQuery, 4).boost()), + () -> assertEquals(1.0f, wildcard(termQuery, 5).boost()), + () -> assertEquals(1.0f, wildcard(termQuery, 6).boost()), + () -> assertEquals(1.0f, wildcard(termQuery, 7).boost()) + ); + } + + @Test + void buildFullTextQueryIgnoresInputContainingOnlyEscapeCharacters() { + ElasticCaseService service = service(List.of("title")); + BoolQuery.Builder query = new BoolQuery.Builder(); + + service.buildFullTextQuery(CaseSearchRequest.builder().fullText("\\").build(), query); + + assertTrue(query.build().must().isEmpty()); + } + + @Test + void buildPetriNetQueryCombinesIdentifiersAndProcessIds() { + ElasticCaseService service = service(List.of("title")); + CaseSearchRequest request = CaseSearchRequest.builder() + .process(List.of( + new CaseSearchRequest.PetriNet("invoice", null), + new CaseSearchRequest.PetriNet(null, "process-id") + )) + .build(); + BoolQuery.Builder query = new BoolQuery.Builder(); + + service.buildPetriNetQuery(request, mock(LoggedUser.class), query); + + BoolQuery processQuery = onlyFilterClause(query.build()).bool(); + assertEquals(Set.of("processIdentifier", "processId"), processQuery.should().stream() + .map(item -> item.terms().field()) + .collect(Collectors.toSet())); + } + + @Test + void buildAuthorQueryIncludesAllProvidedAuthorAttributes() { + ElasticCaseService service = service(List.of("title")); + CaseSearchRequest request = CaseSearchRequest.builder() + .author(List.of(new CaseSearchRequest.Author("id", "Name", "username", "realm"))) + .build(); + BoolQuery.Builder query = new BoolQuery.Builder(); + + service.buildAuthorQuery(request, query); + + BoolQuery authorsQuery = onlyFilterClause(query.build()).bool(); + assertEquals(1, authorsQuery.should().size()); + assertEquals(5, authorsQuery.should().getFirst().bool().must().size()); + } + + @Test + void buildFieldQueriesAddsTaskRoleDataTagIdAndUriFilters() { + ElasticCaseService service = service(List.of("title")); + CaseSearchRequest request = CaseSearchRequest.builder() + .transition(List.of("transition-1")) + .role(List.of("role-1")) + .data(Map.of("plain", "value", "nested.keyword", "nested-value")) + .tags(Map.of("key", "tag-value")) + .stringId(List.of("legacy-id")) + .id(List.of("case-id")) + .uriNodeId("uri-node") + .build(); + BoolQuery.Builder query = new BoolQuery.Builder(); + + service.buildTaskQuery(request, query); + service.buildRoleQuery(request, query); + service.buildDataQuery(request, query); + service.buildTagsQuery(request, query); + service.buildCaseIdQuery(request, query); + service.buildUriNodeIdQuery(request, query); + + List filters = query.build().filter(); + assertEquals(6, filters.size()); + assertEquals("taskIds", filters.get(0).terms().field()); + assertEquals("enabledRoles", filters.get(1).terms().field()); + assertEquals(2, filters.get(2).bool().must().size()); + assertEquals(1, filters.get(3).bool().must().size()); + assertEquals("_id", filters.get(4).terms().field()); + assertEquals("uriNodeId", filters.get(5).term().field()); + } + + @Test + void buildStringQueryReplacesCurrentUserPlaceholder() { + ElasticCaseService service = service(List.of("title")); + ObjectId userId = new ObjectId(); + LoggedUser user = mock(LoggedUser.class); + when(user.getId()).thenReturn(userId); + BoolQuery.Builder query = new BoolQuery.Builder(); + + service.buildStringQuery(CaseSearchRequest.builder().query("author:<>").build(), query, user); + + assertEquals("author:" + userId, onlyMustClause(query.build()).queryString().query()); + } + + @Test + void buildGroupQueryHandlesEmptyAndResolvedGroups() { + Fixture fixture = fixture(List.of("title")); + LoggedUser user = mock(LoggedUser.class); + CaseSearchRequest request = CaseSearchRequest.builder().group(List.of("group-1")).build(); + PetriNetReference reference = new PetriNetReference(); + reference.setIdentifier("invoice"); + when(fixture.petriNetService().search(any(PetriNetSearch.class), same(user), any(Pageable.class), eq(Locale.ENGLISH))) + .thenReturn(Page.empty()) + .thenReturn(new PageImpl<>(List.of(reference))); + + assertTrue(fixture.service().buildGroupQuery(request, user, Locale.ENGLISH, new BoolQuery.Builder())); + + BoolQuery.Builder resolvedQuery = new BoolQuery.Builder(); + assertFalse(fixture.service().buildGroupQuery(request, user, Locale.ENGLISH, resolvedQuery)); + assertEquals("processIdentifier", onlyFilterClause(resolvedQuery.build()).terms().field()); + } + + @Test + void resolveUnmappedSortAttributesPreservesPageAndSortDirection() { + ElasticCaseService service = service(List.of("title")); + Pageable original = PageRequest.of(2, 10, Sort.by( + Sort.Order.asc("title"), + Sort.Order.desc("createdDate") + )); + + Pageable resolved = service.resolveUnmappedSortAttributes(original); + + assertEquals(2, resolved.getPageNumber()); + assertEquals(10, resolved.getPageSize()); + assertTrue(resolved.getSort().getOrderFor("title").isAscending()); + assertTrue(resolved.getSort().getOrderFor("createdDate").isDescending()); + } + + @Test + void buildQuerySupportsIntersectionUnionAndBothSortDirections() { + ElasticCaseService service = spy(service(List.of("title"))); + LoggedUser user = mock(LoggedUser.class); + List requests = List.of(new CaseSearchRequest(), new CaseSearchRequest()); + Pageable pageable = PageRequest.of(1, 5, Sort.by( + Sort.Order.asc("title"), + Sort.Order.desc("createdDate") + )); + doAnswer(ignored -> new BoolQuery.Builder()).when(service) + .buildSingleQuery(any(CaseSearchRequest.class), same(user), eq(Locale.ENGLISH)); + + assertNotNull(service.buildQuery(requests, user, pageable, Locale.ENGLISH, true)); + assertNotNull(service.buildQuery(requests, user, pageable, Locale.ENGLISH, false)); + } + + @Test + void buildSingleQueryInvokesAllEmptyFilterPaths() { + ElasticCaseService service = service(List.of("title")); + LoggedUser user = loggedUser(); + + BoolQuery query = service.buildSingleQuery(new CaseSearchRequest(), user, Locale.ENGLISH).build(); + + assertEquals(1, query.filter().size()); + } + + @Test + void searchAndCountHandleEmptyQueryAndRejectNullRequests() { + ElasticCaseService service = spy(service(List.of("title"))); + LoggedUser user = loggedUser(); + Pageable pageable = PageRequest.of(0, 10); + doReturn(null).when(service).buildQuery(anyList(), same(user), any(Pageable.class), eq(Locale.ENGLISH), eq(true)); + + Page result = service.search(List.of(new CaseSearchRequest()), user, pageable, Locale.ENGLISH, true); + + assertTrue(result.isEmpty()); + assertEquals(0, service.count(List.of(new CaseSearchRequest()), user, Locale.ENGLISH, true)); + assertThrows(IllegalArgumentException.class, + () -> service.search(null, user, pageable, Locale.ENGLISH, true)); + assertThrows(IllegalArgumentException.class, + () -> service.count(null, user, Locale.ENGLISH, true)); + } + + @Test + void removeIndexIndexNowAndStopQueuesDelegateToQueueManagers() { + Fixture fixture = fixture(List.of("title")); + ElasticQueueManager indexQueue = mock(ElasticQueueManager.class); + ElasticQueueManager deleteQueue = mock(ElasticQueueManager.class); + fixture.service().caseElasticIndexQueueManager = indexQueue; + fixture.service().caseElasticDeleteQueueManager = deleteQueue; + ElasticCase useCase = mock(ElasticCase.class); + when(useCase.getId()).thenReturn("case-1"); + when(fixture.repository().findById("case-1")).thenReturn(Optional.empty()); + ElasticsearchConverter converter = mock(ElasticsearchConverter.class); + when(fixture.template().getElasticsearchConverter()).thenReturn(converter); + when(converter.mapObject(useCase)).thenReturn(Document.from(new HashMap<>())); + + fixture.service().remove("case-1"); + fixture.service().index(useCase); + fixture.service().indexNow(useCase); + ReflectionTestUtils.invokeMethod(fixture.service(), "stopQueues"); + + ArgumentCaptor deleteOperation = ArgumentCaptor.forClass(BulkOperationWrapper.class); + verify(deleteQueue).push(deleteOperation.capture()); + assertEquals("case-index", deleteOperation.getValue().getOperation().delete().index()); + assertEquals("case-1", deleteOperation.getValue().getOperation().delete().id()); + + ArgumentCaptor indexOperations = ArgumentCaptor.forClass(BulkOperationWrapper.class); + verify(indexQueue, times(2)).push(indexOperations.capture()); + assertTrue(indexOperations.getAllValues().stream() + .allMatch(operation -> "case-index".equals(operation.getOperation().index().index()))); + verify(indexQueue).shutdown(); + verify(deleteQueue).shutdown(); + } + + private void assertTermQuery(BoolQuery termQuery, String expectedValue, float firstBoost, float secondBoost) { + assertEquals("1", termQuery.minimumShouldMatch()); + assertEquals(2, termQuery.should().size()); + assertAll( + () -> assertEquals("title", wildcard(termQuery, 0).field()), + () -> assertEquals(expectedValue, wildcard(termQuery, 0).value()), + () -> assertEquals(firstBoost, wildcard(termQuery, 0).boost()), + () -> assertTrue(wildcard(termQuery, 0).caseInsensitive()), + () -> assertEquals("dataSet.*.fulltextValue", wildcard(termQuery, 1).field()), + () -> assertEquals(expectedValue, wildcard(termQuery, 1).value()), + () -> assertEquals(secondBoost, wildcard(termQuery, 1).boost()), + () -> assertTrue(wildcard(termQuery, 1).caseInsensitive()) + ); + } + + private Query onlyMustClause(BoolQuery query) { + assertEquals(1, query.must().size()); + return query.must().getFirst(); + } + + private Query onlyFilterClause(BoolQuery query) { + assertEquals(1, query.filter().size()); + return query.filter().getFirst(); + } + + private WildcardQuery wildcard(BoolQuery query, int index) { + return query.should().get(index).wildcard(); + } + + private ElasticCaseService service(List fullTextFields) { + return fixture(fullTextFields).service(); + } + + private Fixture fixture(List fullTextFields) { + IElasticCasePrioritySearch prioritySearch = mock(IElasticCasePrioritySearch.class); + when(prioritySearch.fullTextFields()).thenReturn(fullTextFields); + ElasticCaseRepository repository = mock(ElasticCaseRepository.class); + ElasticsearchTemplate template = mock(ElasticsearchTemplate.class); + IPetriNetService petriNetService = mock(IPetriNetService.class); + IWorkflowService workflowService = mock(IWorkflowService.class); + DataConfigurationProperties.ElasticsearchProperties properties = new DataConfigurationProperties.ElasticsearchProperties(); + properties.setIndex(Map.of(DataConfigurationProperties.ElasticsearchProperties.CASE_INDEX, "case-index")); + + ElasticCaseService service = new ElasticCaseService( + repository, + template, + mock(Executor.class), + properties, + petriNetService, + workflowService, + prioritySearch, + mock(ApplicationEventPublisher.class), + mock(ElasticsearchClient.class) + ); + return new Fixture(service, repository, template, petriNetService, workflowService); + } + + private LoggedUser loggedUser() { + LoggedUser user = mock(LoggedUser.class); + when(user.getId()).thenReturn(new ObjectId()); + when(user.getStringId()).thenReturn("user-id"); + when(user.getProcessRoles()).thenReturn(Set.of()); + when(user.getGroupIds()).thenReturn(Set.of()); + return user; + } + + private record Fixture(ElasticCaseService service, + ElasticCaseRepository repository, + ElasticsearchTemplate template, + IPetriNetService petriNetService, + IWorkflowService workflowService) { + } +} From 90f9147c9a8803199eb7fa1dde62282a7edaf62a Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Mon, 3 Aug 2026 14:19:04 +0200 Subject: [PATCH 19/21] Refactor `ElasticCaseService`: replace `replaceAll` with `replace` for string normalization in full-text search --- .../application/engine/elastic/service/ElasticCaseService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application-engine/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java b/application-engine/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java index 7f93512db6..5fddaa4cf7 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java @@ -557,7 +557,7 @@ private BulkOperation createIndexOperation(ElasticCase useCase) { private List normalizeFullTextSearch(String fullText) { return Arrays.stream(fullText - .replaceAll("\\\\", "") + .replace("\\\\", "") .replaceAll("\\s+", " ") .trim() .split("\\s+")) From 8f0ae29504e2767f6d8266c673d27c8c9b01f65e Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Mon, 3 Aug 2026 14:35:51 +0200 Subject: [PATCH 20/21] Deprecate and update case repository methods, add detailed documentation for method usage --- .../domain/roles/ProcessRoleRepository.java | 2 ++ .../domain/repositories/CaseRepository.java | 18 +++++++++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/application-engine/src/main/java/com/netgrif/application/engine/petrinet/domain/roles/ProcessRoleRepository.java b/application-engine/src/main/java/com/netgrif/application/engine/petrinet/domain/roles/ProcessRoleRepository.java index 19fa7e1b7a..fcaba86052 100755 --- a/application-engine/src/main/java/com/netgrif/application/engine/petrinet/domain/roles/ProcessRoleRepository.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/petrinet/domain/roles/ProcessRoleRepository.java @@ -138,6 +138,8 @@ default Optional findByCompositeId(String compositeId) { * @param networkId the short process ID * @param objectId the object ID * @return an {@link Optional} containing the found {@link ProcessRole}, if any + * + * @deprecated since 7.0.2, use {@link #findByNetworkIdentifierAndObjectId(String, ObjectId)} instead */ @Deprecated(since = "7.0.2") @Query("{ '_id.shortProcessId': ?0, '_id.objectId': ?1 }") diff --git a/application-engine/src/main/java/com/netgrif/application/engine/workflow/domain/repositories/CaseRepository.java b/application-engine/src/main/java/com/netgrif/application/engine/workflow/domain/repositories/CaseRepository.java index 75e496e411..4fdb121d90 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/workflow/domain/repositories/CaseRepository.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/workflow/domain/repositories/CaseRepository.java @@ -44,13 +44,25 @@ default Optional findById(String compositeId) { } } + + /** + * @deprecated since 7.0.2, use {@link #findByNetworkIdentifierAndObjectId(String, ObjectId)} + */ @Deprecated(since = "7.0.2") @Query("{ '_id.shortProcessId': ?0, '_id.objectId': ?1 }") - Optional findByNetworkIdAndObjectId(String ProcessId, ObjectId objectId); + Optional findByNetworkIdAndObjectId(String processId, ObjectId objectId); - @Deprecated(since = "7.0.2") + /** + * Finds a case by its network identifier and MongoDB object ID. + *

+ * This method queries cases using the shortProcessIdentifier field in the composite ID. + * + * @param processIdentifier the short process identifier (network identifier) of the case + * @param objectId the MongoDB object ID of the case + * @return an Optional containing the case if found, or empty if not found + */ @Query("{ '_id.shortProcessIdentifier': ?0, '_id.objectId': ?1 }") - Optional findByNetworkIdentifierAndObjectId(String ProcessId, ObjectId objectId); + Optional findByNetworkIdentifierAndObjectId(String processIdentifier, ObjectId objectId); @Override From 702824cf1eba5ce8a5a44b0c1003e429f1718d9e Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Mon, 3 Aug 2026 15:29:32 +0200 Subject: [PATCH 21/21] Refactor `ElasticCaseService`: use `Matcher.quoteReplacement` for safe string replacement in full-text search normalization --- .../application/engine/elastic/service/ElasticCaseService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/application-engine/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java b/application-engine/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java index 5fddaa4cf7..36251bc6ea 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java @@ -43,6 +43,7 @@ import java.util.*; import java.util.function.BinaryOperator; +import java.util.regex.Matcher; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -556,7 +557,7 @@ private BulkOperation createIndexOperation(ElasticCase useCase) { } private List normalizeFullTextSearch(String fullText) { - return Arrays.stream(fullText + return Arrays.stream(Matcher.quoteReplacement(fullText) .replace("\\\\", "") .replaceAll("\\s+", " ") .trim()