From 001f1a7684e34dd4dd673cc0e94b330a2597c8e3 Mon Sep 17 00:00:00 2001 From: chvostek Date: Wed, 9 Sep 2026 20:50:23 +0200 Subject: [PATCH 01/11] [NAE-2483] Action API Improvements - implement getI18nValue for enumeration and multichoice fields --- .../domain/dataset/EnumerationMapField.groovy | 16 ++++++++++++---- .../domain/dataset/MultichoiceMapField.groovy | 17 +++++++++++++---- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/EnumerationMapField.groovy b/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/EnumerationMapField.groovy index 127fa3b4999..246d64b018e 100644 --- a/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/EnumerationMapField.groovy +++ b/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/EnumerationMapField.groovy @@ -11,12 +11,12 @@ class EnumerationMapField extends MapOptionsField { super() } - EnumerationMapField(Map choices) { - super(choices) + EnumerationMapField(Map options) { + super(options) } - EnumerationMapField(Map choices, String defaultValue) { - super(choices) + EnumerationMapField(Map options, String defaultValue) { + super(options) this.defaultValue = defaultValue } @@ -45,6 +45,14 @@ class EnumerationMapField extends MapOptionsField { super.setDefaultValue(defaultValue) } + // todo 2483 doc + I18nString getI18nValue() { + // todo 2483 test + if (this.getValue() == null) { + return null; + } + return this.options?.get(this.getValue()) + } @Override Field clone() { diff --git a/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/MultichoiceMapField.groovy b/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/MultichoiceMapField.groovy index 26c120330ea..564b7279e3e 100644 --- a/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/MultichoiceMapField.groovy +++ b/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/MultichoiceMapField.groovy @@ -11,13 +11,13 @@ class MultichoiceMapField extends MapOptionsField() } - MultichoiceMapField(Map choices) { - super(choices) + MultichoiceMapField(Map options) { + super(options) this.defaultValue = new LinkedHashSet<>() } - MultichoiceMapField(Map choices, LinkedHashSet defaultValues) { - this(choices) + MultichoiceMapField(Map options, LinkedHashSet defaultValues) { + this(options) this.defaultValue = defaultValues } @@ -46,6 +46,15 @@ class MultichoiceMapField extends MapOptionsField getI18nValue() { + // todo 2483 test + if (this.options == null || this.options.isEmpty() || this.getValue() == null) { + return new LinkedHashSet<>() + } + return this.getValue().collect { this.options[it] } as LinkedHashSet + } + @Override Field clone() { MultichoiceMapField clone = new MultichoiceMapField() From d41475c0e9c2553ad912116813e955a1a85aea7c Mon Sep 17 00:00:00 2001 From: chvostek Date: Wed, 9 Sep 2026 22:13:19 +0200 Subject: [PATCH 02/11] [NAE-2483] Action API Improvements - rework structure of search services to include query pre-processing --- .../AbstractResourceSearchService.java | 108 ++++++++++++++++++ .../pfql/service/IResourceSearchService.java | 8 +- .../engine/pfql/service/ISearchService.java | 8 +- .../engine/pfql/service/SearchService.java | 32 +++--- .../caseresource/CaseSearchService.java | 93 +++------------ .../processresource/ProcessSearchService.java | 99 +++------------- .../taskresource/TaskSearchService.java | 89 +++------------ .../userresource/UserSearchService.java | 82 +++---------- .../pfql/service/utils/SearchUtils.java | 17 +++ 9 files changed, 213 insertions(+), 323 deletions(-) create mode 100644 src/main/java/com/netgrif/application/engine/pfql/service/AbstractResourceSearchService.java diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/AbstractResourceSearchService.java b/src/main/java/com/netgrif/application/engine/pfql/service/AbstractResourceSearchService.java new file mode 100644 index 00000000000..a7c20219171 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/pfql/service/AbstractResourceSearchService.java @@ -0,0 +1,108 @@ +package com.netgrif.application.engine.pfql.service; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; + +import static com.netgrif.application.engine.pfql.service.utils.SearchUtils.evaluateQuery; +import static com.netgrif.application.engine.pfql.service.utils.SearchUtils.formatPlaceholders; + +/** + * Abstract base class for resource search services providing shared query pre-processing. + *

+ * Handles common pre-processing steps: + *

    + *
  1. Formatter bracket substitution – fills {@code {}} placeholders with provided arguments
  2. + *
  3. PFQL prefix check/injection – implemented individually by each subclass
  4. + *
+ */ +@Slf4j +public abstract class AbstractResourceSearchService implements IResourceSearchService { + + /** + * Pre-processes the raw query string: fills {@code {}} placeholders and ensures a correct PFQL prefix. + * + * @param rawQuery the raw query string, possibly with {@code {}} placeholders + * @param args arguments to substitute into {@code {}} placeholders (in order) + * @return the fully pre-processed query string ready for evaluation + */ + protected String preProcess(String rawQuery, Object... args) { + String formatted = formatPlaceholders(rawQuery, args); + return ensurePrefix(formatted); + } + + /** + * Ensures the query string has the correct PFQL resource prefix. + * Each implementation defines which prefix is expected and how to inject it if missing. + * + * @param query the query string after placeholder substitution + * @return the query string with the correct prefix guaranteed + */ + protected abstract String ensurePrefix(String query); + + protected abstract Resource doSearchOne(QueryLangEvaluator evaluator); + + protected abstract Page doSearchAll(QueryLangEvaluator evaluator); + + protected abstract long doCount(QueryLangEvaluator evaluator); + + protected abstract boolean doExists(QueryLangEvaluator evaluator); + + // todo 2483 doc + @Override + public Resource searchOne(String queryString, Object... args) { + final String processedQuery = preProcess(queryString, args); + log.debug("Searching one with query: {}", processedQuery); + return searchOne(evaluateQuery(processedQuery)); + } + + @Override + public Resource searchOne(QueryLangEvaluator evaluator) { + checkEvaluatorNotNull(evaluator); + checkEvaluatorIsSingle(evaluator); + checkEvaluatorResourceType(evaluator); + return doSearchOne(evaluator); + } + + @Override + public Page searchAll(String queryString, Object... args) { + final String processedQuery = preProcess(queryString, args); + log.debug("Searching all with query: {}", processedQuery); + return searchAll(evaluateQuery(processedQuery)); + } + + @Override + public Page searchAll(QueryLangEvaluator evaluator) { + checkEvaluatorNotNull(evaluator); + checkEvaluatorIsMultiple(evaluator); + checkEvaluatorResourceType(evaluator); + return doSearchAll(evaluator); + } + + @Override + public long count(String queryString, Object... args) { + final String processedQuery = preProcess(queryString, args); + log.debug("Counting with query: {}", processedQuery); + return count(evaluateQuery(processedQuery)); + } + + @Override + public long count(QueryLangEvaluator evaluator) { + checkEvaluatorNotNull(evaluator); + checkEvaluatorResourceType(evaluator); + return doCount(evaluator); + } + + @Override + public boolean exists(String queryString, Object... args) { + final String processedQuery = preProcess(queryString, args); + log.debug("Checking existence with query: {}", processedQuery); + return exists(evaluateQuery(processedQuery)); + } + + @Override + public boolean exists(QueryLangEvaluator evaluator) { + checkEvaluatorNotNull(evaluator); + checkEvaluatorResourceType(evaluator); + return doExists(evaluator); + } +} diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/IResourceSearchService.java b/src/main/java/com/netgrif/application/engine/pfql/service/IResourceSearchService.java index b097588e24f..9a6d0be8a59 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/IResourceSearchService.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/IResourceSearchService.java @@ -25,16 +25,16 @@ public interface IResourceSearchService { QueryType getQueryResourceType(); - Resource searchOne(String queryString); + Resource searchOne(String queryString, Object... args); Resource searchOne(QueryLangEvaluator evaluator); - Page searchAll(String queryString); + Page searchAll(String queryString, Object... args); Page searchAll(QueryLangEvaluator evaluator); - long count(String queryString); + long count(String queryString, Object... args); long count(QueryLangEvaluator evaluator); - boolean exists(String queryString); + boolean exists(String queryString, Object... args); boolean exists(QueryLangEvaluator evaluator); /** diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/ISearchService.java b/src/main/java/com/netgrif/application/engine/pfql/service/ISearchService.java index 1ea796443f4..2e9847056c7 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/ISearchService.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/ISearchService.java @@ -1,11 +1,11 @@ package com.netgrif.application.engine.pfql.service; public interface ISearchService { - String explainQuery(String query); + String explainQuery(String query, Object... args); - Object search(String query); + Object search(String query, Object... args); - long count(String query); + long count(String query, Object... args); - boolean exists(String query); + boolean exists(String query, Object... args); } diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/SearchService.java b/src/main/java/com/netgrif/application/engine/pfql/service/SearchService.java index f9a27017013..acbddc1e769 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/SearchService.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/SearchService.java @@ -11,17 +11,17 @@ import java.util.stream.Collectors; import static com.netgrif.application.engine.pfql.service.utils.SearchUtils.evaluateQuery; +import static com.netgrif.application.engine.pfql.service.utils.SearchUtils.formatPlaceholders; @Slf4j @Service public class SearchService implements ISearchService { - private final Map> serviceRegistry; + protected final Map> serviceRegistry; public SearchService(List> services) { this.serviceRegistry = services.stream() .collect(Collectors.toMap(IResourceSearchService::getQueryResourceType, Function.identity())); - } /** @@ -31,9 +31,10 @@ public SearchService(List> services) { * @return a human-readable explanation of the query structure */ @Override - public String explainQuery(String input) { - log.debug("Explaining query: {}", input); - String explanation = SearchUtils.explainQuery(input); + public String explainQuery(String input, Object... args) { + final String processedQuery = formatPlaceholders(input, args); + log.debug("Explaining query: {}", processedQuery); + String explanation = SearchUtils.explainQuery(processedQuery); log.trace("Query explanation result: {}", explanation); return explanation; } @@ -47,9 +48,10 @@ public String explainQuery(String input) { * @return a single resource object or a page of resources depending on the query type */ @Override - public Object search(String input) { - log.debug("Executing search with query: {}", input); - QueryLangEvaluator evaluator = evaluateQuery(input); + public Object search(String input, Object... args) { + final String processedQuery = formatPlaceholders(input, args); + log.debug("Executing search with query: {}", processedQuery); + QueryLangEvaluator evaluator = evaluateQuery(processedQuery); log.trace("Evaluated query type: {}, multiple: {}", evaluator.getResourceType(), evaluator.getMultiple()); IResourceSearchService service = this.serviceRegistry.get(evaluator.getResourceType()); if (service == null) { @@ -67,9 +69,10 @@ public Object search(String input) { * @return the count of matching resources */ @Override - public long count(String input) { - log.debug("Counting resources with query: {}", input); - QueryLangEvaluator evaluator = evaluateQuery(input); + public long count(String input, Object... args) { + final String processedQuery = formatPlaceholders(input, args); + log.debug("Counting resources with query: {}", processedQuery); + QueryLangEvaluator evaluator = evaluateQuery(processedQuery); log.trace("Evaluated query type for count: {}", evaluator.getResourceType()); IResourceSearchService service = this.serviceRegistry.get(evaluator.getResourceType()); if (service == null) { @@ -87,9 +90,10 @@ public long count(String input) { * @return true if at least one matching resource exists, false otherwise */ @Override - public boolean exists(String input) { - log.debug("Checking existence with query: {}", input); - QueryLangEvaluator evaluator = evaluateQuery(input); + public boolean exists(String input, Object... args) { + final String processedQuery = formatPlaceholders(input, args); + log.debug("Checking existence with query: {}", processedQuery); + QueryLangEvaluator evaluator = evaluateQuery(processedQuery); log.trace("Evaluated query type for exists: {}", evaluator.getResourceType()); IResourceSearchService service = this.serviceRegistry.get(evaluator.getResourceType()); if (service == null) { diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/caseresource/CaseSearchService.java b/src/main/java/com/netgrif/application/engine/pfql/service/caseresource/CaseSearchService.java index 1a480043c6b..0bc4737ed43 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/caseresource/CaseSearchService.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/caseresource/CaseSearchService.java @@ -4,7 +4,7 @@ import com.netgrif.application.engine.elastic.service.interfaces.IElasticCaseService; import com.netgrif.application.engine.elastic.web.requestbodies.CaseSearchRequest; import com.netgrif.application.engine.pfql.domain.enums.QueryType; -import com.netgrif.application.engine.pfql.service.IResourceSearchService; +import com.netgrif.application.engine.pfql.service.AbstractResourceSearchService; import com.netgrif.application.engine.pfql.service.QueryLangEvaluator; import com.netgrif.application.engine.workflow.domain.Case; import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService; @@ -18,8 +18,6 @@ import java.util.List; -import static com.netgrif.application.engine.pfql.service.utils.SearchUtils.evaluateQuery; - /** * Service implementation for searching and querying Case resources. * Supports both MongoDB and Elasticsearch-based searches depending on the query configuration. @@ -28,11 +26,11 @@ @Slf4j @Service @RequiredArgsConstructor -public class CaseSearchService implements IResourceSearchService { +public class CaseSearchService extends AbstractResourceSearchService { - private final IWorkflowService workflowService; - private final IElasticCaseService elasticCaseService; - private final IUserService userService; + protected final IWorkflowService workflowService; + protected final IElasticCaseService elasticCaseService; + protected final IUserService userService; /** * Returns the query type handled by this service. @@ -45,16 +43,14 @@ public QueryType getQueryResourceType() { } /** - * Searches for a single case matching the provided query string. - * The query string is evaluated and processed before execution. - * - * @param queryString the query string to be evaluated and executed - * @return the first matching Case, or null if no match is found + // todo 2483 + * @param query the query string after placeholder substitution + * @return */ @Override - public Case searchOne(String queryString) { - log.debug("Searching for single case with query: {}", queryString); - return searchOne(evaluateQuery(queryString)); + protected String ensurePrefix(String query) { + // todo 2483 + return query; } /** @@ -66,11 +62,7 @@ public Case searchOne(String queryString) { * @throws IllegalArgumentException if the evaluator is null or configured for multiple results */ @Override - public Case searchOne(QueryLangEvaluator evaluator) { - checkEvaluatorNotNull(evaluator); - checkEvaluatorIsSingle(evaluator); - checkEvaluatorResourceType(evaluator); - + protected Case doSearchOne(QueryLangEvaluator evaluator) { log.debug("Searching for single case using {}", evaluator.getSearchWithElastic() ? "Elasticsearch" : "MongoDB"); if (evaluator.getSearchWithElastic()) { log.trace("Executing Elasticsearch query: {}", evaluator.getFullElasticQuery()); @@ -86,19 +78,6 @@ public Case searchOne(QueryLangEvaluator evaluator) { } } - /** - * Searches for all cases matching the provided query string. - * The query string is evaluated and processed before execution. - * - * @param queryString the query string to be evaluated and executed - * @return a Page containing all matching Cases - */ - @Override - public Page searchAll(String queryString) { - log.debug("Searching for all cases with query: {}", queryString); - return searchAll(evaluateQuery(queryString)); - } - /** * Searches for all cases using a pre-evaluated query evaluator. * Routes the search to either Elasticsearch or MongoDB based on the evaluator configuration. @@ -109,11 +88,7 @@ public Page searchAll(String queryString) { * @throws IllegalArgumentException if the evaluator is null or configured for single result */ @Override - public Page searchAll(QueryLangEvaluator evaluator) { - checkEvaluatorNotNull(evaluator); - checkEvaluatorIsMultiple(evaluator); - checkEvaluatorResourceType(evaluator); - + protected Page doSearchAll(QueryLangEvaluator evaluator) { log.debug("Searching for all cases using {} with pagination: page={}, size={}", evaluator.getSearchWithElastic() ? "Elasticsearch" : "MongoDB", evaluator.getPageable().getPageNumber(), evaluator.getPageable().getPageSize()); @@ -130,19 +105,6 @@ public Page searchAll(QueryLangEvaluator evaluator) { } } - /** - * Counts the number of cases matching the provided query string. - * The query string is evaluated and processed before execution. - * - * @param queryString the query string to be evaluated and executed - * @return the count of matching cases - */ - @Override - public long count(String queryString) { - log.debug("Counting cases with query: {}", queryString); - return count(evaluateQuery(queryString)); - } - /** * Counts the number of cases using a pre-evaluated query evaluator. * Routes the count operation to either Elasticsearch or MongoDB based on the evaluator configuration. @@ -152,10 +114,7 @@ public long count(String queryString) { * @throws IllegalArgumentException if the evaluator is null */ @Override - public long count(QueryLangEvaluator evaluator) { - checkEvaluatorNotNull(evaluator); - checkEvaluatorResourceType(evaluator); - + protected long doCount(QueryLangEvaluator evaluator) { log.debug("Counting cases using {}", evaluator.getSearchWithElastic() ? "Elasticsearch" : "MongoDB"); if (evaluator.getSearchWithElastic()) { log.trace("Executing Elasticsearch count query: {}", evaluator.getFullElasticQuery()); @@ -170,19 +129,6 @@ public long count(QueryLangEvaluator evaluator) { } } - /** - * Checks whether any cases exist that match the provided query string. - * The query string is evaluated and processed before execution. - * - * @param queryString the query string to be evaluated and executed - * @return true if at least one matching case exists, false otherwise - */ - @Override - public boolean exists(String queryString) { - log.debug("Checking existence of case with query: {}", queryString); - return exists(evaluateQuery(queryString)); - } - /** * Checks whether any cases exist using a pre-evaluated query evaluator. * Routes the existence check to either Elasticsearch or MongoDB based on the evaluator configuration. @@ -192,10 +138,7 @@ public boolean exists(String queryString) { * @throws IllegalArgumentException if the evaluator is null */ @Override - public boolean exists(QueryLangEvaluator evaluator) { - checkEvaluatorNotNull(evaluator); - checkEvaluatorResourceType(evaluator); - + protected boolean doExists(QueryLangEvaluator evaluator) { log.debug("Checking existence of cases using {}", evaluator.getSearchWithElastic() ? "Elasticsearch" : "MongoDB"); if (evaluator.getSearchWithElastic()) { log.trace("Executing Elasticsearch exists query: {}", evaluator.getFullElasticQuery()); @@ -210,21 +153,21 @@ public boolean exists(QueryLangEvaluator evaluator) { } } - private long countCasesElastic(String elasticQuery) { + protected long countCasesElastic(String elasticQuery) { CaseSearchRequest caseSearchRequest = new CaseSearchRequest(); caseSearchRequest.query = elasticQuery; return elasticCaseService.count(List.of(caseSearchRequest), userService.getLoggedOrSystem().transformToLoggedUser(), LocaleContextHolder.getLocale(), false); } - private Page findCasesElastic(String elasticQuery, Pageable pageable) { + protected Page findCasesElastic(String elasticQuery, Pageable pageable) { CaseSearchRequest caseSearchRequest = new CaseSearchRequest(); caseSearchRequest.query = elasticQuery; return elasticCaseService.search(List.of(caseSearchRequest), userService.getLoggedOrSystem().transformToLoggedUser(), pageable, LocaleContextHolder.getLocale(), false); } - private boolean existsCasesElastic(String elasticQuery) { + protected boolean existsCasesElastic(String elasticQuery) { return countCasesElastic(elasticQuery) > 0; } } diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/processresource/ProcessSearchService.java b/src/main/java/com/netgrif/application/engine/pfql/service/processresource/ProcessSearchService.java index 4f945338032..aae4a29f70d 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/processresource/ProcessSearchService.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/processresource/ProcessSearchService.java @@ -3,24 +3,20 @@ import com.netgrif.application.engine.petrinet.domain.PetriNet; import com.netgrif.application.engine.petrinet.service.interfaces.IPetriNetService; import com.netgrif.application.engine.pfql.domain.enums.QueryType; -import com.netgrif.application.engine.pfql.service.IResourceSearchService; +import com.netgrif.application.engine.pfql.service.AbstractResourceSearchService; import com.netgrif.application.engine.pfql.service.QueryLangEvaluator; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; -import java.util.Optional; - -import static com.netgrif.application.engine.pfql.service.utils.SearchUtils.evaluateQuery; @Slf4j @Service @RequiredArgsConstructor -public class ProcessSearchService implements IResourceSearchService { +public class ProcessSearchService extends AbstractResourceSearchService { - private final IPetriNetService petriNetService; + protected final IPetriNetService petriNetService; /** * Returns the query type handled by this service. @@ -33,20 +29,14 @@ public QueryType getQueryResourceType() { } /** - * Searches for a single process that matches the provided query string. - *

- * This method parses the query string into an evaluator and delegates to - * {@link #searchOne(QueryLangEvaluator)} for execution. - *

- * - * @param queryString the query string to be evaluated and executed - * @return the matching {@link PetriNet} process, or null if no match is found - * @throws IllegalArgumentException if the query string results in a multiple-results query + // todo 2483 + * @param query the query string after placeholder substitution + * @return */ @Override - public PetriNet searchOne(String queryString) { - log.debug("Searching for single process with query: {}", queryString); - return searchOne(evaluateQuery(queryString)); + protected String ensurePrefix(String query) { + // todo 2483 + return query; } /** @@ -62,11 +52,7 @@ public PetriNet searchOne(String queryString) { * @throws IllegalArgumentException if evaluator is null or configured for multiple results */ @Override - public PetriNet searchOne(QueryLangEvaluator evaluator) { - checkEvaluatorNotNull(evaluator); - checkEvaluatorIsSingle(evaluator); - checkEvaluatorResourceType(evaluator); - + protected PetriNet doSearchOne(QueryLangEvaluator evaluator) { // todo implement Elasticsearch search (service layer and evaluator layer) log.debug("Searching for single process using MongoDB"); @@ -76,23 +62,6 @@ public PetriNet searchOne(QueryLangEvaluator evaluator) { return result; } - /** - * Searches for all processes that match the provided query string. - *

- * This method parses the query string into an evaluator and delegates to - * {@link #searchAll(QueryLangEvaluator)} for execution. - *

- * - * @param queryString the query string to be evaluated and executed - * @return a page of matching {@link PetriNet} processes - * @throws IllegalArgumentException if the query string results in a single-result query - */ - @Override - public Page searchAll(String queryString) { - log.debug("Searching for all processes with query: {}", queryString); - return searchAll(evaluateQuery(queryString)); - } - /** * Searches for all processes using a pre-evaluated query expression. *

@@ -106,11 +75,7 @@ public Page searchAll(String queryString) { * @throws IllegalArgumentException if evaluator is null or configured for single result */ @Override - public Page searchAll(QueryLangEvaluator evaluator) { - checkEvaluatorNotNull(evaluator); - checkEvaluatorIsMultiple(evaluator); - checkEvaluatorResourceType(evaluator); - + protected Page doSearchAll(QueryLangEvaluator evaluator) { // todo implement Elasticsearch search (service layer and evaluator layer) log.debug("Searching for all processes using MongoDB"); @@ -120,22 +85,6 @@ public Page searchAll(QueryLangEvaluator evaluator) { return result; } - /** - * Counts the number of processes that match the provided query string. - *

- * This method parses the query string into an evaluator and delegates to - * {@link #count(QueryLangEvaluator)} for execution. - *

- * - * @param queryString the query string to be evaluated and executed - * @return the count of matching processes - */ - @Override - public long count(String queryString) { - log.debug("Counting processes with query: {}", queryString); - return count(evaluateQuery(queryString)); - } - /** * Counts the number of processes using a pre-evaluated query expression. *

@@ -149,10 +98,7 @@ public long count(String queryString) { * @throws IllegalArgumentException if evaluator is null */ @Override - public long count(QueryLangEvaluator evaluator) { - checkEvaluatorNotNull(evaluator); - checkEvaluatorResourceType(evaluator); - + protected long doCount(QueryLangEvaluator evaluator) { // todo implement Elasticsearch search (service layer and evaluator layer) log.debug("Counting processes using MongoDB"); @@ -162,22 +108,6 @@ public long count(QueryLangEvaluator evaluator) { return result; } - /** - * Checks whether any processes exist that match the provided query string. - *

- * This method parses the query string into an evaluator and delegates to - * {@link #exists(QueryLangEvaluator)} for execution. - *

- * - * @param queryString the query string to be evaluated and executed - * @return true if at least one matching process exists, false otherwise - */ - @Override - public boolean exists(String queryString) { - log.debug("Checking existence of process with query: {}", queryString); - return exists(evaluateQuery(queryString)); - } - /** * Checks whether any processes exist using a pre-evaluated query expression. *

@@ -192,10 +122,7 @@ public boolean exists(String queryString) { * @throws IllegalArgumentException if evaluator is null */ @Override - public boolean exists(QueryLangEvaluator evaluator) { - checkEvaluatorNotNull(evaluator); - checkEvaluatorResourceType(evaluator); - + protected boolean doExists(QueryLangEvaluator evaluator) { // todo implement Elasticsearch search (service layer and evaluator layer) log.debug("Checking existence of processes using MongoDB"); diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/taskresource/TaskSearchService.java b/src/main/java/com/netgrif/application/engine/pfql/service/taskresource/TaskSearchService.java index 4c53bba6212..be3c8e6ac6e 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/taskresource/TaskSearchService.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/taskresource/TaskSearchService.java @@ -4,7 +4,7 @@ import com.netgrif.application.engine.elastic.service.interfaces.IElasticTaskService; import com.netgrif.application.engine.elastic.web.requestbodies.ElasticTaskSearchRequest; import com.netgrif.application.engine.pfql.domain.enums.QueryType; -import com.netgrif.application.engine.pfql.service.IResourceSearchService; +import com.netgrif.application.engine.pfql.service.AbstractResourceSearchService; import com.netgrif.application.engine.pfql.service.QueryLangEvaluator; import com.netgrif.application.engine.workflow.domain.Task; import com.netgrif.application.engine.workflow.service.interfaces.ITaskService; @@ -18,8 +18,6 @@ import java.util.List; -import static com.netgrif.application.engine.pfql.service.utils.SearchUtils.evaluateQuery; - /** * Service implementation for searching Task resources using query language expressions. *

@@ -32,11 +30,11 @@ @Slf4j @Service @RequiredArgsConstructor -public class TaskSearchService implements IResourceSearchService { +public class TaskSearchService extends AbstractResourceSearchService { - private final ITaskService taskService; - private final IElasticTaskService elasticTaskService; - private final IUserService userService; + protected final ITaskService taskService; + protected final IElasticTaskService elasticTaskService; + protected final IUserService userService; /** * Returns the query type handled by this service. @@ -49,15 +47,14 @@ public QueryType getQueryResourceType() { } /** - * Searches for a single task matching the provided query string. - * - * @param queryString the query string to be evaluated and executed - * @return the matching task, or null if no task is found + // todo 2483 + * @param query the query string after placeholder substitution + * @return */ @Override - public Task searchOne(String queryString) { - log.debug("Searching for single task with query: {}", queryString); - return searchOne(evaluateQuery(queryString)); + protected String ensurePrefix(String query) { + // todo 2483 + return query; } /** @@ -73,11 +70,7 @@ public Task searchOne(String queryString) { * @throws IllegalArgumentException if evaluator is null or if the query expects multiple results */ @Override - public Task searchOne(QueryLangEvaluator evaluator) { - checkEvaluatorNotNull(evaluator); - checkEvaluatorIsSingle(evaluator); - checkEvaluatorResourceType(evaluator); - + protected Task doSearchOne(QueryLangEvaluator evaluator) { log.debug("Searching for single task using {}", evaluator.getSearchWithElastic() ? "Elasticsearch" : "MongoDB"); if (evaluator.getSearchWithElastic()) { log.trace("Executing Elasticsearch query: {}", evaluator.getFullElasticQuery()); @@ -93,18 +86,6 @@ public Task searchOne(QueryLangEvaluator evaluator) { } } - /** - * Searches for all tasks matching the provided query string. - * - * @param queryString the query string to be evaluated and executed - * @return a page of matching tasks - */ - @Override - public Page searchAll(String queryString) { - log.debug("Searching for all tasks with query: {}", queryString); - return searchAll(evaluateQuery(queryString)); - } - /** * Searches for all tasks using a pre-evaluated query expression. *

@@ -118,11 +99,7 @@ public Page searchAll(String queryString) { * @throws IllegalArgumentException if evaluator is null or if the query expects a single result */ @Override - public Page searchAll(QueryLangEvaluator evaluator) { - checkEvaluatorNotNull(evaluator); - checkEvaluatorIsMultiple(evaluator); - checkEvaluatorResourceType(evaluator); - + protected Page doSearchAll(QueryLangEvaluator evaluator) { log.debug("Searching for all tasks using {} with pagination: page={}, size={}", evaluator.getSearchWithElastic() ? "Elasticsearch" : "MongoDB", evaluator.getPageable().getPageNumber(), evaluator.getPageable().getPageSize()); @@ -139,18 +116,6 @@ public Page searchAll(QueryLangEvaluator evaluator) { } } - /** - * Counts the number of tasks matching the provided query string. - * - * @param queryString the query string to be evaluated and executed - * @return the count of matching tasks - */ - @Override - public long count(String queryString) { - log.debug("Counting tasks with query: {}", queryString); - return count(evaluateQuery(queryString)); - } - /** * Counts the number of tasks using a pre-evaluated query expression. *

@@ -163,10 +128,7 @@ public long count(String queryString) { * @throws IllegalArgumentException if evaluator is null */ @Override - public long count(QueryLangEvaluator evaluator) { - checkEvaluatorNotNull(evaluator); - checkEvaluatorResourceType(evaluator); - + protected long doCount(QueryLangEvaluator evaluator) { log.debug("Counting tasks using {}", evaluator.getSearchWithElastic() ? "Elasticsearch" : "MongoDB"); if (evaluator.getSearchWithElastic()) { log.trace("Executing Elasticsearch count query: {}", evaluator.getFullElasticQuery()); @@ -181,18 +143,6 @@ public long count(QueryLangEvaluator evaluator) { } } - /** - * Checks whether any tasks exist that match the provided query string. - * - * @param queryString the query string to be evaluated and executed - * @return true if at least one matching task exists, false otherwise - */ - @Override - public boolean exists(String queryString) { - log.debug("Checking existence of task with query: {}", queryString); - return exists(evaluateQuery(queryString)); - } - /** * Checks whether any tasks exist using a pre-evaluated query expression. *

@@ -205,10 +155,7 @@ public boolean exists(String queryString) { * @throws IllegalArgumentException if evaluator is null */ @Override - public boolean exists(QueryLangEvaluator evaluator) { - checkEvaluatorNotNull(evaluator); - checkEvaluatorResourceType(evaluator); - + protected boolean doExists(QueryLangEvaluator evaluator) { log.debug("Checking existence of tasks using {}", evaluator.getSearchWithElastic() ? "Elasticsearch" : "MongoDB"); if (evaluator.getSearchWithElastic()) { log.trace("Executing Elasticsearch exists query: {}", evaluator.getFullElasticQuery()); @@ -229,7 +176,7 @@ public boolean exists(QueryLangEvaluator evaluator) { * @param elasticQuery the Elasticsearch query string * @return the count of matching tasks */ - private long countTasksElastic(String elasticQuery) { + protected long countTasksElastic(String elasticQuery) { ElasticTaskSearchRequest taskSearchRequest = new ElasticTaskSearchRequest(); taskSearchRequest.query = elasticQuery; return elasticTaskService.count(List.of(taskSearchRequest), userService.getLoggedOrSystem().transformToLoggedUser(), @@ -243,7 +190,7 @@ private long countTasksElastic(String elasticQuery) { * @param pageable the pagination information * @return a page of matching tasks */ - private Page findTasksElastic(String elasticQuery, Pageable pageable) { + protected Page findTasksElastic(String elasticQuery, Pageable pageable) { ElasticTaskSearchRequest taskSearchRequest = new ElasticTaskSearchRequest(); taskSearchRequest.query = elasticQuery; return elasticTaskService.search(List.of(taskSearchRequest), userService.getLoggedOrSystem().transformToLoggedUser(), @@ -256,7 +203,7 @@ private Page findTasksElastic(String elasticQuery, Pageable pageable) { * @param elasticQuery the Elasticsearch query string * @return true if at least one matching task exists, false otherwise */ - private boolean existsTasksElastic(String elasticQuery) { + protected boolean existsTasksElastic(String elasticQuery) { return countTasksElastic(elasticQuery) > 0; } diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/userresource/UserSearchService.java b/src/main/java/com/netgrif/application/engine/pfql/service/userresource/UserSearchService.java index ba659c3d508..ec5c5339d4a 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/userresource/UserSearchService.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/userresource/UserSearchService.java @@ -3,6 +3,7 @@ import com.netgrif.application.engine.auth.domain.IUser; import com.netgrif.application.engine.auth.service.interfaces.IUserService; import com.netgrif.application.engine.pfql.domain.enums.QueryType; +import com.netgrif.application.engine.pfql.service.AbstractResourceSearchService; import com.netgrif.application.engine.pfql.service.IResourceSearchService; import com.netgrif.application.engine.pfql.service.QueryLangEvaluator; import lombok.RequiredArgsConstructor; @@ -10,8 +11,6 @@ import org.springframework.data.domain.Page; import org.springframework.stereotype.Service; -import static com.netgrif.application.engine.pfql.service.utils.SearchUtils.evaluateQuery; - /** * Service for searching and querying user resources using PFQL (Process Flow Query Language). *

@@ -27,9 +26,9 @@ @Slf4j @Service @RequiredArgsConstructor -public class UserSearchService implements IResourceSearchService { +public class UserSearchService extends AbstractResourceSearchService { - private final IUserService userService; + protected final IUserService userService; /** * Returns the resource type handled by this search service. @@ -42,16 +41,14 @@ public QueryType getQueryResourceType() { } /** - * Searches for a single user using a PFQL query string. - * - * @param queryString the PFQL query string to search with (e.g., "user: email == 'user@example.com'") - * @return the first user matching the query, or null if no user is found - * @throws IllegalArgumentException if the query string is invalid or evaluates to a non-USER resource type + // todo 2483 + * @param query the query string after placeholder substitution + * @return */ @Override - public IUser searchOne(String queryString) { - log.debug("Searching for single user with query: {}", queryString); - return searchOne(evaluateQuery(queryString)); + protected String ensurePrefix(String query) { + // todo 2483 + return query; } /** @@ -63,11 +60,7 @@ public IUser searchOne(String queryString) { * or has a resource type other than USER */ @Override - public IUser searchOne(QueryLangEvaluator evaluator) { - checkEvaluatorNotNull(evaluator); - checkEvaluatorIsSingle(evaluator); - checkEvaluatorResourceType(evaluator); - + protected IUser doSearchOne(QueryLangEvaluator evaluator) { log.debug("Searching for single user using MongoDB"); log.trace("Executing MongoDB query: {}", evaluator.getFullMongoQuery()); IUser result = userService.searchOne(evaluator.getFullMongoQuery()); @@ -75,19 +68,6 @@ public IUser searchOne(QueryLangEvaluator evaluator) { return result; } - /** - * Searches for all users matching a PFQL query string with pagination support. - * - * @param queryString the PFQL query string to search with (e.g., "users: email like '%@example.com'") - * @return a page of users matching the query - * @throws IllegalArgumentException if the query string is invalid or evaluates to a non-USER resource type - */ - @Override - public Page searchAll(String queryString) { - log.debug("Searching for all users with query: {}", queryString); - return searchAll(evaluateQuery(queryString)); - } - /** * Searches for all users matching a pre-evaluated query with pagination support. * @@ -97,11 +77,7 @@ public Page searchAll(String queryString) { * or has a resource type other than USER */ @Override - public Page searchAll(QueryLangEvaluator evaluator) { - checkEvaluatorNotNull(evaluator); - checkEvaluatorIsMultiple(evaluator); - checkEvaluatorResourceType(evaluator); - + protected Page doSearchAll(QueryLangEvaluator evaluator) { log.debug("Searching for all users using MongoDB with pagination: page={}, size={}", evaluator.getPageable().getPageNumber(), evaluator.getPageable().getPageSize()); log.trace("Executing MongoDB query: {}", evaluator.getFullMongoQuery()); @@ -110,19 +86,6 @@ public Page searchAll(QueryLangEvaluator evaluator) { return result; } - /** - * Counts the number of users matching a PFQL query string. - * - * @param queryString the PFQL query string to count with (e.g., "users: email like '%@example.com'") - * @return the number of users matching the query - * @throws IllegalArgumentException if the query string is invalid or evaluates to a non-USER resource type - */ - @Override - public long count(String queryString) { - log.debug("Counting users with query: {}", queryString); - return count(evaluateQuery(queryString)); - } - /** * Counts the number of users matching a pre-evaluated query. * @@ -131,10 +94,7 @@ public long count(String queryString) { * @throws IllegalArgumentException if the evaluator is null or has a resource type other than USER */ @Override - public long count(QueryLangEvaluator evaluator) { - checkEvaluatorNotNull(evaluator); - checkEvaluatorResourceType(evaluator); - + protected long doCount(QueryLangEvaluator evaluator) { log.debug("Counting users using MongoDB"); log.trace("Executing MongoDB count query: {}", evaluator.getFullMongoQuery()); long result = userService.count(evaluator.getFullMongoQuery()); @@ -142,19 +102,6 @@ public long count(QueryLangEvaluator evaluator) { return result; } - /** - * Checks if any user exists that matches a PFQL query string. - * - * @param queryString the PFQL query string to check with (e.g., "user: email == 'user@example.com'") - * @return true if at least one user matching the query exists, false otherwise - * @throws IllegalArgumentException if the query string is invalid or evaluates to a non-USER resource type - */ - @Override - public boolean exists(String queryString) { - log.debug("Checking existence of user with query: {}", queryString); - return exists(evaluateQuery(queryString)); - } - /** * Checks if any user exists that matches a pre-evaluated query. * @@ -163,10 +110,7 @@ public boolean exists(String queryString) { * @throws IllegalArgumentException if the evaluator is null or has a resource type other than USER */ @Override - public boolean exists(QueryLangEvaluator evaluator) { - checkEvaluatorNotNull(evaluator); - checkEvaluatorResourceType(evaluator); - + protected boolean doExists(QueryLangEvaluator evaluator) { log.debug("Checking existence of users using MongoDB"); log.trace("Executing MongoDB exists query: {}", evaluator.getFullMongoQuery()); boolean result = userService.exists(evaluator.getFullMongoQuery()); diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/utils/SearchUtils.java b/src/main/java/com/netgrif/application/engine/pfql/service/utils/SearchUtils.java index b838499225d..8a2a2067d5f 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/utils/SearchUtils.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/utils/SearchUtils.java @@ -452,4 +452,21 @@ public static String buildElasticQueryInRange(String attribute, String leftValue + ")"; return not ? "NOT " + query : query; } + + /** + * Fills {@code {}} placeholders in the query string with the provided arguments, in order. + * Uses {@link String#format} semantics by replacing {@code {}} with {@code %s} internally. + * + * @param query the query string with optional {@code {}} placeholders + * @param args values to substitute + * @return the query string with placeholders filled + */ + public static String formatPlaceholders(String query, Object... args) { + // todo 2483 + if (args == null || args.length == 0) { + return query; + } + String pattern = query.replace("{}", "%s"); + return String.format(pattern, args); + } } From 7640fe32637315e3721e6c9de573bace0452debe Mon Sep 17 00:00:00 2001 From: chvostek Date: Wed, 9 Sep 2026 23:06:42 +0200 Subject: [PATCH 03/11] [NAE-2483] Action API Improvements - implement prefix ensuring --- .../logic/action/ActionDelegate.groovy | 20 ++++++++++ .../AbstractResourceSearchService.java | 16 ++++---- .../caseresource/CaseSearchService.java | 14 +++++-- .../processresource/ProcessSearchService.java | 16 ++++++-- .../taskresource/TaskSearchService.java | 13 +++++-- .../userresource/UserSearchService.java | 20 ++++++---- .../pfql/service/utils/SearchUtils.java | 37 +++++++++++++++++++ .../engine/pfql/CaseSearchServiceTest.java | 17 +++++++++ .../engine/pfql/ProcessSearchServiceTest.java | 16 ++++++++ .../engine/pfql/TaskSearchServiceTest.java | 15 ++++++++ .../engine/pfql/UserSearchServiceTest.java | 15 ++++++++ 11 files changed, 175 insertions(+), 24 deletions(-) diff --git a/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy b/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy index afdbe5480a3..0b6da43b1b4 100644 --- a/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy +++ b/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy @@ -2541,6 +2541,7 @@ class ActionDelegate { *

      *     searchCase("case: processIdentifier eq 'query_test' and data.number_0.value == 3")
      *     searchCase("case: id eq '5f9b1c2d3e4f5a6b7c8d9e0f'")
+     *     searchCase("id eq '5f9b1c2d3e4f5a6b7c8d9e0f'")
      * 
* * @param query query language string starting with {@code case:} @@ -2560,6 +2561,7 @@ class ActionDelegate { *
      *     pagedSearchCases("cases: processIdentifier eq 'query_test' page 1 size 5 sort by title desc")
      *     pagedSearchCases("cases: author eq 'user@mail.com' and creationDate gt 2020-03-03")
+     *     pagedSearchCases("author eq 'user@mail.com' and creationDate gt 2020-03-03")
      * 
* * @param query query language string starting with {@code cases:} @@ -2579,6 +2581,7 @@ class ActionDelegate { *
      *     searchCases("cases: processIdentifier eq 'query_test' and data.boolean_0.value == true")
      *     searchCases("cases: title contains 'Test' sort by creationDate desc")
+     *     searchCases("title contains 'Test' sort by creationDate desc")
      * 
* * @param query query language string starting with {@code cases:} @@ -2597,6 +2600,7 @@ class ActionDelegate { *
      *     countCases("cases: processIdentifier eq 'query_test'")
      *     countCases("cases: data.boolean_0.value == true and data.text_0.value != '4'")
+     *     countCases("data.boolean_0.value == true and data.text_0.value != '4'")
      * 
* * @param query query language string starting with {@code cases:} @@ -2615,6 +2619,7 @@ class ActionDelegate { *
      *     existsCase("cases: processIdentifier eq 'query_test'")
      *     existsCase("cases: id in ('5f9b1c2d3e4f5a6b7c8d9e0f', '5f9b1c2d3e4f5a6b7c8d9e10')")
+     *     existsCase("id in ('5f9b1c2d3e4f5a6b7c8d9e0f', '5f9b1c2d3e4f5a6b7c8d9e10')")
      * 
* * @param query query language string starting with {@code cases:} @@ -2633,6 +2638,7 @@ class ActionDelegate { *
      *     searchTask("task: transitionId eq 't1' and caseId eq '5f9b1c2d3e4f5a6b7c8d9e0f'")
      *     searchTask("task: id eq '5f9b1c2d3e4f5a6b7c8d9e0f'")
+     *     searchTask("id eq '5f9b1c2d3e4f5a6b7c8d9e0f'")
      * 
* * @param query query language string starting with {@code task:} @@ -2652,6 +2658,7 @@ class ActionDelegate { *
      *     pagedSearchTasks("tasks: title eq 'test' page 0 size 10 sort by lastFinish desc")
      *     pagedSearchTasks("tasks: userId eq 'user1' and state eq enabled")
+     *     pagedSearchTasks("userId eq 'user1' and state eq enabled")
      * 
* * @param query query language string starting with {@code tasks:} @@ -2671,6 +2678,7 @@ class ActionDelegate { *
      *     searchTasks("tasks: processId eq 'my_process' and userId in ('user1', 'user2')")
      *     searchTasks("tasks: title contains 'Approve' sort by title asc")
+     *     searchTasks("title contains 'Approve' sort by title asc")
      * 
* * @param query query language string starting with {@code tasks:} @@ -2689,6 +2697,7 @@ class ActionDelegate { *
      *     countTasks("tasks: caseId eq '5f9b1c2d3e4f5a6b7c8d9e0f'")
      *     countTasks("tasks: transitionId eq 't1' and userId eq 'user1'")
+     *     countTasks("transitionId eq 't1' and userId eq 'user1'")
      * 
* * @param query query language string starting with {@code tasks:} @@ -2707,6 +2716,7 @@ class ActionDelegate { *
      *     existsTask("tasks: caseId eq '5f9b1c2d3e4f5a6b7c8d9e0f'")
      *     existsTask("tasks: transitionId eq 't1' and userId not eq 'user1'")
+     *     existsTask("transitionId eq 't1' and userId not eq 'user1'")
      * 
* * @param query query language string starting with {@code tasks:} @@ -2725,6 +2735,7 @@ class ActionDelegate { *
      *     searchProcess("process: identifier == 'query_test'")
      *     searchProcess("process: identifier eq 'my_process' and version eq 1.0.0")
+     *     searchProcess("identifier eq 'my_process' and version eq 1.0.0")
      * 
* * @param query query language string starting with {@code process:} @@ -2744,6 +2755,7 @@ class ActionDelegate { *
      *     pagedSearchProcesses("processes: identifier eq 'my_process' page 0 size 10 sort by version desc")
      *     pagedSearchProcesses("processes: version in (1.0.0 : 2.0.0)")
+     *     pagedSearchProcesses("version in (1.0.0 : 2.0.0)")
      * 
* * @param query query language string starting with {@code processes:} @@ -2763,6 +2775,7 @@ class ActionDelegate { *
      *     searchProcesses("processes: title contains 'Test' sort by identifier asc")
      *     searchProcesses("processes: identifier in ('process_a', 'process_b')")
+     *     searchProcesses("identifier in ('process_a', 'process_b')")
      * 
* * @param query query language string starting with {@code processes:} @@ -2781,6 +2794,7 @@ class ActionDelegate { *
      *     countProcesses("processes: identifier eq 'my_process'")
      *     countProcesses("processes: version gte 1.0.0")
+     *     countProcesses("version gte 1.0.0")
      * 
* * @param query query language string starting with {@code processes:} @@ -2799,6 +2813,7 @@ class ActionDelegate { *
      *     existsProcess("processes: identifier eq 'my_process'")
      *     existsProcess("processes: version eq 1.0.0")
+     *     existsProcess("version eq 1.0.0")
      * 
* * @param query query language string starting with {@code processes:} @@ -2817,6 +2832,7 @@ class ActionDelegate { *
      *     searchUser("user: email eq 'user@mail.com'")
      *     searchUser("user: name eq 'John' and surname eq 'Doe'")
+     *     searchUser("name eq 'John' and surname eq 'Doe'")
      * 
* * @param query query language string starting with {@code user:} @@ -2836,6 +2852,7 @@ class ActionDelegate { *
      *     pagedSearchUsers("users: name eq 'John' page 0 size 25 sort by surname asc")
      *     pagedSearchUsers("users: email contains '@company.com'")
+     *     pagedSearchUsers("email contains '@company.com'")
      * 
* * @param query query language string starting with {@code users:} @@ -2855,6 +2872,7 @@ class ActionDelegate { *
      *     searchUsers("users: surname eq 'Doe' sort by name asc")
      *     searchUsers("users: email in ('a@mail.com', 'b@mail.com')")
+     *     searchUsers("email in ('a@mail.com', 'b@mail.com')")
      * 
* * @param query query language string starting with {@code users:} @@ -2873,6 +2891,7 @@ class ActionDelegate { *
      *     countUsers("users: email contains '@company.com'")
      *     countUsers("users: name eq 'John'")
+     *     countUsers("name eq 'John'")
      * 
* * @param query query language string starting with {@code users:} @@ -2891,6 +2910,7 @@ class ActionDelegate { *
      *     existsUser("users: email eq 'user@mail.com'")
      *     existsUser("users: name eq 'John' and surname eq 'Doe'")
+     *     existsUser("name eq 'John' and surname eq 'Doe'")
      * 
* * @param query query language string starting with {@code users:} diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/AbstractResourceSearchService.java b/src/main/java/com/netgrif/application/engine/pfql/service/AbstractResourceSearchService.java index a7c20219171..6e2b392f397 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/AbstractResourceSearchService.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/AbstractResourceSearchService.java @@ -22,12 +22,13 @@ public abstract class AbstractResourceSearchService implements IResour * Pre-processes the raw query string: fills {@code {}} placeholders and ensures a correct PFQL prefix. * * @param rawQuery the raw query string, possibly with {@code {}} placeholders + * @param isMulti todo 2483 * @param args arguments to substitute into {@code {}} placeholders (in order) * @return the fully pre-processed query string ready for evaluation */ - protected String preProcess(String rawQuery, Object... args) { + protected String preProcess(String rawQuery, boolean isMulti, Object... args) { String formatted = formatPlaceholders(rawQuery, args); - return ensurePrefix(formatted); + return ensurePrefix(formatted, isMulti); } /** @@ -35,9 +36,10 @@ protected String preProcess(String rawQuery, Object... args) { * Each implementation defines which prefix is expected and how to inject it if missing. * * @param query the query string after placeholder substitution + * @param isMulti todo 2483 * @return the query string with the correct prefix guaranteed */ - protected abstract String ensurePrefix(String query); + protected abstract String ensurePrefix(String query, boolean isMulti); protected abstract Resource doSearchOne(QueryLangEvaluator evaluator); @@ -50,7 +52,7 @@ protected String preProcess(String rawQuery, Object... args) { // todo 2483 doc @Override public Resource searchOne(String queryString, Object... args) { - final String processedQuery = preProcess(queryString, args); + final String processedQuery = preProcess(queryString, false, args); log.debug("Searching one with query: {}", processedQuery); return searchOne(evaluateQuery(processedQuery)); } @@ -65,7 +67,7 @@ public Resource searchOne(QueryLangEvaluator evaluator) { @Override public Page searchAll(String queryString, Object... args) { - final String processedQuery = preProcess(queryString, args); + final String processedQuery = preProcess(queryString, true, args); log.debug("Searching all with query: {}", processedQuery); return searchAll(evaluateQuery(processedQuery)); } @@ -80,7 +82,7 @@ public Page searchAll(QueryLangEvaluator evaluator) { @Override public long count(String queryString, Object... args) { - final String processedQuery = preProcess(queryString, args); + final String processedQuery = preProcess(queryString, true, args); log.debug("Counting with query: {}", processedQuery); return count(evaluateQuery(processedQuery)); } @@ -94,7 +96,7 @@ public long count(QueryLangEvaluator evaluator) { @Override public boolean exists(String queryString, Object... args) { - final String processedQuery = preProcess(queryString, args); + final String processedQuery = preProcess(queryString, false, args); log.debug("Checking existence with query: {}", processedQuery); return exists(evaluateQuery(processedQuery)); } diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/caseresource/CaseSearchService.java b/src/main/java/com/netgrif/application/engine/pfql/service/caseresource/CaseSearchService.java index 0bc4737ed43..5b6a40524c4 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/caseresource/CaseSearchService.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/caseresource/CaseSearchService.java @@ -3,6 +3,7 @@ import com.netgrif.application.engine.auth.service.interfaces.IUserService; import com.netgrif.application.engine.elastic.service.interfaces.IElasticCaseService; import com.netgrif.application.engine.elastic.web.requestbodies.CaseSearchRequest; +import com.netgrif.application.engine.pfql.domain.antlr4.QueryLangParser; import com.netgrif.application.engine.pfql.domain.enums.QueryType; import com.netgrif.application.engine.pfql.service.AbstractResourceSearchService; import com.netgrif.application.engine.pfql.service.QueryLangEvaluator; @@ -18,6 +19,9 @@ import java.util.List; +import static com.netgrif.application.engine.pfql.service.utils.SearchUtils.buildResourcePrefix; +import static com.netgrif.application.engine.pfql.service.utils.SearchUtils.hasResourcePrefix; + /** * Service implementation for searching and querying Case resources. * Supports both MongoDB and Elasticsearch-based searches depending on the query configuration. @@ -28,6 +32,8 @@ @RequiredArgsConstructor public class CaseSearchService extends AbstractResourceSearchService { + protected static List allowedResourcePrefixes = List.of(QueryLangParser.CASE, QueryLangParser.CASES); + protected final IWorkflowService workflowService; protected final IElasticCaseService elasticCaseService; protected final IUserService userService; @@ -48,9 +54,11 @@ public QueryType getQueryResourceType() { * @return */ @Override - protected String ensurePrefix(String query) { - // todo 2483 - return query; + protected String ensurePrefix(String query, boolean isMulti) { + if (query == null || hasResourcePrefix(query, allowedResourcePrefixes)) { + return query; + } + return buildResourcePrefix(isMulti ? QueryLangParser.CASES : QueryLangParser.CASE) + query; } /** diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/processresource/ProcessSearchService.java b/src/main/java/com/netgrif/application/engine/pfql/service/processresource/ProcessSearchService.java index aae4a29f70d..79a0239dc8e 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/processresource/ProcessSearchService.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/processresource/ProcessSearchService.java @@ -2,6 +2,7 @@ import com.netgrif.application.engine.petrinet.domain.PetriNet; import com.netgrif.application.engine.petrinet.service.interfaces.IPetriNetService; +import com.netgrif.application.engine.pfql.domain.antlr4.QueryLangParser; import com.netgrif.application.engine.pfql.domain.enums.QueryType; import com.netgrif.application.engine.pfql.service.AbstractResourceSearchService; import com.netgrif.application.engine.pfql.service.QueryLangEvaluator; @@ -10,11 +11,18 @@ import org.springframework.data.domain.Page; import org.springframework.stereotype.Service; +import java.util.List; +import static com.netgrif.application.engine.pfql.service.utils.SearchUtils.buildResourcePrefix; +import static com.netgrif.application.engine.pfql.service.utils.SearchUtils.hasResourcePrefix; + + +// todo 2483 doc @Slf4j @Service @RequiredArgsConstructor public class ProcessSearchService extends AbstractResourceSearchService { + protected static List allowedResourcePrefixes = List.of(QueryLangParser.PROCESS, QueryLangParser.PROCESSES); protected final IPetriNetService petriNetService; @@ -34,9 +42,11 @@ public QueryType getQueryResourceType() { * @return */ @Override - protected String ensurePrefix(String query) { - // todo 2483 - return query; + protected String ensurePrefix(String query, boolean isMulti) { + if (query == null || hasResourcePrefix(query, allowedResourcePrefixes)) { + return query; + } + return buildResourcePrefix(isMulti ? QueryLangParser.PROCESSES : QueryLangParser.PROCESS) + query; } /** diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/taskresource/TaskSearchService.java b/src/main/java/com/netgrif/application/engine/pfql/service/taskresource/TaskSearchService.java index be3c8e6ac6e..833b7f5bee7 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/taskresource/TaskSearchService.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/taskresource/TaskSearchService.java @@ -3,6 +3,7 @@ import com.netgrif.application.engine.auth.service.interfaces.IUserService; import com.netgrif.application.engine.elastic.service.interfaces.IElasticTaskService; import com.netgrif.application.engine.elastic.web.requestbodies.ElasticTaskSearchRequest; +import com.netgrif.application.engine.pfql.domain.antlr4.QueryLangParser; import com.netgrif.application.engine.pfql.domain.enums.QueryType; import com.netgrif.application.engine.pfql.service.AbstractResourceSearchService; import com.netgrif.application.engine.pfql.service.QueryLangEvaluator; @@ -18,6 +19,9 @@ import java.util.List; +import static com.netgrif.application.engine.pfql.service.utils.SearchUtils.buildResourcePrefix; +import static com.netgrif.application.engine.pfql.service.utils.SearchUtils.hasResourcePrefix; + /** * Service implementation for searching Task resources using query language expressions. *

@@ -31,6 +35,7 @@ @Service @RequiredArgsConstructor public class TaskSearchService extends AbstractResourceSearchService { + protected static List allowedResourcePrefixes = List.of(QueryLangParser.TASK, QueryLangParser.TASKS); protected final ITaskService taskService; protected final IElasticTaskService elasticTaskService; @@ -52,9 +57,11 @@ public QueryType getQueryResourceType() { * @return */ @Override - protected String ensurePrefix(String query) { - // todo 2483 - return query; + protected String ensurePrefix(String query, boolean isMulti) { + if (query == null || hasResourcePrefix(query, allowedResourcePrefixes)) { + return query; + } + return buildResourcePrefix(isMulti ? QueryLangParser.TASKS : QueryLangParser.TASK) + query; } /** diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/userresource/UserSearchService.java b/src/main/java/com/netgrif/application/engine/pfql/service/userresource/UserSearchService.java index ec5c5339d4a..de635e799dc 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/userresource/UserSearchService.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/userresource/UserSearchService.java @@ -2,15 +2,20 @@ import com.netgrif.application.engine.auth.domain.IUser; import com.netgrif.application.engine.auth.service.interfaces.IUserService; +import com.netgrif.application.engine.pfql.domain.antlr4.QueryLangParser; import com.netgrif.application.engine.pfql.domain.enums.QueryType; import com.netgrif.application.engine.pfql.service.AbstractResourceSearchService; -import com.netgrif.application.engine.pfql.service.IResourceSearchService; import com.netgrif.application.engine.pfql.service.QueryLangEvaluator; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; import org.springframework.stereotype.Service; +import java.util.List; + +import static com.netgrif.application.engine.pfql.service.utils.SearchUtils.buildResourcePrefix; +import static com.netgrif.application.engine.pfql.service.utils.SearchUtils.hasResourcePrefix; + /** * Service for searching and querying user resources using PFQL (Process Flow Query Language). *

@@ -18,15 +23,12 @@ * based on PFQL query strings or evaluated query objects. It delegates the actual MongoDB * queries to the {@link IUserService}. *

- * - * @see IResourceSearchService - * @see IUserService - * @see QueryLangEvaluator */ @Slf4j @Service @RequiredArgsConstructor public class UserSearchService extends AbstractResourceSearchService { + protected static List allowedResourcePrefixes = List.of(QueryLangParser.USER, QueryLangParser.USERS); protected final IUserService userService; @@ -46,9 +48,11 @@ public QueryType getQueryResourceType() { * @return */ @Override - protected String ensurePrefix(String query) { - // todo 2483 - return query; + protected String ensurePrefix(String query, boolean isMulti) { + if (query == null || hasResourcePrefix(query, allowedResourcePrefixes)) { + return query; + } + return buildResourcePrefix(isMulti ? QueryLangParser.USERS : QueryLangParser.USER) + query; } /** diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/utils/SearchUtils.java b/src/main/java/com/netgrif/application/engine/pfql/service/utils/SearchUtils.java index 8a2a2067d5f..8f3b711b814 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/utils/SearchUtils.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/utils/SearchUtils.java @@ -18,6 +18,7 @@ import com.querydsl.core.types.dsl.DateTimePath; import com.querydsl.core.types.dsl.StringPath; import lombok.extern.slf4j.Slf4j; +import org.antlr.v4.runtime.CharStream; import org.antlr.v4.runtime.CharStreams; import org.antlr.v4.runtime.CommonTokenStream; import org.antlr.v4.runtime.Token; @@ -39,6 +40,8 @@ public class SearchUtils { public static final List validQueryResourcePrefixes = List.of("case", "cases", "task", "tasks", "process", "processes", "user", "users"); + private static final String QUERY_DELIMITER = ": "; + public static final Map> comparisonOperators = Map.of( ComparisonType.ID, List.of(QueryLangParser.EQ, QueryLangParser.NEQ, QueryLangParser.IN), ComparisonType.STRING, List.of(QueryLangParser.EQ, QueryLangParser.NEQ, QueryLangParser.CONTAINS, QueryLangParser.LT, QueryLangParser.LTE, QueryLangParser.GT, QueryLangParser.GTE), @@ -469,4 +472,38 @@ public static String formatPlaceholders(String query, Object... args) { String pattern = query.replace("{}", "%s"); return String.format(pattern, args); } + + // todo 2483 + public static boolean hasResourcePrefix(String query, List expectedTokenTypes) { + CharStream input = CharStreams.fromString(query.trim()); + QueryLangLexer lexer = new QueryLangLexer(input); + lexer.removeErrorListeners(); + Token firstToken = lexer.nextToken(); + return expectedTokenTypes.contains(firstToken.getType()); + } + + /** + * Builds a canonical PFQL prefix string (resource keyword + delimiter). + * The keyword text is derived from the grammar via the lexer. + * The delimiter form ({@value QUERY_DELIMITER}) corresponds to the + * {@code SPACE? ':' SPACE} alternative of the {@code delimeter} rule. + * + * @param singularTokenType the singular resource token type (e.g. {@link QueryLangParser#CASE}) + * @return the canonical prefix string (e.g. {@code "case: "}) + */ + public static String buildResourcePrefix(int singularTokenType) { + String symbolicName = QueryLangParser.VOCABULARY.getSymbolicName(singularTokenType); + if (symbolicName == null) { + throw new IllegalArgumentException("Unknown token type: " + singularTokenType); + } + CharStream input = CharStreams.fromString(symbolicName.toLowerCase()); + QueryLangLexer lexer = new QueryLangLexer(input); + lexer.removeErrorListeners(); + Token token = lexer.nextToken(); + if (token.getType() != singularTokenType) { + throw new IllegalArgumentException( + "Symbolic name '" + symbolicName + "' does not tokenize to expected type " + singularTokenType); + } + return token.getText() + QUERY_DELIMITER; + } } diff --git a/src/test/java/com/netgrif/application/engine/pfql/CaseSearchServiceTest.java b/src/test/java/com/netgrif/application/engine/pfql/CaseSearchServiceTest.java index 6d34428a50a..83591671e35 100644 --- a/src/test/java/com/netgrif/application/engine/pfql/CaseSearchServiceTest.java +++ b/src/test/java/com/netgrif/application/engine/pfql/CaseSearchServiceTest.java @@ -71,6 +71,11 @@ public void searchOneTest() throws InterruptedException { assertNotNull(result.getPetriNet()); assertEquals(testCase.getStringId(), result.getStringId()); + result = caseSearchService.searchOne("title eq 'test'"); + assertNotNull(result); + assertNotNull(result.getPetriNet()); + assertEquals(testCase.getStringId(), result.getStringId()); + login(mockService.mockLoggedUser()); result = caseSearchService.searchOne("case: title eq 'test'"); assertNull(result); @@ -99,6 +104,12 @@ public void searchAllTest() throws InterruptedException { assertNotNull(result.getContent().get(0).getPetriNet()); assertEquals(testCase.getStringId(), result.getContent().get(0).getStringId()); + result = caseSearchService.searchAll("title eq 'test'"); + assertNotNull(result); + assertEquals(1, result.getTotalElements()); + assertNotNull(result.getContent().get(0).getPetriNet()); + assertEquals(testCase.getStringId(), result.getContent().get(0).getStringId()); + login(mockService.mockLoggedUser()); result = caseSearchService.searchAll("cases: title eq 'test'"); assertNotNull(result); @@ -126,6 +137,9 @@ public void countTest() throws InterruptedException { long result = caseSearchService.count("case: title eq 'test'"); assertEquals(1, result); + result = caseSearchService.count("title eq 'test'"); + assertEquals(1, result); + login(mockService.mockLoggedUser()); result = caseSearchService.count("case: title eq 'test'"); assertEquals(0, result); @@ -151,6 +165,9 @@ public void existsTest() throws InterruptedException { boolean result = caseSearchService.exists("case: title eq 'test'"); assertTrue(result); + result = caseSearchService.exists("title eq 'test'"); + assertTrue(result); + login(mockService.mockLoggedUser()); result = caseSearchService.exists("case: title eq 'test'"); assertFalse(result); diff --git a/src/test/java/com/netgrif/application/engine/pfql/ProcessSearchServiceTest.java b/src/test/java/com/netgrif/application/engine/pfql/ProcessSearchServiceTest.java index 44fefe6f187..9616cf36d3c 100644 --- a/src/test/java/com/netgrif/application/engine/pfql/ProcessSearchServiceTest.java +++ b/src/test/java/com/netgrif/application/engine/pfql/ProcessSearchServiceTest.java @@ -57,6 +57,10 @@ public void searchOneTest() { assertNotNull(result); assertEquals(testNet.getStringId(), result.getStringId()); + result = processSearchService.searchOne("identifier eq 'query_lang_test'"); + assertNotNull(result); + assertEquals(testNet.getStringId(), result.getStringId()); + result = processSearchService.searchOne("process: identifier eq 'wrong'"); assertNull(result); } @@ -73,6 +77,12 @@ public void searchAllTest() { assertEquals(20, result.getPageable().getPageSize()); assertEquals(testNet.getStringId(), result.getContent().get(0).getStringId()); + result = processSearchService.searchAll("identifier eq 'query_lang_test'"); + assertNotNull(result); + assertEquals(1, result.getTotalElements()); + assertEquals(20, result.getPageable().getPageSize()); + assertEquals(testNet.getStringId(), result.getContent().get(0).getStringId()); + result = processSearchService.searchAll("processes: identifier eq 'query_lang_test' page 0 size 67"); assertNotNull(result); assertEquals(67, result.getPageable().getPageSize()); @@ -90,6 +100,9 @@ public void countTest() { long result = processSearchService.count("process: identifier eq 'query_lang_test'"); assertEquals(1, result); + result = processSearchService.count("identifier eq 'query_lang_test'"); + assertEquals(1, result); + result = processSearchService.count("processes: identifier eq 'query_lang_test'"); assertEquals(1, result); @@ -108,6 +121,9 @@ public void existsTest() { boolean result = processSearchService.exists("process: identifier eq 'query_lang_test'"); assertTrue(result); + result = processSearchService.exists("identifier eq 'query_lang_test'"); + assertTrue(result); + result = processSearchService.exists("processes: identifier eq 'query_lang_test'"); assertTrue(result); diff --git a/src/test/java/com/netgrif/application/engine/pfql/TaskSearchServiceTest.java b/src/test/java/com/netgrif/application/engine/pfql/TaskSearchServiceTest.java index 3166f9329df..e391bea1b11 100644 --- a/src/test/java/com/netgrif/application/engine/pfql/TaskSearchServiceTest.java +++ b/src/test/java/com/netgrif/application/engine/pfql/TaskSearchServiceTest.java @@ -73,6 +73,10 @@ public void searchOneTest() { assertNotNull(result); assertEquals(testTaskId, result.getStringId()); + result = taskSearchService.searchOne("id eq '" + testTaskId + "'"); + assertNotNull(result); + assertEquals(testTaskId, result.getStringId()); + login(mockService.mockLoggedUser()); result = taskSearchService.searchOne("task: id eq '" + testTaskId + "'"); assertNull(result); @@ -93,6 +97,11 @@ public void searchAllTest() { assertEquals(1, result.getTotalElements()); assertEquals(testTaskId, result.getContent().get(0).getStringId()); + result = taskSearchService.searchAll("id eq '" + testTaskId + "'"); + assertNotNull(result); + assertEquals(1, result.getTotalElements()); + assertEquals(testTaskId, result.getContent().get(0).getStringId()); + login(mockService.mockLoggedUser()); result = taskSearchService.searchAll("tasks: id eq '" + testTaskId + "'"); assertNotNull(result); @@ -112,6 +121,9 @@ public void countTest() { long result = taskSearchService.count("task: id eq '" + testTaskId + "'"); assertEquals(1, result); + result = taskSearchService.count("id eq '" + testTaskId + "'"); + assertEquals(1, result); + login(mockService.mockLoggedUser()); result = taskSearchService.count("task: id eq '" + testTaskId + "'"); assertEquals(0, result); @@ -132,6 +144,9 @@ public void existsTest() { boolean result = taskSearchService.exists("task: id eq '" + testTaskId + "'"); assertTrue(result); + result = taskSearchService.exists("id eq '" + testTaskId + "'"); + assertTrue(result); + login(mockService.mockLoggedUser()); result = taskSearchService.exists("task: id eq '" + testTaskId + "'"); assertFalse(result); diff --git a/src/test/java/com/netgrif/application/engine/pfql/UserSearchServiceTest.java b/src/test/java/com/netgrif/application/engine/pfql/UserSearchServiceTest.java index 4f3c0a9ec41..19501e56665 100644 --- a/src/test/java/com/netgrif/application/engine/pfql/UserSearchServiceTest.java +++ b/src/test/java/com/netgrif/application/engine/pfql/UserSearchServiceTest.java @@ -50,6 +50,10 @@ public void searchOneTest() { assertNotNull(result); assertEquals(superCreator.getSuperUser().getStringId(), result.getStringId()); + result = userSearchService.searchOne("email eq '" + superCreator.getSuperUser().getEmail() + "'"); + assertNotNull(result); + assertEquals(superCreator.getSuperUser().getStringId(), result.getStringId()); + result = userSearchService.searchOne("user: email eq 'wrong'"); assertNull(result); } @@ -65,6 +69,11 @@ public void searchAllTest() { assertEquals(1, result.getTotalElements()); assertEquals(superCreator.getSuperUser().getStringId(), result.getContent().get(0).getStringId()); + result = userSearchService.searchAll("email eq '" + superCreator.getSuperUser().getEmail() + "'"); + assertNotNull(result); + assertEquals(1, result.getTotalElements()); + assertEquals(superCreator.getSuperUser().getStringId(), result.getContent().get(0).getStringId()); + result = userSearchService.searchAll("users: email eq 'wrong'"); assertNotNull(result); assertEquals(0, result.getTotalElements()); @@ -78,6 +87,9 @@ public void countTest() { long result = userSearchService.count("users: email eq '" + superCreator.getSuperUser().getEmail() + "'"); assertEquals(1, result); + result = userSearchService.count("email eq '" + superCreator.getSuperUser().getEmail() + "'"); + assertEquals(1, result); + result = userSearchService.count("users: email eq 'wrong'"); assertEquals(0, result); } @@ -90,6 +102,9 @@ public void existsTest() { boolean result = userSearchService.exists("users: email eq '" + superCreator.getSuperUser().getEmail() + "'"); assertTrue(result); + result = userSearchService.exists("email eq '" + superCreator.getSuperUser().getEmail() + "'"); + assertTrue(result); + result = userSearchService.exists("users: email eq 'wrong'"); assertFalse(result); } From 01d6bd70e9e9c0a702fd6d523c150bba9a0ab7ca Mon Sep 17 00:00:00 2001 From: chvostek Date: Thu, 10 Sep 2026 20:46:15 +0200 Subject: [PATCH 04/11] [NAE-2483] Action API Improvements - implement PFQL formatters - implement tests --- .../AbstractResourceSearchService.java | 7 +- .../engine/pfql/service/SearchService.java | 13 +- .../caseresource/CaseSearchService.java | 11 +- .../BooleanPlaceholderFormatter.java | 15 + .../CaseRefPlaceholderFormatter.java | 25 + .../DateListPlaceholderFormatter.java | 25 + .../formatters/DatePlaceholderFormatter.java | 43 ++ .../DateTimeListPlaceholderFormatter.java | 25 + .../DateTimePlaceholderFormatter.java | 64 ++ .../NumberListPlaceholderFormatter.java | 25 + .../NumberPlaceholderFormatter.java | 15 + .../ObjectIdListPlaceholderFormatter.java | 25 + .../ObjectIdPlaceholderFormatter.java | 17 + .../QueryLangPlaceholderFormatter.java | 21 + .../QueryLangPlaceholderHandler.java | 37 ++ .../StringListPlaceholderFormatter.java | 25 + .../StringPlaceholderFormatter.java | 15 + .../TaskRefPlaceholderFormatter.java | 25 + .../processresource/ProcessSearchService.java | 8 +- .../taskresource/TaskSearchService.java | 11 +- .../userresource/UserSearchService.java | 8 +- .../pfql/service/utils/SearchUtils.java | 21 +- .../engine/pfql/CaseSearchServiceTest.java | 16 + .../QueryLangPlaceholderHandlerTest.java | 609 ++++++++++++++++++ 24 files changed, 1088 insertions(+), 18 deletions(-) create mode 100644 src/main/java/com/netgrif/application/engine/pfql/service/formatters/BooleanPlaceholderFormatter.java create mode 100644 src/main/java/com/netgrif/application/engine/pfql/service/formatters/CaseRefPlaceholderFormatter.java create mode 100644 src/main/java/com/netgrif/application/engine/pfql/service/formatters/DateListPlaceholderFormatter.java create mode 100644 src/main/java/com/netgrif/application/engine/pfql/service/formatters/DatePlaceholderFormatter.java create mode 100644 src/main/java/com/netgrif/application/engine/pfql/service/formatters/DateTimeListPlaceholderFormatter.java create mode 100644 src/main/java/com/netgrif/application/engine/pfql/service/formatters/DateTimePlaceholderFormatter.java create mode 100644 src/main/java/com/netgrif/application/engine/pfql/service/formatters/NumberListPlaceholderFormatter.java create mode 100644 src/main/java/com/netgrif/application/engine/pfql/service/formatters/NumberPlaceholderFormatter.java create mode 100644 src/main/java/com/netgrif/application/engine/pfql/service/formatters/ObjectIdListPlaceholderFormatter.java create mode 100644 src/main/java/com/netgrif/application/engine/pfql/service/formatters/ObjectIdPlaceholderFormatter.java create mode 100644 src/main/java/com/netgrif/application/engine/pfql/service/formatters/QueryLangPlaceholderFormatter.java create mode 100644 src/main/java/com/netgrif/application/engine/pfql/service/formatters/QueryLangPlaceholderHandler.java create mode 100644 src/main/java/com/netgrif/application/engine/pfql/service/formatters/StringListPlaceholderFormatter.java create mode 100644 src/main/java/com/netgrif/application/engine/pfql/service/formatters/StringPlaceholderFormatter.java create mode 100644 src/main/java/com/netgrif/application/engine/pfql/service/formatters/TaskRefPlaceholderFormatter.java create mode 100644 src/test/java/com/netgrif/application/engine/pfql/formatters/QueryLangPlaceholderHandlerTest.java diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/AbstractResourceSearchService.java b/src/main/java/com/netgrif/application/engine/pfql/service/AbstractResourceSearchService.java index 6e2b392f397..254c71559c8 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/AbstractResourceSearchService.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/AbstractResourceSearchService.java @@ -1,5 +1,7 @@ package com.netgrif.application.engine.pfql.service; +import com.netgrif.application.engine.pfql.service.formatters.QueryLangPlaceholderHandler; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; @@ -16,8 +18,11 @@ * */ @Slf4j +@RequiredArgsConstructor public abstract class AbstractResourceSearchService implements IResourceSearchService { + protected final QueryLangPlaceholderHandler placeholderHandler; + /** * Pre-processes the raw query string: fills {@code {}} placeholders and ensures a correct PFQL prefix. * @@ -27,7 +32,7 @@ public abstract class AbstractResourceSearchService implements IResour * @return the fully pre-processed query string ready for evaluation */ protected String preProcess(String rawQuery, boolean isMulti, Object... args) { - String formatted = formatPlaceholders(rawQuery, args); + String formatted = formatPlaceholders(rawQuery, placeholderHandler, args); return ensurePrefix(formatted, isMulti); } diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/SearchService.java b/src/main/java/com/netgrif/application/engine/pfql/service/SearchService.java index acbddc1e769..0ef4f71c342 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/SearchService.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/SearchService.java @@ -1,6 +1,7 @@ package com.netgrif.application.engine.pfql.service; import com.netgrif.application.engine.pfql.domain.enums.QueryType; +import com.netgrif.application.engine.pfql.service.formatters.QueryLangPlaceholderHandler; import com.netgrif.application.engine.pfql.service.utils.SearchUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -17,9 +18,11 @@ @Service public class SearchService implements ISearchService { + protected final QueryLangPlaceholderHandler placeholderHandler; protected final Map> serviceRegistry; - public SearchService(List> services) { + public SearchService(QueryLangPlaceholderHandler placeholderHandler, List> services) { + this.placeholderHandler = placeholderHandler; this.serviceRegistry = services.stream() .collect(Collectors.toMap(IResourceSearchService::getQueryResourceType, Function.identity())); } @@ -32,7 +35,7 @@ public SearchService(List> services) { */ @Override public String explainQuery(String input, Object... args) { - final String processedQuery = formatPlaceholders(input, args); + final String processedQuery = formatPlaceholders(input, placeholderHandler, args); log.debug("Explaining query: {}", processedQuery); String explanation = SearchUtils.explainQuery(processedQuery); log.trace("Query explanation result: {}", explanation); @@ -49,7 +52,7 @@ public String explainQuery(String input, Object... args) { */ @Override public Object search(String input, Object... args) { - final String processedQuery = formatPlaceholders(input, args); + final String processedQuery = formatPlaceholders(input, placeholderHandler, args); log.debug("Executing search with query: {}", processedQuery); QueryLangEvaluator evaluator = evaluateQuery(processedQuery); log.trace("Evaluated query type: {}, multiple: {}", evaluator.getResourceType(), evaluator.getMultiple()); @@ -70,7 +73,7 @@ public Object search(String input, Object... args) { */ @Override public long count(String input, Object... args) { - final String processedQuery = formatPlaceholders(input, args); + final String processedQuery = formatPlaceholders(input, placeholderHandler, args); log.debug("Counting resources with query: {}", processedQuery); QueryLangEvaluator evaluator = evaluateQuery(processedQuery); log.trace("Evaluated query type for count: {}", evaluator.getResourceType()); @@ -91,7 +94,7 @@ public long count(String input, Object... args) { */ @Override public boolean exists(String input, Object... args) { - final String processedQuery = formatPlaceholders(input, args); + final String processedQuery = formatPlaceholders(input, placeholderHandler, args); log.debug("Checking existence with query: {}", processedQuery); QueryLangEvaluator evaluator = evaluateQuery(processedQuery); log.trace("Evaluated query type for exists: {}", evaluator.getResourceType()); diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/caseresource/CaseSearchService.java b/src/main/java/com/netgrif/application/engine/pfql/service/caseresource/CaseSearchService.java index 5b6a40524c4..b8bda9eb2fd 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/caseresource/CaseSearchService.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/caseresource/CaseSearchService.java @@ -7,9 +7,9 @@ import com.netgrif.application.engine.pfql.domain.enums.QueryType; import com.netgrif.application.engine.pfql.service.AbstractResourceSearchService; import com.netgrif.application.engine.pfql.service.QueryLangEvaluator; +import com.netgrif.application.engine.pfql.service.formatters.QueryLangPlaceholderHandler; import com.netgrif.application.engine.workflow.domain.Case; import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.data.domain.Page; @@ -29,7 +29,6 @@ */ @Slf4j @Service -@RequiredArgsConstructor public class CaseSearchService extends AbstractResourceSearchService { protected static List allowedResourcePrefixes = List.of(QueryLangParser.CASE, QueryLangParser.CASES); @@ -38,6 +37,14 @@ public class CaseSearchService extends AbstractResourceSearchService { protected final IElasticCaseService elasticCaseService; protected final IUserService userService; + public CaseSearchService(QueryLangPlaceholderHandler placeholderHandler, IWorkflowService workflowService, + IElasticCaseService elasticCaseService, IUserService userService) { + super(placeholderHandler); + this.workflowService = workflowService; + this.elasticCaseService = elasticCaseService; + this.userService = userService; + } + /** * Returns the query type handled by this service. * diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/BooleanPlaceholderFormatter.java b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/BooleanPlaceholderFormatter.java new file mode 100644 index 00000000000..3a27cefe6c1 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/BooleanPlaceholderFormatter.java @@ -0,0 +1,15 @@ +package com.netgrif.application.engine.pfql.service.formatters; + +// todo 2483 doc +public class BooleanPlaceholderFormatter implements QueryLangPlaceholderFormatter { + + @Override + public boolean supports(Object value) { + return value instanceof Boolean; + } + + @Override + public String format(Object value) { + return String.valueOf(value); + } +} diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/CaseRefPlaceholderFormatter.java b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/CaseRefPlaceholderFormatter.java new file mode 100644 index 00000000000..52a1444f7af --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/CaseRefPlaceholderFormatter.java @@ -0,0 +1,25 @@ +package com.netgrif.application.engine.pfql.service.formatters; + +import com.netgrif.application.engine.petrinet.domain.dataset.CaseField; + +import java.util.stream.Collectors; + +// todo 2483 doc +public class CaseRefPlaceholderFormatter implements QueryLangPlaceholderFormatter { + + @Override + public boolean supports(Object value) { + return value instanceof CaseField; + } + + @Override + public String format(Object value) { + CaseField field = (CaseField) value; + if (field.getValue() == null) { + return ""; + } + return wrapInBrackets(field.getValue().stream() + .map(this::wrapInSingleQuotes) + .collect(Collectors.joining(", "))); + } +} diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/DateListPlaceholderFormatter.java b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/DateListPlaceholderFormatter.java new file mode 100644 index 00000000000..7c290d71f53 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/DateListPlaceholderFormatter.java @@ -0,0 +1,25 @@ +package com.netgrif.application.engine.pfql.service.formatters; + +import java.util.Collection; +import java.util.stream.Collectors; + +// todo 2483 doc +public class DateListPlaceholderFormatter extends DatePlaceholderFormatter { + + @Override + @SuppressWarnings("Convert2MethodRef") // method reference does not work to super calls + public boolean supports(Object value) { + return value instanceof Collection + && !((Collection) value).isEmpty() + && ((Collection) value).stream().allMatch(item -> super.supports(item)); + } + + @Override + @SuppressWarnings("Convert2MethodRef") // method reference does not work to super calls + public String format(Object value) { + Collection collOfDates = (Collection) value; + return wrapInBrackets(collOfDates.stream() + .map(item -> super.format(item)) + .collect(Collectors.joining(", "))); + } +} diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/DatePlaceholderFormatter.java b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/DatePlaceholderFormatter.java new file mode 100644 index 00000000000..fba2248bb15 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/DatePlaceholderFormatter.java @@ -0,0 +1,43 @@ +package com.netgrif.application.engine.pfql.service.formatters; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.regex.Pattern; + +// todo 2483 doc +public class DatePlaceholderFormatter implements QueryLangPlaceholderFormatter { + + /** + * Regex pattern derived from the {@code DATE} token defined in {@code QueryLang.g4}: + *
+     * DATE: DIGIT DIGIT DIGIT DIGIT '-' ('0' [1-9] | '1' [0-2]) '-' ('0' [1-9] | [12] DIGIT | '3' [01])
+     * 
+ * Example: {@code 2020-03-03} + *

+ * Note: If the grammar changes, this pattern must be updated accordingly. + *

+ */ + protected static final Pattern DATE_PATTERN = Pattern.compile("\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])"); + protected static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + @Override + public boolean supports(Object value) { + return isOfLocalDateType(value) || isOfStringType(value); + } + + @Override + public String format(Object value) { + if (isOfLocalDateType(value)) { + return ((LocalDate) value).format(DATE_FORMATTER); + } + return (String) value; + } + + protected boolean isOfStringType(Object value) { + return value instanceof String && DATE_PATTERN.matcher((String) value).matches(); + } + + protected boolean isOfLocalDateType(Object value) { + return value instanceof LocalDate; + } +} diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/DateTimeListPlaceholderFormatter.java b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/DateTimeListPlaceholderFormatter.java new file mode 100644 index 00000000000..0c7d4f70ce1 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/DateTimeListPlaceholderFormatter.java @@ -0,0 +1,25 @@ +package com.netgrif.application.engine.pfql.service.formatters; + +import java.util.Collection; +import java.util.stream.Collectors; + +// todo 2483 doc +public class DateTimeListPlaceholderFormatter extends DateTimePlaceholderFormatter { + + @Override + @SuppressWarnings("Convert2MethodRef") // method reference does not work to super calls + public boolean supports(Object value) { + return value instanceof Collection + && !((Collection) value).isEmpty() + && ((Collection) value).stream().allMatch(item -> super.supports(item)); + } + + @Override + @SuppressWarnings("Convert2MethodRef") // method reference does not work to super calls + public String format(Object value) { + Collection collOfDateTimes = (Collection) value; + return wrapInBrackets(collOfDateTimes.stream() + .map(item -> super.format(item)) + .collect(Collectors.joining(", "))); + } +} diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/DateTimePlaceholderFormatter.java b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/DateTimePlaceholderFormatter.java new file mode 100644 index 00000000000..0676ae490b9 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/DateTimePlaceholderFormatter.java @@ -0,0 +1,64 @@ +package com.netgrif.application.engine.pfql.service.formatters; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.temporal.ChronoField; +import java.util.Date; +import java.util.regex.Pattern; + +// todo 2483 doc +public class DateTimePlaceholderFormatter implements QueryLangPlaceholderFormatter { + + /** + * Regex pattern derived from the {@code DATETIME} token defined in {@code QueryLang.g4}: + *
+     * DATETIME: DATE 'T' ([01] DIGIT | '2' [0-3]) ':' [0-5] DIGIT ':' [0-5] DIGIT ('.' DIGIT+)?
+     * 
+ * Example: {@code 2020-03-03T20:00:00} or {@code 2020-03-03T20:00:00.055} + *

+ * Note: If the grammar changes, this pattern must be updated accordingly. + *

+ */ + protected static final Pattern DATETIME_PATTERN = Pattern.compile( + "\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])T([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(\\.\\d+)?" + ); + protected static final DateTimeFormatter DATETIME_FORMATTER = new DateTimeFormatterBuilder() + .appendPattern("yyyy-MM-dd'T'HH:mm:ss") + .optionalStart() + .appendFraction(ChronoField.NANO_OF_SECOND, 1, 9, true) + .optionalEnd() + .toFormatter(); + + @Override + public boolean supports(Object value) { + return isOfLocalDateTimeType(value) || isOfDateType(value) || isOfStringType(value); + } + + @Override + public String format(Object value) { + if (isOfLocalDateTimeType(value)) { + return ((LocalDateTime) value).format(DATETIME_FORMATTER); + } else if (isOfDateType(value)) { + return ((Date) value).toInstant() + .atZone(ZoneId.systemDefault()) + .toLocalDateTime() + .format(DATETIME_FORMATTER); + } + + return (String) value; + } + + protected boolean isOfStringType(Object value) { + return value instanceof String && DATETIME_PATTERN.matcher((String) value).matches(); + } + + protected boolean isOfLocalDateTimeType(Object value) { + return value instanceof LocalDateTime; + } + + protected boolean isOfDateType(Object value) { + return value instanceof Date; + } +} diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/NumberListPlaceholderFormatter.java b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/NumberListPlaceholderFormatter.java new file mode 100644 index 00000000000..1224693e8a0 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/NumberListPlaceholderFormatter.java @@ -0,0 +1,25 @@ +package com.netgrif.application.engine.pfql.service.formatters; + +import java.util.Collection; +import java.util.stream.Collectors; + +// todo 2483 doc +public class NumberListPlaceholderFormatter extends NumberPlaceholderFormatter { + + @Override + @SuppressWarnings("Convert2MethodRef") // method reference does not work to super calls + public boolean supports(Object value) { + return value instanceof Collection + && !((Collection) value).isEmpty() + && ((Collection) value).stream().allMatch(item -> super.supports(item)); + } + + @Override + @SuppressWarnings("Convert2MethodRef") // method reference does not work to super calls + public String format(Object value) { + Collection collOfNumbers = (Collection) value; + return wrapInBrackets(collOfNumbers.stream() + .map(item -> super.format(item)) + .collect(Collectors.joining(", "))); + } +} diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/NumberPlaceholderFormatter.java b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/NumberPlaceholderFormatter.java new file mode 100644 index 00000000000..f8b36dfcb6c --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/NumberPlaceholderFormatter.java @@ -0,0 +1,15 @@ +package com.netgrif.application.engine.pfql.service.formatters; + +// todo 2483 doc +public class NumberPlaceholderFormatter implements QueryLangPlaceholderFormatter { + + @Override + public boolean supports(Object value) { + return value instanceof Number; + } + + @Override + public String format(Object value) { + return String.valueOf(value); + } +} diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/ObjectIdListPlaceholderFormatter.java b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/ObjectIdListPlaceholderFormatter.java new file mode 100644 index 00000000000..f10fef2922c --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/ObjectIdListPlaceholderFormatter.java @@ -0,0 +1,25 @@ +package com.netgrif.application.engine.pfql.service.formatters; + +import java.util.Collection; +import java.util.stream.Collectors; + +// todo 2483 doc +public class ObjectIdListPlaceholderFormatter extends ObjectIdPlaceholderFormatter { + + @Override + @SuppressWarnings("Convert2MethodRef") // method reference does not work to super calls + public boolean supports(Object value) { + return value instanceof Collection + && !((Collection) value).isEmpty() + && ((Collection) value).stream().allMatch(item -> super.supports(item)); + } + + @Override + @SuppressWarnings("Convert2MethodRef") // method reference does not work to super calls + public String format(Object value) { + Collection collOfNumbers = (Collection) value; + return wrapInBrackets(collOfNumbers.stream() + .map(item -> super.format(item)) + .collect(Collectors.joining(", "))); + } +} diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/ObjectIdPlaceholderFormatter.java b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/ObjectIdPlaceholderFormatter.java new file mode 100644 index 00000000000..41a11e36210 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/ObjectIdPlaceholderFormatter.java @@ -0,0 +1,17 @@ +package com.netgrif.application.engine.pfql.service.formatters; + +import org.bson.types.ObjectId; + +// todo 2483 doc +public class ObjectIdPlaceholderFormatter implements QueryLangPlaceholderFormatter { + + @Override + public boolean supports(Object value) { + return value instanceof ObjectId; + } + + @Override + public String format(Object value) { + return wrapInSingleQuotes(((ObjectId) value).toHexString()); + } +} diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/QueryLangPlaceholderFormatter.java b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/QueryLangPlaceholderFormatter.java new file mode 100644 index 00000000000..b191a5bcc3c --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/QueryLangPlaceholderFormatter.java @@ -0,0 +1,21 @@ +package com.netgrif.application.engine.pfql.service.formatters; + +// todo 2483 doc +public interface QueryLangPlaceholderFormatter { + boolean supports(Object value); + String format(Object value); + + default String wrapInBrackets(String valueInBrackets) { + if (valueInBrackets == null) { + return "()"; + } + return "(" + valueInBrackets + ")"; + } + + default String wrapInSingleQuotes(Object valueToWrap) { + if (valueToWrap == null) { + return "''"; + } + return "'" + valueToWrap + "'"; + } +} \ No newline at end of file diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/QueryLangPlaceholderHandler.java b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/QueryLangPlaceholderHandler.java new file mode 100644 index 00000000000..ece328d4ed5 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/QueryLangPlaceholderHandler.java @@ -0,0 +1,37 @@ +package com.netgrif.application.engine.pfql.service.formatters; + +import org.springframework.stereotype.Service; + +import java.util.List; + +// todo 2483 doc +@Service +public class QueryLangPlaceholderHandler { + private final List formatters; + + public QueryLangPlaceholderHandler() { + this.formatters = List.of( + new CaseRefPlaceholderFormatter(), + new TaskRefPlaceholderFormatter(), + new BooleanPlaceholderFormatter(), + new NumberPlaceholderFormatter(), + new NumberListPlaceholderFormatter(), + new ObjectIdPlaceholderFormatter(), + new ObjectIdListPlaceholderFormatter(), + new DateTimePlaceholderFormatter(), + new DateTimeListPlaceholderFormatter(), + new DatePlaceholderFormatter(), + new DateListPlaceholderFormatter(), + new StringPlaceholderFormatter(), + new StringListPlaceholderFormatter() + ); + } + + public String format(Object value) { + return formatters.stream() + .filter(formatter -> formatter.supports(value)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Unsupported placeholder value: " + value)) + .format(value); + } +} diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/StringListPlaceholderFormatter.java b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/StringListPlaceholderFormatter.java new file mode 100644 index 00000000000..ee87788b2c4 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/StringListPlaceholderFormatter.java @@ -0,0 +1,25 @@ +package com.netgrif.application.engine.pfql.service.formatters; + +import java.util.Collection; +import java.util.stream.Collectors; + +// todo 2483 doc +public class StringListPlaceholderFormatter extends StringPlaceholderFormatter { + + @Override + @SuppressWarnings("Convert2MethodRef") // method reference does not work to super calls + public boolean supports(Object value) { + return value instanceof Collection + && !((Collection) value).isEmpty() + && ((Collection) value).stream().allMatch(item -> super.supports(item)); + } + + @Override + @SuppressWarnings("Convert2MethodRef") // method reference does not work to super calls + public String format(Object value) { + Collection collOfStrings = (Collection) value; + return wrapInBrackets(collOfStrings.stream() + .map(item -> super.format(item)) + .collect(Collectors.joining(", "))); + } +} diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/StringPlaceholderFormatter.java b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/StringPlaceholderFormatter.java new file mode 100644 index 00000000000..a5318651591 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/StringPlaceholderFormatter.java @@ -0,0 +1,15 @@ +package com.netgrif.application.engine.pfql.service.formatters; + +// todo 2483 doc +public class StringPlaceholderFormatter implements QueryLangPlaceholderFormatter { + + @Override + public boolean supports(Object value) { + return value instanceof String; + } + + @Override + public String format(Object value) { + return wrapInSingleQuotes(value); + } +} diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/TaskRefPlaceholderFormatter.java b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/TaskRefPlaceholderFormatter.java new file mode 100644 index 00000000000..acac089d582 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/TaskRefPlaceholderFormatter.java @@ -0,0 +1,25 @@ +package com.netgrif.application.engine.pfql.service.formatters; + +import com.netgrif.application.engine.petrinet.domain.dataset.TaskField; + +import java.util.stream.Collectors; + +// todo 2483 doc +public class TaskRefPlaceholderFormatter implements QueryLangPlaceholderFormatter { + + @Override + public boolean supports(Object value) { + return value instanceof TaskField; + } + + @Override + public String format(Object value) { + TaskField field = (TaskField) value; + if (field.getValue() == null) { + return ""; + } + return wrapInBrackets(field.getValue().stream() + .map(this::wrapInSingleQuotes) + .collect(Collectors.joining(", "))); + } +} diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/processresource/ProcessSearchService.java b/src/main/java/com/netgrif/application/engine/pfql/service/processresource/ProcessSearchService.java index 79a0239dc8e..466f670231d 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/processresource/ProcessSearchService.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/processresource/ProcessSearchService.java @@ -6,7 +6,7 @@ import com.netgrif.application.engine.pfql.domain.enums.QueryType; import com.netgrif.application.engine.pfql.service.AbstractResourceSearchService; import com.netgrif.application.engine.pfql.service.QueryLangEvaluator; -import lombok.RequiredArgsConstructor; +import com.netgrif.application.engine.pfql.service.formatters.QueryLangPlaceholderHandler; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; import org.springframework.stereotype.Service; @@ -20,12 +20,16 @@ // todo 2483 doc @Slf4j @Service -@RequiredArgsConstructor public class ProcessSearchService extends AbstractResourceSearchService { protected static List allowedResourcePrefixes = List.of(QueryLangParser.PROCESS, QueryLangParser.PROCESSES); protected final IPetriNetService petriNetService; + public ProcessSearchService(QueryLangPlaceholderHandler placeholderHandler, IPetriNetService petriNetService) { + super(placeholderHandler); + this.petriNetService = petriNetService; + } + /** * Returns the query type handled by this service. * diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/taskresource/TaskSearchService.java b/src/main/java/com/netgrif/application/engine/pfql/service/taskresource/TaskSearchService.java index 833b7f5bee7..b585d481fed 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/taskresource/TaskSearchService.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/taskresource/TaskSearchService.java @@ -7,9 +7,9 @@ import com.netgrif.application.engine.pfql.domain.enums.QueryType; import com.netgrif.application.engine.pfql.service.AbstractResourceSearchService; import com.netgrif.application.engine.pfql.service.QueryLangEvaluator; +import com.netgrif.application.engine.pfql.service.formatters.QueryLangPlaceholderHandler; import com.netgrif.application.engine.workflow.domain.Task; import com.netgrif.application.engine.workflow.service.interfaces.ITaskService; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.data.domain.Page; @@ -33,7 +33,6 @@ */ @Slf4j @Service -@RequiredArgsConstructor public class TaskSearchService extends AbstractResourceSearchService { protected static List allowedResourcePrefixes = List.of(QueryLangParser.TASK, QueryLangParser.TASKS); @@ -41,6 +40,14 @@ public class TaskSearchService extends AbstractResourceSearchService { protected final IElasticTaskService elasticTaskService; protected final IUserService userService; + public TaskSearchService(QueryLangPlaceholderHandler placeholderHandler, ITaskService taskService, + IElasticTaskService elasticTaskService, IUserService userService) { + super(placeholderHandler); + this.taskService = taskService; + this.elasticTaskService = elasticTaskService; + this.userService = userService; + } + /** * Returns the query type handled by this service. * diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/userresource/UserSearchService.java b/src/main/java/com/netgrif/application/engine/pfql/service/userresource/UserSearchService.java index de635e799dc..732a3f2002c 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/userresource/UserSearchService.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/userresource/UserSearchService.java @@ -6,7 +6,7 @@ import com.netgrif.application.engine.pfql.domain.enums.QueryType; import com.netgrif.application.engine.pfql.service.AbstractResourceSearchService; import com.netgrif.application.engine.pfql.service.QueryLangEvaluator; -import lombok.RequiredArgsConstructor; +import com.netgrif.application.engine.pfql.service.formatters.QueryLangPlaceholderHandler; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; import org.springframework.stereotype.Service; @@ -26,12 +26,16 @@ */ @Slf4j @Service -@RequiredArgsConstructor public class UserSearchService extends AbstractResourceSearchService { protected static List allowedResourcePrefixes = List.of(QueryLangParser.USER, QueryLangParser.USERS); protected final IUserService userService; + public UserSearchService(QueryLangPlaceholderHandler placeholderHandler, IUserService userService) { + super(placeholderHandler); + this.userService = userService; + } + /** * Returns the resource type handled by this search service. * diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/utils/SearchUtils.java b/src/main/java/com/netgrif/application/engine/pfql/service/utils/SearchUtils.java index 8f3b711b814..c1cab2697d7 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/utils/SearchUtils.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/utils/SearchUtils.java @@ -12,6 +12,7 @@ import com.netgrif.application.engine.pfql.service.QueryLangErrorListener; import com.netgrif.application.engine.pfql.service.QueryLangEvaluator; import com.netgrif.application.engine.pfql.service.QueryLangExplainEvaluator; +import com.netgrif.application.engine.pfql.service.formatters.QueryLangPlaceholderHandler; import com.querydsl.core.BooleanBuilder; import com.querydsl.core.types.Predicate; import com.querydsl.core.types.dsl.BooleanExpression; @@ -464,13 +465,25 @@ public static String buildElasticQueryInRange(String attribute, String leftValue * @param args values to substitute * @return the query string with placeholders filled */ - public static String formatPlaceholders(String query, Object... args) { - // todo 2483 + public static String formatPlaceholders(String query, QueryLangPlaceholderHandler handler, Object... args) { if (args == null || args.length == 0) { return query; } - String pattern = query.replace("{}", "%s"); - return String.format(pattern, args); + + StringBuilder result = new StringBuilder(query); + int argIndex = 0; + int searchFrom = 0; + + while (argIndex < args.length) { + int idx = result.indexOf("{}", searchFrom); + if (idx == -1) { + break; + } + String replacement = handler.format(args[argIndex++]); + result.replace(idx, idx + 2, replacement); + searchFrom = idx + replacement.length(); + } + return result.toString(); } // todo 2483 diff --git a/src/test/java/com/netgrif/application/engine/pfql/CaseSearchServiceTest.java b/src/test/java/com/netgrif/application/engine/pfql/CaseSearchServiceTest.java index 83591671e35..319b4546eb5 100644 --- a/src/test/java/com/netgrif/application/engine/pfql/CaseSearchServiceTest.java +++ b/src/test/java/com/netgrif/application/engine/pfql/CaseSearchServiceTest.java @@ -21,6 +21,7 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit.jupiter.SpringExtension; +import java.util.List; import java.util.Map; import java.util.Optional; @@ -76,6 +77,21 @@ public void searchOneTest() throws InterruptedException { assertNotNull(result.getPetriNet()); assertEquals(testCase.getStringId(), result.getStringId()); + result = caseSearchService.searchOne("title eq {}", "test"); + assertNotNull(result); + assertNotNull(result.getPetriNet()); + assertEquals(testCase.getStringId(), result.getStringId()); + + result = caseSearchService.searchOne("title in ('test', 'test2')"); + assertNotNull(result); + assertNotNull(result.getPetriNet()); + assertEquals(testCase.getStringId(), result.getStringId()); + + result = caseSearchService.searchOne("title in {}", List.of("test", "test2")); + assertNotNull(result); + assertNotNull(result.getPetriNet()); + assertEquals(testCase.getStringId(), result.getStringId()); + login(mockService.mockLoggedUser()); result = caseSearchService.searchOne("case: title eq 'test'"); assertNull(result); diff --git a/src/test/java/com/netgrif/application/engine/pfql/formatters/QueryLangPlaceholderHandlerTest.java b/src/test/java/com/netgrif/application/engine/pfql/formatters/QueryLangPlaceholderHandlerTest.java new file mode 100644 index 00000000000..9522b8b3843 --- /dev/null +++ b/src/test/java/com/netgrif/application/engine/pfql/formatters/QueryLangPlaceholderHandlerTest.java @@ -0,0 +1,609 @@ + +package com.netgrif.application.engine.pfql.formatters; + +import com.netgrif.application.engine.petrinet.domain.dataset.CaseField; +import com.netgrif.application.engine.petrinet.domain.dataset.TaskField; +import com.netgrif.application.engine.pfql.service.formatters.QueryLangPlaceholderHandler; +import com.netgrif.application.engine.pfql.service.utils.SearchUtils; +import org.bson.types.ObjectId; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.Date; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +@ActiveProfiles({"test"}) +@ExtendWith(SpringExtension.class) +public class QueryLangPlaceholderHandlerTest { + + @Autowired + private QueryLangPlaceholderHandler placeholderHandler; + + // ========================================================================= + // CASE queries + // ========================================================================= + + @Test + public void testCasePlaceholder_String() { + String query = SearchUtils.formatPlaceholders("case: title eq {}", placeholderHandler, "my-title"); + assertEquals("case: title eq 'my-title'", query); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + @Test + public void testCasePlaceholder_StringList() { + String query = SearchUtils.formatPlaceholders("case: title in {}", placeholderHandler, + List.of("title-a", "title-b", "title-c")); + assertEquals("case: title in ('title-a', 'title-b', 'title-c')", query); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + @Test + public void testCasePlaceholder_Boolean() { + String query = SearchUtils.formatPlaceholders("case: data.active.value eq {}", placeholderHandler, true); + assertEquals("case: data.active.value eq true", query); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + @Test + public void testCasePlaceholder_Number() { + String query = SearchUtils.formatPlaceholders("case: data.count.value eq {}", placeholderHandler, 42); + assertEquals("case: data.count.value eq 42", query); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + @Test + public void testCasePlaceholder_NumberDouble() { + String query = SearchUtils.formatPlaceholders("case: data.price.value gt {}", placeholderHandler, 3.14); + assertEquals("case: data.price.value gt 3.14", query); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + @Test + public void testCasePlaceholder_NumberList() { + String query = SearchUtils.formatPlaceholders("case: data.score.value in {}", placeholderHandler, + List.of(1, 2, 3)); + assertEquals("case: data.score.value in (1, 2, 3)", query); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + @Test + public void testCasePlaceholder_ObjectId() { + ObjectId id = new ObjectId("507f1f77bcf86cd799439011"); + String query = SearchUtils.formatPlaceholders("case: id eq {}", placeholderHandler, id); + assertEquals("case: id eq '507f1f77bcf86cd799439011'", query); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + @Test + public void testCasePlaceholder_ObjectIdList() { + ObjectId id1 = new ObjectId("507f1f77bcf86cd799439011"); + ObjectId id2 = new ObjectId("507f1f77bcf86cd799439012"); + String query = SearchUtils.formatPlaceholders("case: id in {}", placeholderHandler, List.of(id1, id2)); + assertEquals("case: id in ('507f1f77bcf86cd799439011', '507f1f77bcf86cd799439012')", query); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + @Test + public void testCasePlaceholder_DateTime_LocalDateTime() { + LocalDateTime dt = LocalDateTime.of(2024, 3, 15, 10, 30, 0, 5); + String query = SearchUtils.formatPlaceholders("case: creationDate gt {}", placeholderHandler, dt); + assertEquals("case: creationDate gt 2024-03-15T10:30:00.5", query); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + @Test + public void testCasePlaceholder_DateTime_String() { + String query = SearchUtils.formatPlaceholders("case: creationDate lte {}", placeholderHandler, "2024-03-15T10:30:00"); + assertEquals("case: creationDate lte 2024-03-15T10:30:00", query); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + @Test + public void testCasePlaceholder_DateTimeList() { + LocalDateTime dt1 = LocalDateTime.of(2024, 1, 1, 0, 0, 0); + LocalDateTime dt2 = LocalDateTime.of(2024, 6, 1, 0, 0, 0); + Date dt3 = new Date(dt2.plusDays(1).toEpochSecond(ZoneOffset.UTC)); + String query = SearchUtils.formatPlaceholders("case: creationDate in {}", placeholderHandler, List.of(dt1, dt2, dt3)); + assertEquals("case: creationDate in (2024-01-01T00:00:00.0, 2024-06-01T00:00:00.0, 2024-06-02T00:00:00.0)", query); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + @Test + public void testCasePlaceholder_Date_LocalDate() { + LocalDate date = LocalDate.of(2024, 5, 20); + String query = SearchUtils.formatPlaceholders("case: creationDate gte {}", placeholderHandler, date); + assertEquals("case: creationDate gte 2024-05-20", query); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + @Test + public void testCasePlaceholder_Date_String() { + String query = SearchUtils.formatPlaceholders("case: creationDate lt {}", placeholderHandler, "2024-05-20"); + assertEquals("case: creationDate lt 2024-05-20", query); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + @Test + public void testCasePlaceholder_DateList() { + LocalDate d1 = LocalDate.of(2024, 1, 10); + LocalDate d2 = LocalDate.of(2024, 2, 20); + LocalDate d3 = LocalDate.of(2024, 3, 30); + String query = SearchUtils.formatPlaceholders("case: creationDate in {}", placeholderHandler, List.of(d1, d2, d3)); + assertEquals("case: creationDate in (2024-01-10, 2024-02-20, 2024-03-30)", query); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + @Test + public void testCasePlaceholder_CaseRef_WithValues() { + CaseField caseField = new CaseField(); + caseField.setValue(List.of("507f1f77bcf86cd799439011", "507f1f77bcf86cd799439012")); + String query = SearchUtils.formatPlaceholders("case: id in {}", placeholderHandler, caseField); + assertEquals("case: id in ('507f1f77bcf86cd799439011', '507f1f77bcf86cd799439012')", query); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + @Test + public void testCasePlaceholder_CaseRef_Null() { + CaseField caseField = new CaseField(); + caseField.setValue(null); + String formatted = placeholderHandler.format(caseField); + assertEquals("", formatted); + } + + @Test + public void testCasePlaceholder_MultiplePlaceholders() { + String query = SearchUtils.formatPlaceholders( + "case: processIdentifier eq {} and title eq {} and data.count.value gt {}", + placeholderHandler, + "my-process", "My Case", 5 + ); + assertEquals("case: processIdentifier eq 'my-process' and title eq 'My Case' and data.count.value gt 5", query); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + @Test + public void testCasePlaceholder_MultiplePlaceholders_MixedTypes() { + ObjectId oid = new ObjectId("507f1f77bcf86cd799439011"); + LocalDate date = LocalDate.of(2023, 6, 1); + String query = SearchUtils.formatPlaceholders( + "case: id eq {} and creationDate gte {} and title in {}", + placeholderHandler, + oid, date, List.of("Alpha", "Beta") + ); + assertEquals( + "case: id eq '507f1f77bcf86cd799439011' and creationDate gte 2023-06-01 and title in ('Alpha', 'Beta')", + query + ); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + // ========================================================================= + // TASK queries + // ========================================================================= + + @Test + public void testTaskPlaceholder_String() { + String query = SearchUtils.formatPlaceholders("task: transitionId eq {}", placeholderHandler, "t1"); + assertEquals("task: transitionId eq 't1'", query); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + @Test + public void testTaskPlaceholder_StringList() { + String query = SearchUtils.formatPlaceholders("task: transitionId in {}", placeholderHandler, + List.of("t1", "t2", "t3")); + assertEquals("task: transitionId in ('t1', 't2', 't3')", query); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + @Test + public void testTaskPlaceholder_Boolean() { + // Boolean formats to 'true'/'false' which can appear in caseId eq context + String formatted = placeholderHandler.format(false); + assertEquals("false", formatted); + } + + @Test + public void testTaskPlaceholder_Number() { + String query = SearchUtils.formatPlaceholders("task: caseId eq {}", placeholderHandler, 100); + assertEquals("task: caseId eq 100", query); + // caseId is a string field so this won't parse correctly, but we verify formatting + String formatted = placeholderHandler.format(100); + assertEquals("100", formatted); + } + + @Test + public void testTaskPlaceholder_NumberList() { + String formatted = placeholderHandler.format(List.of(10L, 20L, 30L)); + assertEquals("(10, 20, 30)", formatted); + } + + @Test + public void testTaskPlaceholder_ObjectId() { + ObjectId id = new ObjectId("507f1f77bcf86cd799439022"); + String query = SearchUtils.formatPlaceholders("task: id eq {}", placeholderHandler, id); + assertEquals("task: id eq '507f1f77bcf86cd799439022'", query); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + @Test + public void testTaskPlaceholder_ObjectIdList() { + ObjectId id1 = new ObjectId("507f1f77bcf86cd799439022"); + ObjectId id2 = new ObjectId("507f1f77bcf86cd799439033"); + String query = SearchUtils.formatPlaceholders("task: id in {}", placeholderHandler, List.of(id1, id2)); + assertEquals("task: id in ('507f1f77bcf86cd799439022', '507f1f77bcf86cd799439033')", query); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + @Test + public void testTaskPlaceholder_DateTime_LocalDateTime() { + LocalDateTime dt = LocalDateTime.of(2024, 6, 10, 8, 0, 0); + String query = SearchUtils.formatPlaceholders("task: lastAssign gt {}", placeholderHandler, dt); + assertEquals("task: lastAssign gt 2024-06-10T08:00:00.0", query); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + @Test + public void testTaskPlaceholder_DateTime_String() { + String query = SearchUtils.formatPlaceholders("task: lastFinish lte {}", placeholderHandler, "2024-11-30T23:59:59"); + assertEquals("task: lastFinish lte 2024-11-30T23:59:59", query); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + @Test + public void testTaskPlaceholder_DateTimeList() { + LocalDateTime dt1 = LocalDateTime.of(2024, 1, 1, 12, 0, 0); + LocalDateTime dt2 = LocalDateTime.of(2024, 12, 31, 12, 0, 0); + String query = SearchUtils.formatPlaceholders("task: lastAssign in {}", placeholderHandler, List.of(dt1, dt2)); + assertEquals("task: lastAssign in (2024-01-01T12:00:00.0, 2024-12-31T12:00:00.0)", query); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + @Test + public void testTaskPlaceholder_TaskRef_WithValues() { + TaskField taskField = new TaskField(); + taskField.setValue(List.of("task-id-1", "task-id-2", "task-id-3")); + String formatted = placeholderHandler.format(taskField); + assertEquals("('task-id-1', 'task-id-2', 'task-id-3')", formatted); + } + + @Test + public void testTaskPlaceholder_TaskRef_Null() { + TaskField taskField = new TaskField(); + taskField.setValue(null); + String formatted = placeholderHandler.format(taskField); + assertEquals("", formatted); + } + + @Test + public void testTaskPlaceholder_MultiplePlaceholders() { + String query = SearchUtils.formatPlaceholders( + "task: processId eq {} and userId eq {} and transitionId eq {}", + placeholderHandler, + "proc-123", "user-456", "t1" + ); + assertEquals("task: processId eq 'proc-123' and userId eq 'user-456' and transitionId eq 't1'", query); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + @Test + public void testTaskPlaceholder_MultiplePlaceholders_DateAndString() { + LocalDateTime from = LocalDateTime.of(2024, 1, 1, 0, 0, 0); + LocalDateTime to = LocalDateTime.of(2024, 12, 31, 23, 59, 59); + String query = SearchUtils.formatPlaceholders( + "task: transitionId eq {} and lastAssign in ({} : {})", + placeholderHandler, + "transition-1", from, to + ); + assertEquals( + "task: transitionId eq 'transition-1' and lastAssign in (2024-01-01T00:00:00.0 : 2024-12-31T23:59:59.0)", + query + ); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + // ========================================================================= + // PROCESS queries + // ========================================================================= + + @Test + public void testProcessPlaceholder_String() { + String query = SearchUtils.formatPlaceholders("process: identifier eq {}", placeholderHandler, "my-process"); + assertEquals("process: identifier eq 'my-process'", query); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + @Test + public void testProcessPlaceholder_StringList() { + String query = SearchUtils.formatPlaceholders("process: identifier in {}", placeholderHandler, + List.of("proc-a", "proc-b")); + assertEquals("process: identifier in ('proc-a', 'proc-b')", query); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + @Test + public void testProcessPlaceholder_Boolean() { + String formatted = placeholderHandler.format(true); + assertEquals("true", formatted); + } + + @Test + public void testProcessPlaceholder_Number_Integer() { + String formatted = placeholderHandler.format(7); + assertEquals("7", formatted); + } + + @Test + public void testProcessPlaceholder_Number_Long() { + String formatted = placeholderHandler.format(1000L); + assertEquals("1000", formatted); + } + + @Test + public void testProcessPlaceholder_NumberList() { + String formatted = placeholderHandler.format(List.of(1, 2, 3)); + assertEquals("(1, 2, 3)", formatted); + } + + @Test + public void testProcessPlaceholder_ObjectId() { + ObjectId id = new ObjectId("507f1f77bcf86cd799439055"); + String query = SearchUtils.formatPlaceholders("process: id eq {}", placeholderHandler, id); + assertEquals("process: id eq '507f1f77bcf86cd799439055'", query); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + @Test + public void testProcessPlaceholder_ObjectIdList() { + ObjectId id1 = new ObjectId("507f1f77bcf86cd799439055"); + ObjectId id2 = new ObjectId("507f1f77bcf86cd799439066"); + String query = SearchUtils.formatPlaceholders("process: id in {}", placeholderHandler, List.of(id1, id2)); + assertEquals("process: id in ('507f1f77bcf86cd799439055', '507f1f77bcf86cd799439066')", query); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + @Test + public void testProcessPlaceholder_DateTime_LocalDateTime() { + LocalDateTime dt = LocalDateTime.of(2023, 11, 1, 9, 0, 0); + String query = SearchUtils.formatPlaceholders("process: creationDate eq {}", placeholderHandler, dt); + assertEquals("process: creationDate eq 2023-11-01T09:00:00.0", query); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + @Test + public void testProcessPlaceholder_DateTime_String() { + String query = SearchUtils.formatPlaceholders("process: creationDate gte {}", placeholderHandler, "2023-01-01T00:00:00"); + assertEquals("process: creationDate gte 2023-01-01T00:00:00", query); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + @Test + public void testProcessPlaceholder_DateTimeList() { + LocalDateTime dt1 = LocalDateTime.of(2022, 1, 1, 0, 0, 0); + LocalDateTime dt2 = LocalDateTime.of(2023, 1, 1, 0, 0, 0); + String query = SearchUtils.formatPlaceholders("process: creationDate in {}", placeholderHandler, List.of(dt1, dt2)); + assertEquals("process: creationDate in (2022-01-01T00:00:00.0, 2023-01-01T00:00:00.0)", query); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + @Test + public void testProcessPlaceholder_Date_LocalDate() { + LocalDate date = LocalDate.of(2024, 12, 1); + String query = SearchUtils.formatPlaceholders("process: creationDate lte {}", placeholderHandler, date); + assertEquals("process: creationDate lte 2024-12-01", query); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + @Test + public void testProcessPlaceholder_Date_String() { + String query = SearchUtils.formatPlaceholders("process: creationDate lt {}", placeholderHandler, "2024-12-31"); + assertEquals("process: creationDate lt 2024-12-31", query); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + @Test + public void testProcessPlaceholder_DateList() { + LocalDate d1 = LocalDate.of(2023, 3, 1); + LocalDate d2 = LocalDate.of(2023, 6, 1); + String query = SearchUtils.formatPlaceholders("process: creationDate in {}", placeholderHandler, List.of(d1, d2)); + assertEquals("process: creationDate in (2023-03-01, 2023-06-01)", query); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + @Test + public void testProcessPlaceholder_MultiplePlaceholders() { + String query = SearchUtils.formatPlaceholders( + "process: identifier eq {} and title eq {}", + placeholderHandler, + "my-process", "My Process Title" + ); + assertEquals("process: identifier eq 'my-process' and title eq 'My Process Title'", query); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + @Test + public void testProcessPlaceholder_MultiplePlaceholders_IdAndDateRange() { + ObjectId oid = new ObjectId("507f1f77bcf86cd799439099"); + LocalDateTime from = LocalDateTime.of(2023, 1, 1, 0, 0, 0); + LocalDateTime to = LocalDateTime.of(2024, 1, 1, 0, 0, 0); + String query = SearchUtils.formatPlaceholders( + "process: id eq {} and creationDate in ({} : {})", + placeholderHandler, + oid, from, to + ); + assertEquals( + "process: id eq '507f1f77bcf86cd799439099' and creationDate in (2023-01-01T00:00:00.0 : 2024-01-01T00:00:00.0)", + query + ); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + // ========================================================================= + // USER queries + // ========================================================================= + + @Test + public void testUserPlaceholder_String() { + String query = SearchUtils.formatPlaceholders("user: email eq {}", placeholderHandler, "user@example.com"); + assertEquals("user: email eq 'user@example.com'", query); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + @Test + public void testUserPlaceholder_StringContains() { + String query = SearchUtils.formatPlaceholders("user: name contains {}", placeholderHandler, "John"); + assertEquals("user: name contains 'John'", query); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + @Test + public void testUserPlaceholder_StringList() { + String query = SearchUtils.formatPlaceholders("user: email in {}", placeholderHandler, + List.of("a@example.com", "b@example.com")); + assertEquals("user: email in ('a@example.com', 'b@example.com')", query); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + @Test + public void testUserPlaceholder_Boolean() { + String formatted = placeholderHandler.format(Boolean.FALSE); + assertEquals("false", formatted); + } + + @Test + public void testUserPlaceholder_Number() { + String formatted = placeholderHandler.format(99); + assertEquals("99", formatted); + } + + @Test + public void testUserPlaceholder_NumberList() { + String formatted = placeholderHandler.format(List.of(10, 20)); + assertEquals("(10, 20)", formatted); + } + + @Test + public void testUserPlaceholder_ObjectId() { + ObjectId id = new ObjectId("507f1f77bcf86cd799439077"); + String query = SearchUtils.formatPlaceholders("user: id eq {}", placeholderHandler, id); + assertEquals("user: id eq '507f1f77bcf86cd799439077'", query); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + @Test + public void testUserPlaceholder_ObjectIdList() { + ObjectId id1 = new ObjectId("507f1f77bcf86cd799439077"); + ObjectId id2 = new ObjectId("507f1f77bcf86cd799439088"); + String query = SearchUtils.formatPlaceholders("user: id in {}", placeholderHandler, List.of(id1, id2)); + assertEquals("user: id in ('507f1f77bcf86cd799439077', '507f1f77bcf86cd799439088')", query); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + @Test + public void testUserPlaceholder_DateTime_LocalDateTime() { + LocalDateTime dt = LocalDateTime.of(2024, 7, 4, 12, 0, 0); + String formatted = placeholderHandler.format(dt); + assertEquals("2024-07-04T12:00:00.0", formatted); + } + + @Test + public void testUserPlaceholder_DateTime_String() { + String formatted = placeholderHandler.format("2024-07-04T12:00:00"); + assertEquals("2024-07-04T12:00:00", formatted); + } + + @Test + public void testUserPlaceholder_DateTimeList() { + LocalDateTime dt1 = LocalDateTime.of(2024, 1, 1, 0, 0, 0); + LocalDateTime dt2 = LocalDateTime.of(2024, 6, 15, 0, 0, 0); + String formatted = placeholderHandler.format(List.of(dt1, dt2)); + assertEquals("(2024-01-01T00:00:00.0, 2024-06-15T00:00:00.0)", formatted); + } + + @Test + public void testUserPlaceholder_Date_LocalDate() { + LocalDate date = LocalDate.of(2024, 8, 22); + String formatted = placeholderHandler.format(date); + assertEquals("2024-08-22", formatted); + } + + @Test + public void testUserPlaceholder_Date_String() { + String formatted = placeholderHandler.format("2024-08-22"); + assertEquals("2024-08-22", formatted); + } + + @Test + public void testUserPlaceholder_DateList() { + LocalDate d1 = LocalDate.of(2024, 4, 1); + LocalDate d2 = LocalDate.of(2024, 8, 1); + String formatted = placeholderHandler.format(List.of(d1, d2)); + assertEquals("(2024-04-01, 2024-08-01)", formatted); + } + + @Test + public void testUserPlaceholder_MultiplePlaceholders() { + String query = SearchUtils.formatPlaceholders( + "user: name eq {} and surname eq {} and email eq {}", + placeholderHandler, + "John", "Doe", "john.doe@example.com" + ); + assertEquals("user: name eq 'John' and surname eq 'Doe' and email eq 'john.doe@example.com'", query); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + @Test + public void testUserPlaceholder_MultiplePlaceholders_IdAndNameList() { + ObjectId oid = new ObjectId("507f1f77bcf86cd799439077"); + String query = SearchUtils.formatPlaceholders( + "user: id eq {} and email in {}", + placeholderHandler, + oid, List.of("alice@example.com", "bob@example.com") + ); + assertEquals( + "user: id eq '507f1f77bcf86cd799439077' and email in ('alice@example.com', 'bob@example.com')", + query + ); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + } + + // ========================================================================= + // Unsupported type + // ========================================================================= + + @Test + public void testUnsupportedType_ThrowsException() { + Object unsupported = new Object(); + assertThrows(IllegalArgumentException.class, () -> placeholderHandler.format(unsupported)); + } + + // ========================================================================= + // No placeholders + // ========================================================================= + + @Test + public void testNoPlaceholders_QueryUnchanged() { + String original = "case: title eq 'fixed-title'"; + String result = SearchUtils.formatPlaceholders(original, placeholderHandler); + assertEquals(original, result); + assertDoesNotThrow(() -> SearchUtils.evaluateQuery(result)); + } + + @Test + public void testNullArgs_QueryUnchanged() { + String original = "case: title eq 'fixed-title'"; + String result = SearchUtils.formatPlaceholders(original, placeholderHandler, (Object[]) null); + assertEquals(original, result); + } +} \ No newline at end of file From dca1b1370dcb70ded70990438e751f64e7460a87 Mon Sep 17 00:00:00 2001 From: chvostek Date: Fri, 11 Sep 2026 08:28:08 +0200 Subject: [PATCH 05/11] [NAE-2483] Action API Improvements - fix tests --- .../QueryLangPlaceholderHandlerTest.java | 504 ++++++++---------- 1 file changed, 226 insertions(+), 278 deletions(-) diff --git a/src/test/java/com/netgrif/application/engine/pfql/formatters/QueryLangPlaceholderHandlerTest.java b/src/test/java/com/netgrif/application/engine/pfql/formatters/QueryLangPlaceholderHandlerTest.java index 9522b8b3843..b151a04514b 100644 --- a/src/test/java/com/netgrif/application/engine/pfql/formatters/QueryLangPlaceholderHandlerTest.java +++ b/src/test/java/com/netgrif/application/engine/pfql/formatters/QueryLangPlaceholderHandlerTest.java @@ -4,7 +4,6 @@ import com.netgrif.application.engine.petrinet.domain.dataset.CaseField; import com.netgrif.application.engine.petrinet.domain.dataset.TaskField; import com.netgrif.application.engine.pfql.service.formatters.QueryLangPlaceholderHandler; -import com.netgrif.application.engine.pfql.service.utils.SearchUtils; import org.bson.types.ObjectId; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -15,10 +14,10 @@ import java.time.LocalDate; import java.time.LocalDateTime; -import java.time.ZoneOffset; -import java.util.Date; -import java.util.List; +import java.util.*; +import static com.netgrif.application.engine.pfql.service.utils.SearchUtils.evaluateQuery; +import static com.netgrif.application.engine.pfql.service.utils.SearchUtils.formatPlaceholders; import static org.junit.jupiter.api.Assertions.*; @SpringBootTest @@ -34,149 +33,141 @@ public class QueryLangPlaceholderHandlerTest { // ========================================================================= @Test - public void testCasePlaceholder_String() { - String query = SearchUtils.formatPlaceholders("case: title eq {}", placeholderHandler, "my-title"); + public void testCase_String() { + String query = formatPlaceholders("case: title eq {}", placeholderHandler, "my-title"); assertEquals("case: title eq 'my-title'", query); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + assertDoesNotThrow(() -> evaluateQuery(query)); } @Test - public void testCasePlaceholder_StringList() { - String query = SearchUtils.formatPlaceholders("case: title in {}", placeholderHandler, + public void testCase_StringList() { + String query = formatPlaceholders("case: title in {}", placeholderHandler, List.of("title-a", "title-b", "title-c")); assertEquals("case: title in ('title-a', 'title-b', 'title-c')", query); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + assertDoesNotThrow(() -> evaluateQuery(query)); } @Test - public void testCasePlaceholder_Boolean() { - String query = SearchUtils.formatPlaceholders("case: data.active.value eq {}", placeholderHandler, true); + public void testCase_Boolean() { + String query = formatPlaceholders("case: data.active.value eq {}", placeholderHandler, true); assertEquals("case: data.active.value eq true", query); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + assertDoesNotThrow(() -> evaluateQuery(query)); } @Test - public void testCasePlaceholder_Number() { - String query = SearchUtils.formatPlaceholders("case: data.count.value eq {}", placeholderHandler, 42); + public void testCase_Number() { + String query = formatPlaceholders("case: data.count.value eq {}", placeholderHandler, 42); assertEquals("case: data.count.value eq 42", query); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + assertDoesNotThrow(() -> evaluateQuery(query)); } @Test - public void testCasePlaceholder_NumberDouble() { - String query = SearchUtils.formatPlaceholders("case: data.price.value gt {}", placeholderHandler, 3.14); + public void testCase_NumberDouble() { + String query = formatPlaceholders("case: data.price.value gt {}", placeholderHandler, 3.14); assertEquals("case: data.price.value gt 3.14", query); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + assertDoesNotThrow(() -> evaluateQuery(query)); } @Test - public void testCasePlaceholder_NumberList() { - String query = SearchUtils.formatPlaceholders("case: data.score.value in {}", placeholderHandler, + public void testCase_NumberList() { + String query = formatPlaceholders("case: data.score.value in {}", placeholderHandler, List.of(1, 2, 3)); assertEquals("case: data.score.value in (1, 2, 3)", query); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + assertDoesNotThrow(() -> evaluateQuery(query)); } @Test - public void testCasePlaceholder_ObjectId() { + public void testCase_ObjectId() { ObjectId id = new ObjectId("507f1f77bcf86cd799439011"); - String query = SearchUtils.formatPlaceholders("case: id eq {}", placeholderHandler, id); + String query = formatPlaceholders("case: id eq {}", placeholderHandler, id); assertEquals("case: id eq '507f1f77bcf86cd799439011'", query); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + assertDoesNotThrow(() -> evaluateQuery(query)); } @Test - public void testCasePlaceholder_ObjectIdList() { + public void testCase_ObjectIdList() { ObjectId id1 = new ObjectId("507f1f77bcf86cd799439011"); ObjectId id2 = new ObjectId("507f1f77bcf86cd799439012"); - String query = SearchUtils.formatPlaceholders("case: id in {}", placeholderHandler, List.of(id1, id2)); + String query = formatPlaceholders("case: id in {}", placeholderHandler, List.of(id1, id2)); assertEquals("case: id in ('507f1f77bcf86cd799439011', '507f1f77bcf86cd799439012')", query); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + assertDoesNotThrow(() -> evaluateQuery(query)); } @Test - public void testCasePlaceholder_DateTime_LocalDateTime() { - LocalDateTime dt = LocalDateTime.of(2024, 3, 15, 10, 30, 0, 5); - String query = SearchUtils.formatPlaceholders("case: creationDate gt {}", placeholderHandler, dt); + public void testCase_DateTime_LocalDateTime() { + LocalDateTime dt = LocalDateTime.of(2024, 3, 15, 10, 30, 0, 500_000_000); + String query = formatPlaceholders("case: creationDate gt {}", placeholderHandler, dt); assertEquals("case: creationDate gt 2024-03-15T10:30:00.5", query); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + assertDoesNotThrow(() -> evaluateQuery(query)); } @Test - public void testCasePlaceholder_DateTime_String() { - String query = SearchUtils.formatPlaceholders("case: creationDate lte {}", placeholderHandler, "2024-03-15T10:30:00"); + public void testCase_DateTime_String() { + String query = formatPlaceholders("case: creationDate lte {}", placeholderHandler, "2024-03-15T10:30:00"); assertEquals("case: creationDate lte 2024-03-15T10:30:00", query); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + assertDoesNotThrow(() -> evaluateQuery(query)); } @Test - public void testCasePlaceholder_DateTimeList() { + public void testCase_DateTimeList() { LocalDateTime dt1 = LocalDateTime.of(2024, 1, 1, 0, 0, 0); - LocalDateTime dt2 = LocalDateTime.of(2024, 6, 1, 0, 0, 0); - Date dt3 = new Date(dt2.plusDays(1).toEpochSecond(ZoneOffset.UTC)); - String query = SearchUtils.formatPlaceholders("case: creationDate in {}", placeholderHandler, List.of(dt1, dt2, dt3)); - assertEquals("case: creationDate in (2024-01-01T00:00:00.0, 2024-06-01T00:00:00.0, 2024-06-02T00:00:00.0)", query); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + LocalDateTime dt2 = LocalDateTime.of(2024, 6, 1, 12, 0, 0); + Date dt3 = new GregorianCalendar(2024, Calendar.DECEMBER, 1, 12, 0).getTime(); + String query = formatPlaceholders("case: creationDate in {}", placeholderHandler, List.of(dt1, dt2, dt3)); + assertEquals("case: creationDate in (2024-01-01T00:00:00.0, 2024-06-01T12:00:00.0, 2024-12-01T12:00:00.0)", query); + assertDoesNotThrow(() -> evaluateQuery(query)); } @Test - public void testCasePlaceholder_Date_LocalDate() { + public void testCase_Date_LocalDate() { LocalDate date = LocalDate.of(2024, 5, 20); - String query = SearchUtils.formatPlaceholders("case: creationDate gte {}", placeholderHandler, date); + String query = formatPlaceholders("case: creationDate gte {}", placeholderHandler, date); assertEquals("case: creationDate gte 2024-05-20", query); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + assertDoesNotThrow(() -> evaluateQuery(query)); } @Test - public void testCasePlaceholder_Date_String() { - String query = SearchUtils.formatPlaceholders("case: creationDate lt {}", placeholderHandler, "2024-05-20"); + public void testCase_Date_String() { + String query = formatPlaceholders("case: creationDate lt {}", placeholderHandler, "2024-05-20"); assertEquals("case: creationDate lt 2024-05-20", query); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + assertDoesNotThrow(() -> evaluateQuery(query)); } @Test - public void testCasePlaceholder_DateList() { + public void testCase_DateList() { LocalDate d1 = LocalDate.of(2024, 1, 10); LocalDate d2 = LocalDate.of(2024, 2, 20); LocalDate d3 = LocalDate.of(2024, 3, 30); - String query = SearchUtils.formatPlaceholders("case: creationDate in {}", placeholderHandler, List.of(d1, d2, d3)); + String query = formatPlaceholders("case: creationDate in {}", placeholderHandler, List.of(d1, d2, d3)); assertEquals("case: creationDate in (2024-01-10, 2024-02-20, 2024-03-30)", query); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + assertDoesNotThrow(() -> evaluateQuery(query)); } @Test - public void testCasePlaceholder_CaseRef_WithValues() { + public void testCase_CaseRef() { CaseField caseField = new CaseField(); caseField.setValue(List.of("507f1f77bcf86cd799439011", "507f1f77bcf86cd799439012")); - String query = SearchUtils.formatPlaceholders("case: id in {}", placeholderHandler, caseField); + String query = formatPlaceholders("case: id in {}", placeholderHandler, caseField); assertEquals("case: id in ('507f1f77bcf86cd799439011', '507f1f77bcf86cd799439012')", query); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); - } - - @Test - public void testCasePlaceholder_CaseRef_Null() { - CaseField caseField = new CaseField(); - caseField.setValue(null); - String formatted = placeholderHandler.format(caseField); - assertEquals("", formatted); + assertDoesNotThrow(() -> evaluateQuery(query)); } @Test public void testCasePlaceholder_MultiplePlaceholders() { - String query = SearchUtils.formatPlaceholders( + String query = formatPlaceholders( "case: processIdentifier eq {} and title eq {} and data.count.value gt {}", placeholderHandler, "my-process", "My Case", 5 ); assertEquals("case: processIdentifier eq 'my-process' and title eq 'My Case' and data.count.value gt 5", query); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + assertDoesNotThrow(() -> evaluateQuery(query)); } @Test - public void testCasePlaceholder_MultiplePlaceholders_MixedTypes() { + public void testCase_MultiplePlaceholders_ObjectIdAndDateAndStringList() { ObjectId oid = new ObjectId("507f1f77bcf86cd799439011"); LocalDate date = LocalDate.of(2023, 6, 1); - String query = SearchUtils.formatPlaceholders( + String query = formatPlaceholders( "case: id eq {} and creationDate gte {} and title in {}", placeholderHandler, oid, date, List.of("Alpha", "Beta") @@ -185,7 +176,20 @@ public void testCasePlaceholder_MultiplePlaceholders_MixedTypes() { "case: id eq '507f1f77bcf86cd799439011' and creationDate gte 2023-06-01 and title in ('Alpha', 'Beta')", query ); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + assertDoesNotThrow(() -> evaluateQuery(query)); + } + + @Test + public void testCase_MultiplePlaceholders_DateTimeRange() { + LocalDateTime from = LocalDateTime.of(2023, 1, 1, 0, 0, 0); + LocalDateTime to = LocalDateTime.of(2024, 1, 1, 0, 0, 0); + String query = formatPlaceholders( + "case: creationDate in ({} : {})", + placeholderHandler, + from, to + ); + assertEquals("case: creationDate in (2023-01-01T00:00:00.0 : 2024-01-01T00:00:00.0)", query); + assertDoesNotThrow(() -> evaluateQuery(query)); } // ========================================================================= @@ -193,115 +197,122 @@ public void testCasePlaceholder_MultiplePlaceholders_MixedTypes() { // ========================================================================= @Test - public void testTaskPlaceholder_String() { - String query = SearchUtils.formatPlaceholders("task: transitionId eq {}", placeholderHandler, "t1"); + public void testTask_String() { + String query = formatPlaceholders("task: transitionId eq {}", placeholderHandler, "t1"); assertEquals("task: transitionId eq 't1'", query); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + assertDoesNotThrow(() -> evaluateQuery(query)); } @Test - public void testTaskPlaceholder_StringList() { - String query = SearchUtils.formatPlaceholders("task: transitionId in {}", placeholderHandler, + public void testTask_StringList() { + String query = formatPlaceholders("task: transitionId in {}", placeholderHandler, List.of("t1", "t2", "t3")); assertEquals("task: transitionId in ('t1', 't2', 't3')", query); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + assertDoesNotThrow(() -> evaluateQuery(query)); } @Test - public void testTaskPlaceholder_Boolean() { - // Boolean formats to 'true'/'false' which can appear in caseId eq context - String formatted = placeholderHandler.format(false); - assertEquals("false", formatted); - } - - @Test - public void testTaskPlaceholder_Number() { - String query = SearchUtils.formatPlaceholders("task: caseId eq {}", placeholderHandler, 100); - assertEquals("task: caseId eq 100", query); - // caseId is a string field so this won't parse correctly, but we verify formatting - String formatted = placeholderHandler.format(100); - assertEquals("100", formatted); - } - - @Test - public void testTaskPlaceholder_NumberList() { - String formatted = placeholderHandler.format(List.of(10L, 20L, 30L)); - assertEquals("(10, 20, 30)", formatted); - } - - @Test - public void testTaskPlaceholder_ObjectId() { + public void testTask_ObjectId() { ObjectId id = new ObjectId("507f1f77bcf86cd799439022"); - String query = SearchUtils.formatPlaceholders("task: id eq {}", placeholderHandler, id); + String query = formatPlaceholders("task: id eq {}", placeholderHandler, id); assertEquals("task: id eq '507f1f77bcf86cd799439022'", query); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + assertDoesNotThrow(() -> evaluateQuery(query)); } @Test - public void testTaskPlaceholder_ObjectIdList() { + public void testTask_ObjectIdList() { ObjectId id1 = new ObjectId("507f1f77bcf86cd799439022"); ObjectId id2 = new ObjectId("507f1f77bcf86cd799439033"); - String query = SearchUtils.formatPlaceholders("task: id in {}", placeholderHandler, List.of(id1, id2)); + String query = formatPlaceholders("task: id in {}", placeholderHandler, List.of(id1, id2)); assertEquals("task: id in ('507f1f77bcf86cd799439022', '507f1f77bcf86cd799439033')", query); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + assertDoesNotThrow(() -> evaluateQuery(query)); } @Test - public void testTaskPlaceholder_DateTime_LocalDateTime() { + public void testTask_DateTime_LocalDateTime() { LocalDateTime dt = LocalDateTime.of(2024, 6, 10, 8, 0, 0); - String query = SearchUtils.formatPlaceholders("task: lastAssign gt {}", placeholderHandler, dt); + String query = formatPlaceholders("task: lastAssign gt {}", placeholderHandler, dt); assertEquals("task: lastAssign gt 2024-06-10T08:00:00.0", query); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + assertDoesNotThrow(() -> evaluateQuery(query)); } @Test - public void testTaskPlaceholder_DateTime_String() { - String query = SearchUtils.formatPlaceholders("task: lastFinish lte {}", placeholderHandler, "2024-11-30T23:59:59"); + public void testTask_DateTime_String() { + String query = formatPlaceholders("task: lastFinish lte {}", placeholderHandler, "2024-11-30T23:59:59"); assertEquals("task: lastFinish lte 2024-11-30T23:59:59", query); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + assertDoesNotThrow(() -> evaluateQuery(query)); } @Test - public void testTaskPlaceholder_DateTimeList() { + public void testTask_DateTimeList() { LocalDateTime dt1 = LocalDateTime.of(2024, 1, 1, 12, 0, 0); LocalDateTime dt2 = LocalDateTime.of(2024, 12, 31, 12, 0, 0); - String query = SearchUtils.formatPlaceholders("task: lastAssign in {}", placeholderHandler, List.of(dt1, dt2)); + String query = formatPlaceholders("task: lastAssign in {}", placeholderHandler, List.of(dt1, dt2)); assertEquals("task: lastAssign in (2024-01-01T12:00:00.0, 2024-12-31T12:00:00.0)", query); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + assertDoesNotThrow(() -> evaluateQuery(query)); } @Test - public void testTaskPlaceholder_TaskRef_WithValues() { - TaskField taskField = new TaskField(); - taskField.setValue(List.of("task-id-1", "task-id-2", "task-id-3")); - String formatted = placeholderHandler.format(taskField); - assertEquals("('task-id-1', 'task-id-2', 'task-id-3')", formatted); + public void testTask_Date_LocalDate() { + LocalDate date = LocalDate.of(2024, 9, 1); + String query = formatPlaceholders("task: lastAssign gte {}", placeholderHandler, date); + assertEquals("task: lastAssign gte 2024-09-01", query); + assertDoesNotThrow(() -> evaluateQuery(query)); } @Test - public void testTaskPlaceholder_TaskRef_Null() { + public void testTask_Date_String() { + String query = formatPlaceholders("task: lastFinish lt {}", placeholderHandler, "2024-12-31"); + assertEquals("task: lastFinish lt 2024-12-31", query); + assertDoesNotThrow(() -> evaluateQuery(query)); + } + + @Test + public void testTask_DateList() { + LocalDate d1 = LocalDate.of(2024, 3, 1); + LocalDate d2 = LocalDate.of(2024, 9, 1); + String query = formatPlaceholders("task: lastFinish in {}", placeholderHandler, List.of(d1, d2)); + assertEquals("task: lastFinish in (2024-03-01, 2024-09-01)", query); + assertDoesNotThrow(() -> evaluateQuery(query)); + } + + @Test + public void testTask_TaskRef() { TaskField taskField = new TaskField(); - taskField.setValue(null); - String formatted = placeholderHandler.format(taskField); - assertEquals("", formatted); + taskField.setValue(List.of("507f1f77bcf86cd799439011", "507f1f77bcf86cd799439012", "507f1f77bcf86cd799439013")); + String query = formatPlaceholders("task: id in {}", placeholderHandler, taskField); + assertEquals("task: id in ('507f1f77bcf86cd799439011', '507f1f77bcf86cd799439012', '507f1f77bcf86cd799439013')", query); + assertDoesNotThrow(() -> evaluateQuery(query)); + } + + @Test + public void testTask_MultiplePlaceholders_StringAndObjectId() { + ObjectId id = new ObjectId("507f1f77bcf86cd799439022"); + String query = formatPlaceholders( + "task: processId eq {} and id eq {}", + placeholderHandler, + "507f1f77bcf86cd799439011", id + ); + assertEquals("task: processId eq '507f1f77bcf86cd799439011' and id eq '507f1f77bcf86cd799439022'", query); + assertDoesNotThrow(() -> evaluateQuery(query)); } @Test - public void testTaskPlaceholder_MultiplePlaceholders() { - String query = SearchUtils.formatPlaceholders( + public void testTask_MultiplePlaceholders_ThreeStrings() { + String query = formatPlaceholders( "task: processId eq {} and userId eq {} and transitionId eq {}", placeholderHandler, - "proc-123", "user-456", "t1" + "507f1f77bcf86cd799439011", "507f1f77bcf86cd799439012", "t1" ); - assertEquals("task: processId eq 'proc-123' and userId eq 'user-456' and transitionId eq 't1'", query); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + assertEquals("task: processId eq '507f1f77bcf86cd799439011' and userId eq '507f1f77bcf86cd799439012' and transitionId eq 't1'", query); + assertDoesNotThrow(() -> evaluateQuery(query)); } @Test - public void testTaskPlaceholder_MultiplePlaceholders_DateAndString() { + public void testTask_MultiplePlaceholders_DateTimeRange() { LocalDateTime from = LocalDateTime.of(2024, 1, 1, 0, 0, 0); LocalDateTime to = LocalDateTime.of(2024, 12, 31, 23, 59, 59); - String query = SearchUtils.formatPlaceholders( + String query = formatPlaceholders( "task: transitionId eq {} and lastAssign in ({} : {})", placeholderHandler, "transition-1", from, to @@ -310,7 +321,7 @@ public void testTaskPlaceholder_MultiplePlaceholders_DateAndString() { "task: transitionId eq 'transition-1' and lastAssign in (2024-01-01T00:00:00.0 : 2024-12-31T23:59:59.0)", query ); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + assertDoesNotThrow(() -> evaluateQuery(query)); } // ========================================================================= @@ -318,126 +329,119 @@ public void testTaskPlaceholder_MultiplePlaceholders_DateAndString() { // ========================================================================= @Test - public void testProcessPlaceholder_String() { - String query = SearchUtils.formatPlaceholders("process: identifier eq {}", placeholderHandler, "my-process"); + public void testProcess_String() { + String query = formatPlaceholders("process: identifier eq {}", placeholderHandler, "my-process"); assertEquals("process: identifier eq 'my-process'", query); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + assertDoesNotThrow(() -> evaluateQuery(query)); } @Test - public void testProcessPlaceholder_StringList() { - String query = SearchUtils.formatPlaceholders("process: identifier in {}", placeholderHandler, + public void testProcess_StringList() { + String query = formatPlaceholders("process: identifier in {}", placeholderHandler, List.of("proc-a", "proc-b")); assertEquals("process: identifier in ('proc-a', 'proc-b')", query); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); - } - - @Test - public void testProcessPlaceholder_Boolean() { - String formatted = placeholderHandler.format(true); - assertEquals("true", formatted); + assertDoesNotThrow(() -> evaluateQuery(query)); } @Test - public void testProcessPlaceholder_Number_Integer() { - String formatted = placeholderHandler.format(7); - assertEquals("7", formatted); + public void testProcess_Number() { + String query = formatPlaceholders("process: version eq {}.{}.{}", placeholderHandler, 1, 2, 3); + assertEquals("process: version eq 1.2.3", query); + assertDoesNotThrow(() -> evaluateQuery(query)); } @Test - public void testProcessPlaceholder_Number_Long() { - String formatted = placeholderHandler.format(1000L); - assertEquals("1000", formatted); + public void testProcess_NumberList() { + String query = formatPlaceholders("process: version in ({}.{}.{}, {}.{}.{})", placeholderHandler, + 1, 0, 0, 2, 0, 0); + assertEquals("process: version in (1.0.0, 2.0.0)", query); + assertDoesNotThrow(() -> evaluateQuery(query)); } - @Test - public void testProcessPlaceholder_NumberList() { - String formatted = placeholderHandler.format(List.of(1, 2, 3)); - assertEquals("(1, 2, 3)", formatted); - } + // todo 2483 version object @Test - public void testProcessPlaceholder_ObjectId() { + public void testProcess_ObjectId() { ObjectId id = new ObjectId("507f1f77bcf86cd799439055"); - String query = SearchUtils.formatPlaceholders("process: id eq {}", placeholderHandler, id); + String query = formatPlaceholders("process: id eq {}", placeholderHandler, id); assertEquals("process: id eq '507f1f77bcf86cd799439055'", query); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + assertDoesNotThrow(() -> evaluateQuery(query)); } @Test - public void testProcessPlaceholder_ObjectIdList() { + public void testProcess_ObjectIdList() { ObjectId id1 = new ObjectId("507f1f77bcf86cd799439055"); ObjectId id2 = new ObjectId("507f1f77bcf86cd799439066"); - String query = SearchUtils.formatPlaceholders("process: id in {}", placeholderHandler, List.of(id1, id2)); + String query = formatPlaceholders("process: id in {}", placeholderHandler, List.of(id1, id2)); assertEquals("process: id in ('507f1f77bcf86cd799439055', '507f1f77bcf86cd799439066')", query); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + assertDoesNotThrow(() -> evaluateQuery(query)); } @Test - public void testProcessPlaceholder_DateTime_LocalDateTime() { + public void testProcess_DateTime_LocalDateTime() { LocalDateTime dt = LocalDateTime.of(2023, 11, 1, 9, 0, 0); - String query = SearchUtils.formatPlaceholders("process: creationDate eq {}", placeholderHandler, dt); + String query = formatPlaceholders("process: creationDate eq {}", placeholderHandler, dt); assertEquals("process: creationDate eq 2023-11-01T09:00:00.0", query); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + assertDoesNotThrow(() -> evaluateQuery(query)); } @Test - public void testProcessPlaceholder_DateTime_String() { - String query = SearchUtils.formatPlaceholders("process: creationDate gte {}", placeholderHandler, "2023-01-01T00:00:00"); + public void testProcess_DateTime_String() { + String query = formatPlaceholders("process: creationDate gte {}", placeholderHandler, "2023-01-01T00:00:00"); assertEquals("process: creationDate gte 2023-01-01T00:00:00", query); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + assertDoesNotThrow(() -> evaluateQuery(query)); } @Test - public void testProcessPlaceholder_DateTimeList() { + public void testProcess_DateTimeList() { LocalDateTime dt1 = LocalDateTime.of(2022, 1, 1, 0, 0, 0); LocalDateTime dt2 = LocalDateTime.of(2023, 1, 1, 0, 0, 0); - String query = SearchUtils.formatPlaceholders("process: creationDate in {}", placeholderHandler, List.of(dt1, dt2)); + String query = formatPlaceholders("process: creationDate in {}", placeholderHandler, List.of(dt1, dt2)); assertEquals("process: creationDate in (2022-01-01T00:00:00.0, 2023-01-01T00:00:00.0)", query); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + assertDoesNotThrow(() -> evaluateQuery(query)); } @Test - public void testProcessPlaceholder_Date_LocalDate() { + public void testProcess_Date_LocalDate() { LocalDate date = LocalDate.of(2024, 12, 1); - String query = SearchUtils.formatPlaceholders("process: creationDate lte {}", placeholderHandler, date); + String query = formatPlaceholders("process: creationDate lte {}", placeholderHandler, date); assertEquals("process: creationDate lte 2024-12-01", query); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + assertDoesNotThrow(() -> evaluateQuery(query)); } @Test - public void testProcessPlaceholder_Date_String() { - String query = SearchUtils.formatPlaceholders("process: creationDate lt {}", placeholderHandler, "2024-12-31"); + public void testProcess_Date_String() { + String query = formatPlaceholders("process: creationDate lt {}", placeholderHandler, "2024-12-31"); assertEquals("process: creationDate lt 2024-12-31", query); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + assertDoesNotThrow(() -> evaluateQuery(query)); } @Test - public void testProcessPlaceholder_DateList() { + public void testProcess_DateList() { LocalDate d1 = LocalDate.of(2023, 3, 1); LocalDate d2 = LocalDate.of(2023, 6, 1); - String query = SearchUtils.formatPlaceholders("process: creationDate in {}", placeholderHandler, List.of(d1, d2)); + String query = formatPlaceholders("process: creationDate in {}", placeholderHandler, List.of(d1, d2)); assertEquals("process: creationDate in (2023-03-01, 2023-06-01)", query); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + assertDoesNotThrow(() -> evaluateQuery(query)); } @Test - public void testProcessPlaceholder_MultiplePlaceholders() { - String query = SearchUtils.formatPlaceholders( + public void testProcess_MultiplePlaceholders_TwoStrings() { + String query = formatPlaceholders( "process: identifier eq {} and title eq {}", placeholderHandler, "my-process", "My Process Title" ); assertEquals("process: identifier eq 'my-process' and title eq 'My Process Title'", query); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + assertDoesNotThrow(() -> evaluateQuery(query)); } @Test - public void testProcessPlaceholder_MultiplePlaceholders_IdAndDateRange() { + public void testProcess_MultiplePlaceholders_ObjectIdAndDateRange() { ObjectId oid = new ObjectId("507f1f77bcf86cd799439099"); LocalDateTime from = LocalDateTime.of(2023, 1, 1, 0, 0, 0); LocalDateTime to = LocalDateTime.of(2024, 1, 1, 0, 0, 0); - String query = SearchUtils.formatPlaceholders( + String query = formatPlaceholders( "process: id eq {} and creationDate in ({} : {})", placeholderHandler, oid, from, to @@ -446,7 +450,23 @@ public void testProcessPlaceholder_MultiplePlaceholders_IdAndDateRange() { "process: id eq '507f1f77bcf86cd799439099' and creationDate in (2023-01-01T00:00:00.0 : 2024-01-01T00:00:00.0)", query ); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + assertDoesNotThrow(() -> evaluateQuery(query)); + } + + @Test + public void testProcess_MultiplePlaceholders_StringListAndDateList() { + LocalDate d1 = LocalDate.of(2023, 1, 1); + LocalDate d2 = LocalDate.of(2024, 1, 1); + String query = formatPlaceholders( + "process: identifier in {} and creationDate in {}", + placeholderHandler, + List.of("proc-a", "proc-b"), List.of(d1, d2) + ); + assertEquals( + "process: identifier in ('proc-a', 'proc-b') and creationDate in (2023-01-01, 2024-01-01)", + query + ); + assertDoesNotThrow(() -> evaluateQuery(query)); } // ========================================================================= @@ -454,119 +474,59 @@ public void testProcessPlaceholder_MultiplePlaceholders_IdAndDateRange() { // ========================================================================= @Test - public void testUserPlaceholder_String() { - String query = SearchUtils.formatPlaceholders("user: email eq {}", placeholderHandler, "user@example.com"); + public void testUser_String() { + String query = formatPlaceholders("user: email eq {}", placeholderHandler, "user@example.com"); assertEquals("user: email eq 'user@example.com'", query); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + assertDoesNotThrow(() -> evaluateQuery(query)); } @Test - public void testUserPlaceholder_StringContains() { - String query = SearchUtils.formatPlaceholders("user: name contains {}", placeholderHandler, "John"); + public void testUser_StringContains() { + String query = formatPlaceholders("user: name contains {}", placeholderHandler, "John"); assertEquals("user: name contains 'John'", query); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); - } - - @Test - public void testUserPlaceholder_StringList() { - String query = SearchUtils.formatPlaceholders("user: email in {}", placeholderHandler, - List.of("a@example.com", "b@example.com")); - assertEquals("user: email in ('a@example.com', 'b@example.com')", query); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); - } - - @Test - public void testUserPlaceholder_Boolean() { - String formatted = placeholderHandler.format(Boolean.FALSE); - assertEquals("false", formatted); + assertDoesNotThrow(() -> evaluateQuery(query)); } @Test - public void testUserPlaceholder_Number() { - String formatted = placeholderHandler.format(99); - assertEquals("99", formatted); + public void testUser_StringList() { + String query = formatPlaceholders("user: email in {}", placeholderHandler, + List.of("a@example.com", "b@example.com", "c@example.com")); + assertEquals("user: email in ('a@example.com', 'b@example.com', 'c@example.com')", query); + assertDoesNotThrow(() -> evaluateQuery(query)); } @Test - public void testUserPlaceholder_NumberList() { - String formatted = placeholderHandler.format(List.of(10, 20)); - assertEquals("(10, 20)", formatted); - } - - @Test - public void testUserPlaceholder_ObjectId() { + public void testUser_ObjectId() { ObjectId id = new ObjectId("507f1f77bcf86cd799439077"); - String query = SearchUtils.formatPlaceholders("user: id eq {}", placeholderHandler, id); + String query = formatPlaceholders("user: id eq {}", placeholderHandler, id); assertEquals("user: id eq '507f1f77bcf86cd799439077'", query); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + assertDoesNotThrow(() -> evaluateQuery(query)); } @Test - public void testUserPlaceholder_ObjectIdList() { + public void testUser_ObjectIdList() { ObjectId id1 = new ObjectId("507f1f77bcf86cd799439077"); ObjectId id2 = new ObjectId("507f1f77bcf86cd799439088"); - String query = SearchUtils.formatPlaceholders("user: id in {}", placeholderHandler, List.of(id1, id2)); + String query = formatPlaceholders("user: id in {}", placeholderHandler, List.of(id1, id2)); assertEquals("user: id in ('507f1f77bcf86cd799439077', '507f1f77bcf86cd799439088')", query); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); - } - - @Test - public void testUserPlaceholder_DateTime_LocalDateTime() { - LocalDateTime dt = LocalDateTime.of(2024, 7, 4, 12, 0, 0); - String formatted = placeholderHandler.format(dt); - assertEquals("2024-07-04T12:00:00.0", formatted); + assertDoesNotThrow(() -> evaluateQuery(query)); } @Test - public void testUserPlaceholder_DateTime_String() { - String formatted = placeholderHandler.format("2024-07-04T12:00:00"); - assertEquals("2024-07-04T12:00:00", formatted); - } - - @Test - public void testUserPlaceholder_DateTimeList() { - LocalDateTime dt1 = LocalDateTime.of(2024, 1, 1, 0, 0, 0); - LocalDateTime dt2 = LocalDateTime.of(2024, 6, 15, 0, 0, 0); - String formatted = placeholderHandler.format(List.of(dt1, dt2)); - assertEquals("(2024-01-01T00:00:00.0, 2024-06-15T00:00:00.0)", formatted); - } - - @Test - public void testUserPlaceholder_Date_LocalDate() { - LocalDate date = LocalDate.of(2024, 8, 22); - String formatted = placeholderHandler.format(date); - assertEquals("2024-08-22", formatted); - } - - @Test - public void testUserPlaceholder_Date_String() { - String formatted = placeholderHandler.format("2024-08-22"); - assertEquals("2024-08-22", formatted); - } - - @Test - public void testUserPlaceholder_DateList() { - LocalDate d1 = LocalDate.of(2024, 4, 1); - LocalDate d2 = LocalDate.of(2024, 8, 1); - String formatted = placeholderHandler.format(List.of(d1, d2)); - assertEquals("(2024-04-01, 2024-08-01)", formatted); - } - - @Test - public void testUserPlaceholder_MultiplePlaceholders() { - String query = SearchUtils.formatPlaceholders( + public void testUser_MultiplePlaceholders_ThreeStrings() { + String query = formatPlaceholders( "user: name eq {} and surname eq {} and email eq {}", placeholderHandler, "John", "Doe", "john.doe@example.com" ); assertEquals("user: name eq 'John' and surname eq 'Doe' and email eq 'john.doe@example.com'", query); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + assertDoesNotThrow(() -> evaluateQuery(query)); } @Test - public void testUserPlaceholder_MultiplePlaceholders_IdAndNameList() { + public void testUser_MultiplePlaceholders_ObjectIdAndStringList() { ObjectId oid = new ObjectId("507f1f77bcf86cd799439077"); - String query = SearchUtils.formatPlaceholders( + String query = formatPlaceholders( "user: id eq {} and email in {}", placeholderHandler, oid, List.of("alice@example.com", "bob@example.com") @@ -575,35 +535,23 @@ public void testUserPlaceholder_MultiplePlaceholders_IdAndNameList() { "user: id eq '507f1f77bcf86cd799439077' and email in ('alice@example.com', 'bob@example.com')", query ); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(query)); + assertDoesNotThrow(() -> evaluateQuery(query)); } // ========================================================================= - // Unsupported type + // Edge cases // ========================================================================= @Test public void testUnsupportedType_ThrowsException() { - Object unsupported = new Object(); - assertThrows(IllegalArgumentException.class, () -> placeholderHandler.format(unsupported)); + assertThrows(IllegalArgumentException.class, () -> placeholderHandler.format(new Object())); } - // ========================================================================= - // No placeholders - // ========================================================================= - @Test public void testNoPlaceholders_QueryUnchanged() { String original = "case: title eq 'fixed-title'"; - String result = SearchUtils.formatPlaceholders(original, placeholderHandler); - assertEquals(original, result); - assertDoesNotThrow(() -> SearchUtils.evaluateQuery(result)); - } - - @Test - public void testNullArgs_QueryUnchanged() { - String original = "case: title eq 'fixed-title'"; - String result = SearchUtils.formatPlaceholders(original, placeholderHandler, (Object[]) null); + String result = formatPlaceholders(original, placeholderHandler); assertEquals(original, result); + assertDoesNotThrow(() -> evaluateQuery(result)); } } \ No newline at end of file From 5dbc152c82c3b67abff05729982981de26235a2a Mon Sep 17 00:00:00 2001 From: chvostek Date: Fri, 11 Sep 2026 08:33:30 +0200 Subject: [PATCH 06/11] [NAE-2483] Action API Improvements - implement version formatter --- .../QueryLangPlaceholderHandler.java | 2 ++ .../VersionListPlaceholderFormatter.java | 25 +++++++++++++++++++ .../VersionPlaceholderFormatter.java | 17 +++++++++++++ .../QueryLangPlaceholderHandlerTest.java | 16 +++++++++++- 4 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/netgrif/application/engine/pfql/service/formatters/VersionListPlaceholderFormatter.java create mode 100644 src/main/java/com/netgrif/application/engine/pfql/service/formatters/VersionPlaceholderFormatter.java diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/QueryLangPlaceholderHandler.java b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/QueryLangPlaceholderHandler.java index ece328d4ed5..33f9634358f 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/QueryLangPlaceholderHandler.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/QueryLangPlaceholderHandler.java @@ -18,6 +18,8 @@ public QueryLangPlaceholderHandler() { new NumberListPlaceholderFormatter(), new ObjectIdPlaceholderFormatter(), new ObjectIdListPlaceholderFormatter(), + new VersionPlaceholderFormatter(), + new VersionListPlaceholderFormatter(), new DateTimePlaceholderFormatter(), new DateTimeListPlaceholderFormatter(), new DatePlaceholderFormatter(), diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/VersionListPlaceholderFormatter.java b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/VersionListPlaceholderFormatter.java new file mode 100644 index 00000000000..53f00166963 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/VersionListPlaceholderFormatter.java @@ -0,0 +1,25 @@ +package com.netgrif.application.engine.pfql.service.formatters; + +import java.util.Collection; +import java.util.stream.Collectors; + +// todo 2483 doc +public class VersionListPlaceholderFormatter extends VersionPlaceholderFormatter { + + @Override + @SuppressWarnings("Convert2MethodRef") // method reference does not work to super calls + public boolean supports(Object value) { + return value instanceof Collection + && !((Collection) value).isEmpty() + && ((Collection) value).stream().allMatch(item -> super.supports(item)); + } + + @Override + @SuppressWarnings("Convert2MethodRef") // method reference does not work to super calls + public String format(Object value) { + Collection collOfStrings = (Collection) value; + return wrapInBrackets(collOfStrings.stream() + .map(item -> super.format(item)) + .collect(Collectors.joining(", "))); + } +} diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/VersionPlaceholderFormatter.java b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/VersionPlaceholderFormatter.java new file mode 100644 index 00000000000..1ac3241cc68 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/VersionPlaceholderFormatter.java @@ -0,0 +1,17 @@ +package com.netgrif.application.engine.pfql.service.formatters; + +import com.netgrif.application.engine.petrinet.domain.version.Version; + +// todo 2483 doc +public class VersionPlaceholderFormatter implements QueryLangPlaceholderFormatter { + + @Override + public boolean supports(Object value) { + return value instanceof Version; + } + + @Override + public String format(Object value) { + return value.toString(); + } +} diff --git a/src/test/java/com/netgrif/application/engine/pfql/formatters/QueryLangPlaceholderHandlerTest.java b/src/test/java/com/netgrif/application/engine/pfql/formatters/QueryLangPlaceholderHandlerTest.java index b151a04514b..886ec0da478 100644 --- a/src/test/java/com/netgrif/application/engine/pfql/formatters/QueryLangPlaceholderHandlerTest.java +++ b/src/test/java/com/netgrif/application/engine/pfql/formatters/QueryLangPlaceholderHandlerTest.java @@ -3,6 +3,7 @@ import com.netgrif.application.engine.petrinet.domain.dataset.CaseField; import com.netgrif.application.engine.petrinet.domain.dataset.TaskField; +import com.netgrif.application.engine.petrinet.domain.version.Version; import com.netgrif.application.engine.pfql.service.formatters.QueryLangPlaceholderHandler; import org.bson.types.ObjectId; import org.junit.jupiter.api.Test; @@ -358,7 +359,20 @@ public void testProcess_NumberList() { assertDoesNotThrow(() -> evaluateQuery(query)); } - // todo 2483 version object + @Test + public void testProcess_Version() { + String query = formatPlaceholders("process: version eq {}", placeholderHandler, new Version(1, 2, 3)); + assertEquals("process: version eq 1.2.3", query); + assertDoesNotThrow(() -> evaluateQuery(query)); + } + + @Test + public void testProcess_VersionList() { + String query = formatPlaceholders("process: version in {}", placeholderHandler, + List.of(new Version(1, 0, 0), new Version(2, 0, 0))); + assertEquals("process: version in (1.0.0, 2.0.0)", query); + assertDoesNotThrow(() -> evaluateQuery(query)); + } @Test public void testProcess_ObjectId() { From 1446c826ef52cf5694a355154967876d05e800a4 Mon Sep 17 00:00:00 2001 From: chvostek Date: Fri, 11 Sep 2026 10:13:19 +0200 Subject: [PATCH 07/11] [NAE-2483] Action API Improvements - add support to option caseRefs in CaseRefPlaceholderFormatter - reorder formatters in handler to improve efficiency --- .../domain/dataset/EnumerationMapField.groovy | 12 +++++- .../domain/dataset/MultichoiceMapField.groovy | 13 +++++- .../CaseRefPlaceholderFormatter.java | 40 ++++++++++++++++--- .../QueryLangPlaceholderHandler.java | 6 +-- .../TaskRefPlaceholderFormatter.java | 2 +- .../QueryLangPlaceholderHandlerTest.java | 10 +++++ 6 files changed, 70 insertions(+), 13 deletions(-) diff --git a/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/EnumerationMapField.groovy b/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/EnumerationMapField.groovy index 246d64b018e..01d147feb88 100644 --- a/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/EnumerationMapField.groovy +++ b/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/EnumerationMapField.groovy @@ -45,9 +45,17 @@ class EnumerationMapField extends MapOptionsField { super.setDefaultValue(defaultValue) } - // todo 2483 doc + /** + * Returns the internationalized string value corresponding to the currently selected option key. + *

+ * This method retrieves the {@link I18nString} from the options map that corresponds to the + * current value of this field. + *

+ * + * @return the {@link I18nString} object representing the internationalized value of the selected + * option, or {@code null} if the field's value is null or if no matching option exists. + */ I18nString getI18nValue() { - // todo 2483 test if (this.getValue() == null) { return null; } diff --git a/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/MultichoiceMapField.groovy b/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/MultichoiceMapField.groovy index 564b7279e3e..8e010f26733 100644 --- a/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/MultichoiceMapField.groovy +++ b/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/MultichoiceMapField.groovy @@ -46,9 +46,18 @@ class MultichoiceMapField extends MapOptionsField + * This method maps each selected value (key) in the field's current value to its corresponding + * {@link I18nString} from the options map. + *

+ * + * @return a {@link LinkedHashSet} of {@link I18nString} objects representing the internationalized + * values of the selected options. Returns an empty set if options are null, empty, or if + * the field's value is null. + */ Set getI18nValue() { - // todo 2483 test if (this.options == null || this.options.isEmpty() || this.getValue() == null) { return new LinkedHashSet<>() } diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/CaseRefPlaceholderFormatter.java b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/CaseRefPlaceholderFormatter.java index 52a1444f7af..8f01870359d 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/CaseRefPlaceholderFormatter.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/CaseRefPlaceholderFormatter.java @@ -1,6 +1,8 @@ package com.netgrif.application.engine.pfql.service.formatters; +import com.netgrif.application.engine.petrinet.domain.Component; import com.netgrif.application.engine.petrinet.domain.dataset.CaseField; +import com.netgrif.application.engine.petrinet.domain.dataset.MapOptionsField; import java.util.stream.Collectors; @@ -9,17 +11,45 @@ public class CaseRefPlaceholderFormatter implements QueryLangPlaceholderFormatte @Override public boolean supports(Object value) { - return value instanceof CaseField; + return isValueCaseRef(value) || isOptionsCaseRef(value); } @Override public String format(Object value) { - CaseField field = (CaseField) value; - if (field.getValue() == null) { - return ""; + if (isValueCaseRef(value)) { + CaseField field = (CaseField) value; + if (field.getValue() == null) { + return "()"; + } + return wrapInBrackets(field.getValue().stream() + .map(this::wrapInSingleQuotes) + .collect(Collectors.joining(", "))); + } + + MapOptionsField field = (MapOptionsField) value; + if (field.getOptions() == null) { + return "()"; } - return wrapInBrackets(field.getValue().stream() + + return wrapInBrackets(field.getOptions().keySet().stream() .map(this::wrapInSingleQuotes) .collect(Collectors.joining(", "))); } + + protected boolean isValueCaseRef(Object value) { + return value instanceof CaseField; + } + + protected boolean isOptionsCaseRef(Object value) { + boolean isOptionsField = value instanceof MapOptionsField; + if (isOptionsField) { + MapOptionsField field = (MapOptionsField) value; + Component component = field.getComponent(); + if (component == null) { + return false; + } + return component.getName() != null && component.getName().equals("caseref"); + } + return false; + } } diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/QueryLangPlaceholderHandler.java b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/QueryLangPlaceholderHandler.java index 33f9634358f..1728c911be4 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/QueryLangPlaceholderHandler.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/QueryLangPlaceholderHandler.java @@ -11,8 +11,6 @@ public class QueryLangPlaceholderHandler { public QueryLangPlaceholderHandler() { this.formatters = List.of( - new CaseRefPlaceholderFormatter(), - new TaskRefPlaceholderFormatter(), new BooleanPlaceholderFormatter(), new NumberPlaceholderFormatter(), new NumberListPlaceholderFormatter(), @@ -25,7 +23,9 @@ public QueryLangPlaceholderHandler() { new DatePlaceholderFormatter(), new DateListPlaceholderFormatter(), new StringPlaceholderFormatter(), - new StringListPlaceholderFormatter() + new StringListPlaceholderFormatter(), + new CaseRefPlaceholderFormatter(), + new TaskRefPlaceholderFormatter() ); } diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/TaskRefPlaceholderFormatter.java b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/TaskRefPlaceholderFormatter.java index acac089d582..bbbcae99b3d 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/TaskRefPlaceholderFormatter.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/TaskRefPlaceholderFormatter.java @@ -16,7 +16,7 @@ public boolean supports(Object value) { public String format(Object value) { TaskField field = (TaskField) value; if (field.getValue() == null) { - return ""; + return "()"; } return wrapInBrackets(field.getValue().stream() .map(this::wrapInSingleQuotes) diff --git a/src/test/java/com/netgrif/application/engine/pfql/formatters/QueryLangPlaceholderHandlerTest.java b/src/test/java/com/netgrif/application/engine/pfql/formatters/QueryLangPlaceholderHandlerTest.java index 886ec0da478..35e44792096 100644 --- a/src/test/java/com/netgrif/application/engine/pfql/formatters/QueryLangPlaceholderHandlerTest.java +++ b/src/test/java/com/netgrif/application/engine/pfql/formatters/QueryLangPlaceholderHandlerTest.java @@ -1,7 +1,10 @@ package com.netgrif.application.engine.pfql.formatters; +import com.netgrif.application.engine.petrinet.domain.Component; +import com.netgrif.application.engine.petrinet.domain.I18nString; import com.netgrif.application.engine.petrinet.domain.dataset.CaseField; +import com.netgrif.application.engine.petrinet.domain.dataset.EnumerationMapField; import com.netgrif.application.engine.petrinet.domain.dataset.TaskField; import com.netgrif.application.engine.petrinet.domain.version.Version; import com.netgrif.application.engine.pfql.service.formatters.QueryLangPlaceholderHandler; @@ -151,6 +154,13 @@ public void testCase_CaseRef() { String query = formatPlaceholders("case: id in {}", placeholderHandler, caseField); assertEquals("case: id in ('507f1f77bcf86cd799439011', '507f1f77bcf86cd799439012')", query); assertDoesNotThrow(() -> evaluateQuery(query)); + + EnumerationMapField caseOptionField = new EnumerationMapField(); + caseOptionField.setComponent(new Component("caseref")); + caseOptionField.setOptions(Map.of("507f1f77bcf86cd799439011", new I18nString(), "507f1f77bcf86cd799439012", new I18nString())); + String query2 = formatPlaceholders("case: id in {}", placeholderHandler, caseOptionField); + assertEquals("case: id in ('507f1f77bcf86cd799439011', '507f1f77bcf86cd799439012')", query2); + assertDoesNotThrow(() -> evaluateQuery(query2)); } @Test From f50632eeb7706f63508fb68518cd16b0a23a5b34 Mon Sep 17 00:00:00 2001 From: chvostek Date: Fri, 11 Sep 2026 10:43:31 +0200 Subject: [PATCH 08/11] [NAE-2483] Action API Improvements - add documentation --- .../AbstractResourceSearchService.java | 5 +- .../pfql/service/IResourceSearchService.java | 116 ++++++++++++++++++ .../caseresource/CaseSearchService.java | 5 - .../BooleanPlaceholderFormatter.java | 11 +- .../CaseRefPlaceholderFormatter.java | 21 +++- .../DateListPlaceholderFormatter.java | 14 ++- .../formatters/DatePlaceholderFormatter.java | 20 ++- .../DateTimeListPlaceholderFormatter.java | 14 ++- .../DateTimePlaceholderFormatter.java | 25 +++- .../NumberListPlaceholderFormatter.java | 12 +- .../NumberPlaceholderFormatter.java | 9 +- .../ObjectIdListPlaceholderFormatter.java | 16 ++- .../ObjectIdPlaceholderFormatter.java | 16 ++- .../QueryLangPlaceholderFormatter.java | 15 ++- .../QueryLangPlaceholderHandler.java | 23 +++- .../StringListPlaceholderFormatter.java | 13 +- .../StringPlaceholderFormatter.java | 17 ++- .../TaskRefPlaceholderFormatter.java | 13 +- .../VersionListPlaceholderFormatter.java | 15 ++- .../VersionPlaceholderFormatter.java | 9 +- .../processresource/ProcessSearchService.java | 15 ++- .../taskresource/TaskSearchService.java | 5 - .../userresource/UserSearchService.java | 5 - .../pfql/service/utils/SearchUtils.java | 12 +- 24 files changed, 384 insertions(+), 42 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/AbstractResourceSearchService.java b/src/main/java/com/netgrif/application/engine/pfql/service/AbstractResourceSearchService.java index 254c71559c8..21a99f5a394 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/AbstractResourceSearchService.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/AbstractResourceSearchService.java @@ -27,7 +27,7 @@ public abstract class AbstractResourceSearchService implements IResour * Pre-processes the raw query string: fills {@code {}} placeholders and ensures a correct PFQL prefix. * * @param rawQuery the raw query string, possibly with {@code {}} placeholders - * @param isMulti todo 2483 + * @param isMulti if the prefix should address multiple resources * @param args arguments to substitute into {@code {}} placeholders (in order) * @return the fully pre-processed query string ready for evaluation */ @@ -41,7 +41,7 @@ protected String preProcess(String rawQuery, boolean isMulti, Object... args) { * Each implementation defines which prefix is expected and how to inject it if missing. * * @param query the query string after placeholder substitution - * @param isMulti todo 2483 + * @param isMulti if the prefix should address multiple resources * @return the query string with the correct prefix guaranteed */ protected abstract String ensurePrefix(String query, boolean isMulti); @@ -54,7 +54,6 @@ protected String preProcess(String rawQuery, boolean isMulti, Object... args) { protected abstract boolean doExists(QueryLangEvaluator evaluator); - // todo 2483 doc @Override public Resource searchOne(String queryString, Object... args) { final String processedQuery = preProcess(queryString, false, args); diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/IResourceSearchService.java b/src/main/java/com/netgrif/application/engine/pfql/service/IResourceSearchService.java index 9a6d0be8a59..57846900b33 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/IResourceSearchService.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/IResourceSearchService.java @@ -23,18 +23,134 @@ */ public interface IResourceSearchService { + /** + * Returns the resource type that this search service is designed to handle. + *

+ * This method identifies the specific {@link QueryType} associated with the resource + * managed by implementations of this service. It is used for validation to ensure + * that query evaluators match the expected resource type. + *

+ * + * @return the {@link QueryType} representing the resource type handled by this service + */ QueryType getQueryResourceType(); + /** + * Searches for a single resource using a query string with optional placeholder arguments. + *

+ * The query string may contain placeholders that will be replaced with the provided arguments. + * This method parses and evaluates the query string, then executes the search operation. + *

+ * + * @param queryString the query language expression to evaluate and execute + * @param args optional arguments to substitute into query placeholders + * @return the first resource matching the query, or null if no resource is found + * @throws IllegalArgumentException if the query is invalid or expects multiple results + */ Resource searchOne(String queryString, Object... args); + + /** + * Searches for a single resource using a pre-evaluated query. + *

+ * This method executes a search operation using a {@link QueryLangEvaluator} that has + * already been evaluated and validated. The evaluator must be configured to expect + * a single result and match the service's resource type. + *

+ * + * @param evaluator the evaluated query object containing the search criteria and metadata + * @return the first resource matching the query, or null if no resource is found + * @throws IllegalArgumentException if the evaluator is null, not configured for single results, + * or has a resource type mismatch + */ Resource searchOne(QueryLangEvaluator evaluator); + /** + * Searches for all resources matching a query string with pagination support and optional placeholder arguments. + *

+ * The query string may contain placeholders that will be replaced with the provided arguments. + * This method parses and evaluates the query string, then executes a paginated search operation. + * Results are returned in pages according to the pagination settings in the query. + *

+ * + * @param queryString the query language expression to evaluate and execute + * @param args optional arguments to substitute into query placeholders + * @return a page of resources matching the query with pagination information + * @throws IllegalArgumentException if the query is invalid or expects a single result + */ Page searchAll(String queryString, Object... args); + + /** + * Searches for all resources matching a pre-evaluated query with pagination support. + *

+ * This method executes a paginated search operation using a {@link QueryLangEvaluator} that has + * already been evaluated and validated. The evaluator must be configured to expect + * multiple results and match the service's resource type. Pagination settings from the + * evaluator determine the page size and number. + *

+ * + * @param evaluator the evaluated query object containing the search criteria, pagination settings, and metadata + * @return a page of resources matching the query with pagination information + * @throws IllegalArgumentException if the evaluator is null, not configured for multiple results, + * or has a resource type mismatch + */ Page searchAll(QueryLangEvaluator evaluator); + /** + * Counts the number of resources matching a query string with optional placeholder arguments. + *

+ * The query string may contain placeholders that will be replaced with the provided arguments. + * This method parses and evaluates the query string, then counts the matching resources + * without retrieving them. + *

+ * + * @param queryString the query language expression to evaluate and execute + * @param args optional arguments to substitute into query placeholders + * @return the number of resources matching the query + * @throws IllegalArgumentException if the query is invalid + */ long count(String queryString, Object... args); + + /** + * Counts the number of resources matching a pre-evaluated query. + *

+ * This method executes a count operation using a {@link QueryLangEvaluator} that has + * already been evaluated and validated. The evaluator must match the service's resource type. + * This operation counts matching resources without retrieving them. + *

+ * + * @param evaluator the evaluated query object containing the search criteria and metadata + * @return the number of resources matching the query + * @throws IllegalArgumentException if the evaluator is null or has a resource type mismatch + */ long count(QueryLangEvaluator evaluator); + /** + * Checks if any resource exists that matches a query string with optional placeholder arguments. + *

+ * The query string may contain placeholders that will be replaced with the provided arguments. + * This method parses and evaluates the query string, then checks for the existence of at least + * one matching resource without retrieving it. + *

+ * + * @param queryString the query language expression to evaluate and execute + * @param args optional arguments to substitute into query placeholders + * @return true if at least one resource matching the query exists, false otherwise + * @throws IllegalArgumentException if the query is invalid + */ boolean exists(String queryString, Object... args); + + /** + * Checks if any resource exists that matches a pre-evaluated query. + *

+ * This method executes an existence check using a {@link QueryLangEvaluator} that has + * already been evaluated and validated. The evaluator must match the service's resource type. + * This operation checks for the existence of at least one matching resource without retrieving it. + *

+ * + * @param evaluator the evaluated query object containing the search criteria and metadata + * @return true if at least one resource matching the query exists, false otherwise + * @throws IllegalArgumentException if the evaluator is null or has a resource type mismatch + */ boolean exists(QueryLangEvaluator evaluator); /** diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/caseresource/CaseSearchService.java b/src/main/java/com/netgrif/application/engine/pfql/service/caseresource/CaseSearchService.java index b8bda9eb2fd..c0281180eaf 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/caseresource/CaseSearchService.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/caseresource/CaseSearchService.java @@ -55,11 +55,6 @@ public QueryType getQueryResourceType() { return QueryType.CASE; } - /** - // todo 2483 - * @param query the query string after placeholder substitution - * @return - */ @Override protected String ensurePrefix(String query, boolean isMulti) { if (query == null || hasResourcePrefix(query, allowedResourcePrefixes)) { diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/BooleanPlaceholderFormatter.java b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/BooleanPlaceholderFormatter.java index 3a27cefe6c1..7747723152a 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/BooleanPlaceholderFormatter.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/BooleanPlaceholderFormatter.java @@ -1,6 +1,15 @@ package com.netgrif.application.engine.pfql.service.formatters; -// todo 2483 doc +/** + * Formatter implementation for Boolean placeholder values in PFQL queries. + *

+ * This formatter handles the conversion of Boolean objects into their string representation + * for use in query language placeholders. It supports both {@code true} and {@code false} values, + * converting them to their corresponding string literals. + *

+ * + * @see QueryLangPlaceholderFormatter + */ public class BooleanPlaceholderFormatter implements QueryLangPlaceholderFormatter { @Override diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/CaseRefPlaceholderFormatter.java b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/CaseRefPlaceholderFormatter.java index 8f01870359d..090b2c1507e 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/CaseRefPlaceholderFormatter.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/CaseRefPlaceholderFormatter.java @@ -6,7 +6,26 @@ import java.util.stream.Collectors; -// todo 2483 doc +/** + * Formatter implementation for CaseRef field placeholder values in PFQL queries. + *

+ * This formatter handles the conversion of case reference fields into their string representation + * for use in query language placeholders. It supports two types of case reference values: + *

+ *
    + *
  • {@link CaseField} - Direct case reference fields containing a list of case IDs
  • + *
  • {@link MapOptionsField} - Map-based option fields with a "caseref" component
  • + *
+ *

+ * The formatter converts case reference values into a comma-separated list of single-quoted + * strings enclosed in brackets, e.g., {@code ('case-id-1', 'case-id-2')}. + * Empty or null values are formatted as empty brackets {@code ()}. + *

+ * + * @see QueryLangPlaceholderFormatter + * @see CaseField + * @see MapOptionsField + */ public class CaseRefPlaceholderFormatter implements QueryLangPlaceholderFormatter { @Override diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/DateListPlaceholderFormatter.java b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/DateListPlaceholderFormatter.java index 7c290d71f53..79636e6b617 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/DateListPlaceholderFormatter.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/DateListPlaceholderFormatter.java @@ -3,7 +3,19 @@ import java.util.Collection; import java.util.stream.Collectors; -// todo 2483 doc +/** + * Formatter for collections of date values in PFQL placeholders. + *

+ * This formatter extends {@link DatePlaceholderFormatter} to handle collections of date objects. + * It validates that all items in the collection are supported date types and formats them as a + * comma-separated list wrapped in brackets suitable for MongoDB query syntax. + *

+ *

+ * Example output: {@code [2023-01-15T10:30:00Z, 2023-02-20T14:45:00Z, 2023-03-25T08:15:00Z]} + *

+ * + * @see DatePlaceholderFormatter + */ public class DateListPlaceholderFormatter extends DatePlaceholderFormatter { @Override diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/DatePlaceholderFormatter.java b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/DatePlaceholderFormatter.java index fba2248bb15..5c6c28bc8be 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/DatePlaceholderFormatter.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/DatePlaceholderFormatter.java @@ -4,7 +4,25 @@ import java.time.format.DateTimeFormatter; import java.util.regex.Pattern; -// todo 2483 doc +/** + * Formatter for date placeholders in PFQL queries. + *

+ * This formatter handles date values that can be either {@link LocalDate} instances or + * string representations matching the ISO date format (yyyy-MM-dd). It validates and + * formats date values according to the {@code DATE} token specification defined in the + * QueryLang.g4 grammar. + *

+ *

+ * Supported date formats: + *

    + *
  • {@link LocalDate} objects - formatted to yyyy-MM-dd string
  • + *
  • String values matching pattern: yyyy-MM-dd (e.g., "2020-03-03")
  • + *
+ *

+ * + * @see QueryLangPlaceholderFormatter + * @see LocalDate + */ public class DatePlaceholderFormatter implements QueryLangPlaceholderFormatter { /** diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/DateTimeListPlaceholderFormatter.java b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/DateTimeListPlaceholderFormatter.java index 0c7d4f70ce1..f35f57b65d5 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/DateTimeListPlaceholderFormatter.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/DateTimeListPlaceholderFormatter.java @@ -3,7 +3,19 @@ import java.util.Collection; import java.util.stream.Collectors; -// todo 2483 doc +/** + * Formatter for handling collections of date-time values in PFQL placeholders. + *

+ * This formatter extends {@link DateTimePlaceholderFormatter} to support formatting of collections + * containing date-time objects. It validates that all items in the collection are supported date-time + * types and formats them into a bracketed, comma-separated string representation suitable for + * MongoDB queries. + *

+ *

+ * The formatter only supports non-empty collections where every element passes the parent class's + * {@link DateTimePlaceholderFormatter#supports(Object)} validation. + *

+ */ public class DateTimeListPlaceholderFormatter extends DateTimePlaceholderFormatter { @Override diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/DateTimePlaceholderFormatter.java b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/DateTimePlaceholderFormatter.java index 0676ae490b9..2a232ec59b7 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/DateTimePlaceholderFormatter.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/DateTimePlaceholderFormatter.java @@ -8,7 +8,30 @@ import java.util.Date; import java.util.regex.Pattern; -// todo 2483 doc +/** + * Formatter for datetime placeholders in PFQL queries. + *

+ * This formatter handles datetime values in queries by converting them to a standardized string format + * that matches the {@code DATETIME} token defined in the QueryLang grammar. It supports conversion from + * multiple Java time types including {@link LocalDateTime}, {@link Date}, and pre-formatted datetime strings. + *

+ *

+ * The formatter produces datetime strings in ISO 8601-like format: {@code yyyy-MM-dd'T'HH:mm:ss[.nnnnnnnnn]}, + * where the fractional seconds part is optional and can have 1 to 9 digits. + *

+ *

+ * Examples of supported formats: + *

    + *
  • {@code 2020-03-03T20:00:00}
  • + *
  • {@code 2020-03-03T20:00:00.055}
  • + *
  • {@code 2026-09-11T14:30:45.123456789}
  • + *
+ *

+ * + * @see QueryLangPlaceholderFormatter + * @see LocalDateTime + * @see Date + */ public class DateTimePlaceholderFormatter implements QueryLangPlaceholderFormatter { /** diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/NumberListPlaceholderFormatter.java b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/NumberListPlaceholderFormatter.java index 1224693e8a0..b9b76fbfb36 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/NumberListPlaceholderFormatter.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/NumberListPlaceholderFormatter.java @@ -3,7 +3,17 @@ import java.util.Collection; import java.util.stream.Collectors; -// todo 2483 doc +/** + * Formatter for collections of numeric values in PFQL placeholders. + *

+ * This formatter extends {@link NumberPlaceholderFormatter} to handle collections of numbers. + * It formats a collection of numeric values by converting each number individually using the + * parent formatter and joining them with commas, wrapped in brackets. + *

+ *

+ * Example: A collection [1, 2.5, 3] would be formatted as "(1, 2.5, 3)" + *

+ */ public class NumberListPlaceholderFormatter extends NumberPlaceholderFormatter { @Override diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/NumberPlaceholderFormatter.java b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/NumberPlaceholderFormatter.java index f8b36dfcb6c..5782e1d5599 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/NumberPlaceholderFormatter.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/NumberPlaceholderFormatter.java @@ -1,6 +1,13 @@ package com.netgrif.application.engine.pfql.service.formatters; -// todo 2483 doc +/** + * Formatter for converting Number values to their string representation in PFQL queries. + *

+ * This formatter handles all numeric types (Integer, Long, Double, Float, etc.) by converting + * them to their string representation using {@link String#valueOf(Object)}. It is used during + * placeholder substitution in PFQL query processing to safely embed numeric values into queries. + *

+ */ public class NumberPlaceholderFormatter implements QueryLangPlaceholderFormatter { @Override diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/ObjectIdListPlaceholderFormatter.java b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/ObjectIdListPlaceholderFormatter.java index f10fef2922c..199fdfa2381 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/ObjectIdListPlaceholderFormatter.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/ObjectIdListPlaceholderFormatter.java @@ -3,7 +3,21 @@ import java.util.Collection; import java.util.stream.Collectors; -// todo 2483 doc +/** + * Formatter for handling collections of ObjectId values in PFQL queries. + *

+ * This formatter extends {@link ObjectIdPlaceholderFormatter} to support formatting collections of ObjectId + * values. It validates that all items in the collection are valid + * ObjectId values and formats them as a comma-separated list wrapped in brackets. + *

+ *

+ * Example transformation: A collection containing ObjectId("507f1f77bcf86cd799439011") and + * ObjectId("507f191e810c19729de860ea") would be formatted as: + * ('507f1f77bcf86cd799439011', '507f191e810c19729de860ea') + *

+ * + * @see ObjectIdPlaceholderFormatter + */ public class ObjectIdListPlaceholderFormatter extends ObjectIdPlaceholderFormatter { @Override diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/ObjectIdPlaceholderFormatter.java b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/ObjectIdPlaceholderFormatter.java index 41a11e36210..c9a1eb411c0 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/ObjectIdPlaceholderFormatter.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/ObjectIdPlaceholderFormatter.java @@ -2,7 +2,21 @@ import org.bson.types.ObjectId; -// todo 2483 doc +/** + * Formatter for converting MongoDB {@link ObjectId} instances into PFQL query string format. + *

+ * This formatter is part of the PFQL placeholder formatting system. + * It handles the conversion of {@link ObjectId} objects into their hexadecimal string representation + * wrapped in single quotes, making them suitable for use in MongoDB queries generated from PFQL expressions. + *

+ *

+ * When a PFQL query contains placeholder values that are {@link ObjectId} instances, this formatter + * ensures they are properly converted to their string format (e.g., {@code '507f1f77bcf86cd799439011'}) + *

+ * + * @see QueryLangPlaceholderFormatter + * @see ObjectId + */ public class ObjectIdPlaceholderFormatter implements QueryLangPlaceholderFormatter { @Override diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/QueryLangPlaceholderFormatter.java b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/QueryLangPlaceholderFormatter.java index b191a5bcc3c..87f99763d01 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/QueryLangPlaceholderFormatter.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/QueryLangPlaceholderFormatter.java @@ -1,6 +1,19 @@ package com.netgrif.application.engine.pfql.service.formatters; -// todo 2483 doc +/** + * Interface for formatting placeholder values in PFQL queries. + *

+ * Implementations of this interface are responsible for converting Java objects into their + * string representations. Each formatter implementation + * supports specific types of objects and provides custom formatting logic for those types. + *

+ *

+ * The formatter provides utility methods for wrapping values in brackets and single quotes, + * which are commonly needed when constructing query strings. + *

+ * + * @see QueryLangPlaceholderHandler + */ public interface QueryLangPlaceholderFormatter { boolean supports(Object value); String format(Object value); diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/QueryLangPlaceholderHandler.java b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/QueryLangPlaceholderHandler.java index 1728c911be4..df1960367ff 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/QueryLangPlaceholderHandler.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/QueryLangPlaceholderHandler.java @@ -4,7 +4,15 @@ import java.util.List; -// todo 2483 doc +/** + * Service responsible for formatting placeholder values in PFQL queries based on their type. + *

+ * This handler maintains a list of specialized formatters for different data types and selects + * the appropriate formatter based on the runtime type of the value being formatted. Supported + * types include primitives (boolean, number), collections (lists), temporal types (date, datetime), + * identifiers (ObjectId, version), and reference types (case ref, task ref). + *

+ */ @Service public class QueryLangPlaceholderHandler { private final List formatters; @@ -29,6 +37,19 @@ public QueryLangPlaceholderHandler() { ); } + /** + * Formats a placeholder value according to its runtime type. + *

+ * This method iterates through the registered formatters and uses the first one that + * supports the given value type. The formatted result is a string representation suitable + * for use in MongoDB queries. + *

+ * + * @param value the placeholder value to format; can be of various types including primitives, + * collections, temporal types, or reference types + * @return the formatted string representation of the value suitable for MongoDB queries + * @throws IllegalArgumentException if no formatter supports the given value type + */ public String format(Object value) { return formatters.stream() .filter(formatter -> formatter.supports(value)) diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/StringListPlaceholderFormatter.java b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/StringListPlaceholderFormatter.java index ee87788b2c4..78c4b846d45 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/StringListPlaceholderFormatter.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/StringListPlaceholderFormatter.java @@ -3,7 +3,18 @@ import java.util.Collection; import java.util.stream.Collectors; -// todo 2483 doc +/** + * Formatter for collections of strings in PFQL placeholder substitution. + *

+ * This formatter handles collections of string values by formatting each individual string + * using the parent {@link StringPlaceholderFormatter} logic and then combining them into + * a comma-separated list wrapped in brackets. + *

+ *

+ * The formatter only supports non-empty collections where all items are supported by the + * parent string formatter. + *

+ */ public class StringListPlaceholderFormatter extends StringPlaceholderFormatter { @Override diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/StringPlaceholderFormatter.java b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/StringPlaceholderFormatter.java index a5318651591..4359c1eb953 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/StringPlaceholderFormatter.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/StringPlaceholderFormatter.java @@ -1,6 +1,21 @@ package com.netgrif.application.engine.pfql.service.formatters; -// todo 2483 doc +/** + * Formatter implementation for handling String value placeholders in PFQL queries. + *

+ * This formatter is responsible for converting String placeholder values into properly formatted + * query string representations by wrapping them in single quotes. It is part of the placeholder + * handling mechanism that ensures type-safe query construction. + *

+ *

+ * The formatter implements {@link QueryLangPlaceholderFormatter} and is automatically selected + * by {@link com.netgrif.application.engine.pfql.service.formatters.QueryLangPlaceholderHandler} + * when processing String-typed placeholder values during query evaluation. + *

+ * + * @see QueryLangPlaceholderFormatter + * @see com.netgrif.application.engine.pfql.service.formatters.QueryLangPlaceholderHandler + */ public class StringPlaceholderFormatter implements QueryLangPlaceholderFormatter { @Override diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/TaskRefPlaceholderFormatter.java b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/TaskRefPlaceholderFormatter.java index bbbcae99b3d..f811ffc00e2 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/TaskRefPlaceholderFormatter.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/TaskRefPlaceholderFormatter.java @@ -4,7 +4,18 @@ import java.util.stream.Collectors; -// todo 2483 doc +/** + * Formatter for TaskField placeholders in PFQL queries. + *

+ * This formatter handles the conversion of {@link TaskField} values into properly formatted + * query strings. It wraps task reference values in brackets and quotes. + *

+ *

+ * If the TaskField contains null values, it returns an empty bracket pair "()". + * Otherwise, it formats each task reference value by wrapping it in single quotes and + * joining them with commas within brackets. + *

+ */ public class TaskRefPlaceholderFormatter implements QueryLangPlaceholderFormatter { @Override diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/VersionListPlaceholderFormatter.java b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/VersionListPlaceholderFormatter.java index 53f00166963..8040d9c99a1 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/VersionListPlaceholderFormatter.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/VersionListPlaceholderFormatter.java @@ -3,7 +3,20 @@ import java.util.Collection; import java.util.stream.Collectors; -// todo 2483 doc +/** + * Formatter for collections of version values in PFQL queries. + *

+ * This formatter extends {@link VersionPlaceholderFormatter} to handle collections of version objects. + * It validates that all items in the collection are supported version values and formats them as a + * comma-separated list wrapped in brackets. + *

+ *

+ * The formatter only supports non-empty collections where every element is a valid version value + * as determined by the parent {@link VersionPlaceholderFormatter#supports(Object)} method. + *

+ * + * @see VersionPlaceholderFormatter + */ public class VersionListPlaceholderFormatter extends VersionPlaceholderFormatter { @Override diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/VersionPlaceholderFormatter.java b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/VersionPlaceholderFormatter.java index 1ac3241cc68..1f6777acdd4 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/formatters/VersionPlaceholderFormatter.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/formatters/VersionPlaceholderFormatter.java @@ -2,7 +2,14 @@ import com.netgrif.application.engine.petrinet.domain.version.Version; -// todo 2483 doc +/** + * Formatter for Version objects used in PFQL queries. + *

+ * This formatter handles the conversion of {@link Version} objects into their string + * representation for use in PFQL query placeholders. It supports Version objects and + * formats them by calling their {@code toString()} method. + *

+ */ public class VersionPlaceholderFormatter implements QueryLangPlaceholderFormatter { @Override diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/processresource/ProcessSearchService.java b/src/main/java/com/netgrif/application/engine/pfql/service/processresource/ProcessSearchService.java index 466f670231d..cdbd1e6f0da 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/processresource/ProcessSearchService.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/processresource/ProcessSearchService.java @@ -17,7 +17,15 @@ import static com.netgrif.application.engine.pfql.service.utils.SearchUtils.hasResourcePrefix; -// todo 2483 doc +/** + * Service for searching and querying process resources using PFQL. + *

+ * This service provides methods to search for processes, count processes, and check process existence + * based on PFQL query strings or evaluated query objects. It delegates the actual MongoDB + * queries to the {@link IPetriNetService}. Future implementations will support Elasticsearch as an alternative + * search backend. + *

+ */ @Slf4j @Service public class ProcessSearchService extends AbstractResourceSearchService { @@ -40,11 +48,6 @@ public QueryType getQueryResourceType() { return QueryType.PROCESS; } - /** - // todo 2483 - * @param query the query string after placeholder substitution - * @return - */ @Override protected String ensurePrefix(String query, boolean isMulti) { if (query == null || hasResourcePrefix(query, allowedResourcePrefixes)) { diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/taskresource/TaskSearchService.java b/src/main/java/com/netgrif/application/engine/pfql/service/taskresource/TaskSearchService.java index b585d481fed..6abb6b6b932 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/taskresource/TaskSearchService.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/taskresource/TaskSearchService.java @@ -58,11 +58,6 @@ public QueryType getQueryResourceType() { return QueryType.TASK; } - /** - // todo 2483 - * @param query the query string after placeholder substitution - * @return - */ @Override protected String ensurePrefix(String query, boolean isMulti) { if (query == null || hasResourcePrefix(query, allowedResourcePrefixes)) { diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/userresource/UserSearchService.java b/src/main/java/com/netgrif/application/engine/pfql/service/userresource/UserSearchService.java index 732a3f2002c..3bdc5fcb1a9 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/userresource/UserSearchService.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/userresource/UserSearchService.java @@ -46,11 +46,6 @@ public QueryType getQueryResourceType() { return QueryType.USER; } - /** - // todo 2483 - * @param query the query string after placeholder substitution - * @return - */ @Override protected String ensurePrefix(String query, boolean isMulti) { if (query == null || hasResourcePrefix(query, allowedResourcePrefixes)) { diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/utils/SearchUtils.java b/src/main/java/com/netgrif/application/engine/pfql/service/utils/SearchUtils.java index c1cab2697d7..49b14b16f59 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/utils/SearchUtils.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/utils/SearchUtils.java @@ -486,7 +486,17 @@ public static String formatPlaceholders(String query, QueryLangPlaceholderHandle return result.toString(); } - // todo 2483 + /** + * Checks if a PFQL query string begins with a resource token that matches one of the expected token types. + * The method tokenizes the trimmed query and compares the first token's type against the provided list. + * + * @param query the PFQL query string to check (will be trimmed before tokenization) + * @param expectedTokenTypes a list of token type constants (e.g., {@link QueryLangParser#CASE}, + * {@link QueryLangParser#TASK}) that are considered valid resource prefixes + * @return {@code true} if the first token of the query matches one of the expected types; {@code false} otherwise + * @see #buildResourcePrefix(int) + * @see #validQueryResourcePrefixes + */ public static boolean hasResourcePrefix(String query, List expectedTokenTypes) { CharStream input = CharStreams.fromString(query.trim()); QueryLangLexer lexer = new QueryLangLexer(input); From f829ab47abebfd89dc57bdef1e268e8b1336bbfa Mon Sep 17 00:00:00 2001 From: chvostek Date: Fri, 11 Sep 2026 12:46:55 +0200 Subject: [PATCH 09/11] [NAE-2483] Action API Improvements - resolve duplicated code --- .../AbstractResourceSearchService.java | 32 +++++++++++++------ .../caseresource/CaseSearchService.java | 10 +----- .../processresource/ProcessSearchService.java | 12 +------ .../taskresource/TaskSearchService.java | 10 +----- .../userresource/UserSearchService.java | 11 +------ 5 files changed, 26 insertions(+), 49 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/AbstractResourceSearchService.java b/src/main/java/com/netgrif/application/engine/pfql/service/AbstractResourceSearchService.java index 21a99f5a394..1c2c33319ce 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/AbstractResourceSearchService.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/AbstractResourceSearchService.java @@ -5,8 +5,10 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; -import static com.netgrif.application.engine.pfql.service.utils.SearchUtils.evaluateQuery; -import static com.netgrif.application.engine.pfql.service.utils.SearchUtils.formatPlaceholders; +import java.util.List; + +import static com.netgrif.application.engine.pfql.service.utils.SearchUtils.*; +import static com.netgrif.application.engine.pfql.service.utils.SearchUtils.buildResourcePrefix; /** * Abstract base class for resource search services providing shared query pre-processing. @@ -36,14 +38,7 @@ protected String preProcess(String rawQuery, boolean isMulti, Object... args) { return ensurePrefix(formatted, isMulti); } - /** - * Ensures the query string has the correct PFQL resource prefix. - * Each implementation defines which prefix is expected and how to inject it if missing. - * - * @param query the query string after placeholder substitution - * @param isMulti if the prefix should address multiple resources - * @return the query string with the correct prefix guaranteed - */ + protected abstract String ensurePrefix(String query, boolean isMulti); protected abstract Resource doSearchOne(QueryLangEvaluator evaluator); @@ -54,6 +49,23 @@ protected String preProcess(String rawQuery, boolean isMulti, Object... args) { protected abstract boolean doExists(QueryLangEvaluator evaluator); + /** + * Ensures the query string has the correct PFQL resource prefix. + * Each implementation defines which prefix is expected and how to inject it if missing. + * + * @param query the query string after placeholder substitution + * @param isMulti if the prefix should address multiple resources + * @param multiPrefixToken token of prefix to search multiple resources + * @param singlePrefixToken token of prefix to search single resource + * @return the query string with the correct prefix guaranteed + */ + protected String doEnsurePrefix(String query, boolean isMulti, int multiPrefixToken, int singlePrefixToken) { + if (query == null || hasResourcePrefix(query, List.of(multiPrefixToken, singlePrefixToken))) { + return query; + } + return buildResourcePrefix(isMulti ? multiPrefixToken : singlePrefixToken) + query; + } + @Override public Resource searchOne(String queryString, Object... args) { final String processedQuery = preProcess(queryString, false, args); diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/caseresource/CaseSearchService.java b/src/main/java/com/netgrif/application/engine/pfql/service/caseresource/CaseSearchService.java index c0281180eaf..0b891507529 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/caseresource/CaseSearchService.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/caseresource/CaseSearchService.java @@ -19,9 +19,6 @@ import java.util.List; -import static com.netgrif.application.engine.pfql.service.utils.SearchUtils.buildResourcePrefix; -import static com.netgrif.application.engine.pfql.service.utils.SearchUtils.hasResourcePrefix; - /** * Service implementation for searching and querying Case resources. * Supports both MongoDB and Elasticsearch-based searches depending on the query configuration. @@ -31,8 +28,6 @@ @Service public class CaseSearchService extends AbstractResourceSearchService { - protected static List allowedResourcePrefixes = List.of(QueryLangParser.CASE, QueryLangParser.CASES); - protected final IWorkflowService workflowService; protected final IElasticCaseService elasticCaseService; protected final IUserService userService; @@ -57,10 +52,7 @@ public QueryType getQueryResourceType() { @Override protected String ensurePrefix(String query, boolean isMulti) { - if (query == null || hasResourcePrefix(query, allowedResourcePrefixes)) { - return query; - } - return buildResourcePrefix(isMulti ? QueryLangParser.CASES : QueryLangParser.CASE) + query; + return doEnsurePrefix(query, isMulti, QueryLangParser.CASES, QueryLangParser.CASE); } /** diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/processresource/ProcessSearchService.java b/src/main/java/com/netgrif/application/engine/pfql/service/processresource/ProcessSearchService.java index cdbd1e6f0da..690b0ebfe45 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/processresource/ProcessSearchService.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/processresource/ProcessSearchService.java @@ -11,12 +11,6 @@ import org.springframework.data.domain.Page; import org.springframework.stereotype.Service; -import java.util.List; - -import static com.netgrif.application.engine.pfql.service.utils.SearchUtils.buildResourcePrefix; -import static com.netgrif.application.engine.pfql.service.utils.SearchUtils.hasResourcePrefix; - - /** * Service for searching and querying process resources using PFQL. *

@@ -29,7 +23,6 @@ @Slf4j @Service public class ProcessSearchService extends AbstractResourceSearchService { - protected static List allowedResourcePrefixes = List.of(QueryLangParser.PROCESS, QueryLangParser.PROCESSES); protected final IPetriNetService petriNetService; @@ -50,10 +43,7 @@ public QueryType getQueryResourceType() { @Override protected String ensurePrefix(String query, boolean isMulti) { - if (query == null || hasResourcePrefix(query, allowedResourcePrefixes)) { - return query; - } - return buildResourcePrefix(isMulti ? QueryLangParser.PROCESSES : QueryLangParser.PROCESS) + query; + return doEnsurePrefix(query, isMulti, QueryLangParser.PROCESSES, QueryLangParser.PROCESS); } /** diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/taskresource/TaskSearchService.java b/src/main/java/com/netgrif/application/engine/pfql/service/taskresource/TaskSearchService.java index 6abb6b6b932..9e87c245bb3 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/taskresource/TaskSearchService.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/taskresource/TaskSearchService.java @@ -19,9 +19,6 @@ import java.util.List; -import static com.netgrif.application.engine.pfql.service.utils.SearchUtils.buildResourcePrefix; -import static com.netgrif.application.engine.pfql.service.utils.SearchUtils.hasResourcePrefix; - /** * Service implementation for searching Task resources using query language expressions. *

@@ -34,8 +31,6 @@ @Slf4j @Service public class TaskSearchService extends AbstractResourceSearchService { - protected static List allowedResourcePrefixes = List.of(QueryLangParser.TASK, QueryLangParser.TASKS); - protected final ITaskService taskService; protected final IElasticTaskService elasticTaskService; protected final IUserService userService; @@ -60,10 +55,7 @@ public QueryType getQueryResourceType() { @Override protected String ensurePrefix(String query, boolean isMulti) { - if (query == null || hasResourcePrefix(query, allowedResourcePrefixes)) { - return query; - } - return buildResourcePrefix(isMulti ? QueryLangParser.TASKS : QueryLangParser.TASK) + query; + return doEnsurePrefix(query, isMulti, QueryLangParser.TASKS, QueryLangParser.TASK); } /** diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/userresource/UserSearchService.java b/src/main/java/com/netgrif/application/engine/pfql/service/userresource/UserSearchService.java index 3bdc5fcb1a9..eeca34b2200 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/userresource/UserSearchService.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/userresource/UserSearchService.java @@ -11,11 +11,6 @@ import org.springframework.data.domain.Page; import org.springframework.stereotype.Service; -import java.util.List; - -import static com.netgrif.application.engine.pfql.service.utils.SearchUtils.buildResourcePrefix; -import static com.netgrif.application.engine.pfql.service.utils.SearchUtils.hasResourcePrefix; - /** * Service for searching and querying user resources using PFQL (Process Flow Query Language). *

@@ -27,7 +22,6 @@ @Slf4j @Service public class UserSearchService extends AbstractResourceSearchService { - protected static List allowedResourcePrefixes = List.of(QueryLangParser.USER, QueryLangParser.USERS); protected final IUserService userService; @@ -48,10 +42,7 @@ public QueryType getQueryResourceType() { @Override protected String ensurePrefix(String query, boolean isMulti) { - if (query == null || hasResourcePrefix(query, allowedResourcePrefixes)) { - return query; - } - return buildResourcePrefix(isMulti ? QueryLangParser.USERS : QueryLangParser.USER) + query; + return doEnsurePrefix(query, isMulti, QueryLangParser.USERS, QueryLangParser.USER); } /** From eadfa75b5e37eac9b90ea563e152caa8f106a80e Mon Sep 17 00:00:00 2001 From: chvostek Date: Fri, 11 Sep 2026 13:51:44 +0200 Subject: [PATCH 10/11] [NAE-2483] Action API Improvements - handle formatter edge cases --- .../engine/pfql/service/utils/SearchUtils.java | 10 ++++++++++ .../formatters/QueryLangPlaceholderHandlerTest.java | 8 +++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/utils/SearchUtils.java b/src/main/java/com/netgrif/application/engine/pfql/service/utils/SearchUtils.java index 49b14b16f59..1837d232158 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/utils/SearchUtils.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/utils/SearchUtils.java @@ -469,6 +469,9 @@ public static String formatPlaceholders(String query, QueryLangPlaceholderHandle if (args == null || args.length == 0) { return query; } + if (query == null) { + throw new IllegalArgumentException("Query cannot be null when placeholder arguments are provided."); + } StringBuilder result = new StringBuilder(query); int argIndex = 0; @@ -483,6 +486,13 @@ public static String formatPlaceholders(String query, QueryLangPlaceholderHandle result.replace(idx, idx + 2, replacement); searchFrom = idx + replacement.length(); } + if (argIndex < args.length) { + throw new IllegalArgumentException( + "Too many placeholder arguments supplied: expected " + argIndex + " but got " + args.length + "."); + } + if (result.indexOf("{}", searchFrom) != -1) { + throw new IllegalArgumentException("Too many placeholders present: not enough arguments provided."); + } return result.toString(); } diff --git a/src/test/java/com/netgrif/application/engine/pfql/formatters/QueryLangPlaceholderHandlerTest.java b/src/test/java/com/netgrif/application/engine/pfql/formatters/QueryLangPlaceholderHandlerTest.java index 35e44792096..780897e1a64 100644 --- a/src/test/java/com/netgrif/application/engine/pfql/formatters/QueryLangPlaceholderHandlerTest.java +++ b/src/test/java/com/netgrif/application/engine/pfql/formatters/QueryLangPlaceholderHandlerTest.java @@ -568,7 +568,13 @@ public void testUser_MultiplePlaceholders_ObjectIdAndStringList() { @Test public void testUnsupportedType_ThrowsException() { - assertThrows(IllegalArgumentException.class, () -> placeholderHandler.format(new Object())); + assertThrows(IllegalArgumentException.class, () -> formatPlaceholders("user: name contains {}", placeholderHandler, new Object())); + } + + @Test + public void testWrongNumOfArgs_ThrowsException() { + assertThrows(IllegalArgumentException.class, () -> formatPlaceholders("user: name contains {}", placeholderHandler, "John", "Small")); + assertThrows(IllegalArgumentException.class, () -> formatPlaceholders("user: name contains {} {}", placeholderHandler, "John")); } @Test From de0a96f633e3ad5a20afe2efd0e314d7e2470f54 Mon Sep 17 00:00:00 2001 From: chvostek Date: Fri, 11 Sep 2026 15:10:39 +0200 Subject: [PATCH 11/11] [NAE-2483] Action API Improvements - fix test --- .../pfql/formatters/QueryLangPlaceholderHandlerTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/netgrif/application/engine/pfql/formatters/QueryLangPlaceholderHandlerTest.java b/src/test/java/com/netgrif/application/engine/pfql/formatters/QueryLangPlaceholderHandlerTest.java index 780897e1a64..865d0fcc87a 100644 --- a/src/test/java/com/netgrif/application/engine/pfql/formatters/QueryLangPlaceholderHandlerTest.java +++ b/src/test/java/com/netgrif/application/engine/pfql/formatters/QueryLangPlaceholderHandlerTest.java @@ -159,7 +159,8 @@ public void testCase_CaseRef() { caseOptionField.setComponent(new Component("caseref")); caseOptionField.setOptions(Map.of("507f1f77bcf86cd799439011", new I18nString(), "507f1f77bcf86cd799439012", new I18nString())); String query2 = formatPlaceholders("case: id in {}", placeholderHandler, caseOptionField); - assertEquals("case: id in ('507f1f77bcf86cd799439011', '507f1f77bcf86cd799439012')", query2); + assertTrue(query2.equals("case: id in ('507f1f77bcf86cd799439011', '507f1f77bcf86cd799439012')") + || query2.equals("case: id in ('507f1f77bcf86cd799439012', '507f1f77bcf86cd799439011')")); assertDoesNotThrow(() -> evaluateQuery(query2)); }