From 3e96187b04f2616832a64f8986da9b9b7acb98a5 Mon Sep 17 00:00:00 2001 From: chvostek Date: Wed, 15 Jul 2026 13:02:06 +0200 Subject: [PATCH 01/20] [NAE-2466] Advanced search for PFQL - add todos --- .../engine/pfql/domain/antlr4/QueryLang.g4 | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/pfql/domain/antlr4/QueryLang.g4 b/src/main/java/com/netgrif/application/engine/pfql/domain/antlr4/QueryLang.g4 index 55867f5289..cf38edbf6e 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/domain/antlr4/QueryLang.g4 +++ b/src/main/java/com/netgrif/application/engine/pfql/domain/antlr4/QueryLang.g4 @@ -107,27 +107,27 @@ processComparisons: idComparison ; caseComparisons: idComparison - | processIdObjIdComparison + | processIdObjIdComparison // todo 2466 not in FE | processIdentifierComparison | titleComparison | creationDateComparison | authorComparison - | placesComparison - | tasksStateComparison - | tasksUserIdComparison + | placesComparison // todo 2466 not in FE + | tasksStateComparison // todo 2466 not in FE + | tasksUserIdComparison // todo 2466 not in FE | dataValueComparison - | dataOptionsComparison + | dataOptionsComparison // todo 2466 not in FE ; -taskComparisons: idComparison +taskComparisons: idComparison // todo 2466 not in FE | transitionIdComparison - | titleComparison - | stateComparison + | titleComparison // todo 2466 not in FE + | stateComparison // todo 2466 not in FE | userIdComparison - | caseIdComparison + | caseIdComparison // todo 2466 not in FE | processIdComparison - | lastAssignComparison - | lastFinishComparison + | lastAssignComparison // todo 2466 not in FE + | lastFinishComparison // todo 2466 not in FE ; userComparisons: idComparison From 0930e5208c85d519d43890dc1a61de5fc548a801 Mon Sep 17 00:00:00 2001 From: chvostek Date: Sat, 18 Jul 2026 10:44:57 +0200 Subject: [PATCH 02/20] [NAE-2466] Advanced search for PFQL - fix handling white spaces in PFQL for elasticsearch query - fix equality query for case.title - --- .../application/engine/pfql/service/QueryLangEvaluator.java | 6 +++++- .../application/engine/pfql/service/utils/SearchUtils.java | 3 +++ .../application/engine/workflow/web/WorkflowController.java | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/QueryLangEvaluator.java b/src/main/java/com/netgrif/application/engine/pfql/service/QueryLangEvaluator.java index cb55327ffd..d53ae38243 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/QueryLangEvaluator.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/QueryLangEvaluator.java @@ -591,7 +591,11 @@ public void exitTitleBasic(QueryLangParser.TitleBasicContext ctx) { break; case CASE: stringPath = QCase.case$.title; - setElasticQuery(ctx, buildElasticQuery("title", op.getType(), string, not)); + String elasticAttribute = "title"; + if (op.getType() == QueryLangParser.EQ || op.getType() == QueryLangParser.NEQ) { + elasticAttribute += ".keyword"; + } + setElasticQuery(ctx, buildElasticQuery(elasticAttribute, op.getType(), string, not)); break; case TASK: stringPath = QTask.task.title.defaultValue; 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 4a4f5ddf04..9d0231eef2 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 @@ -400,6 +400,9 @@ public static Predicate buildDateTimePredicateInRange(DateTimePath searchPfql(@RequestBody SingleCaseSearchRequestA ResourceLinkAssembler.addLinks(resources, ElasticCase.class, selfLink.getRel().toString()); return resources; } catch (IllegalArgumentException e) { + log.warn("Bad request when calling PFQL search API: {}", e.getMessage()); throw new BadRequestException("Bad request: " + e.getMessage()); } catch (Exception e) { log.error("Something went wrong while searching cases by PFQL", e); From 447beceb93bf02195dca82675823464b5babd3f2 Mon Sep 17 00:00:00 2001 From: chvostek Date: Sat, 18 Jul 2026 10:54:38 +0200 Subject: [PATCH 03/20] [NAE-2466] Advanced search for PFQL - fix test --- .../application/engine/pfql/QueryLangTest.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/test/java/com/netgrif/application/engine/pfql/QueryLangTest.java b/src/test/java/com/netgrif/application/engine/pfql/QueryLangTest.java index 1a96ab8bf1..8754469974 100644 --- a/src/test/java/com/netgrif/application/engine/pfql/QueryLangTest.java +++ b/src/test/java/com/netgrif/application/engine/pfql/QueryLangTest.java @@ -1061,40 +1061,40 @@ public void testComplexElasticCaseQuery() { // and comparison actual = evaluateQuery(String.format("case: id eq '%s' and title eq 'test'", GENERIC_OBJECT_ID)).getFullElasticQuery(); - expected = String.format("stringId:%s AND title:test", GENERIC_OBJECT_ID); + expected = String.format("stringId:%s AND title.keyword:test", GENERIC_OBJECT_ID); assertEquals(expected, actual); // and not comparison actual = evaluateQuery(String.format("case: id eq '%s' and title not eq 'test'", GENERIC_OBJECT_ID)).getFullElasticQuery(); - expected = String.format("stringId:%s AND NOT title:test", GENERIC_OBJECT_ID); + expected = String.format("stringId:%s AND NOT title.keyword:test", GENERIC_OBJECT_ID); assertEquals(expected, actual); actual = evaluateQuery(String.format("case: id eq '%s' and title != 'test'", GENERIC_OBJECT_ID)).getFullElasticQuery(); - expected = String.format("stringId:%s AND NOT title:test", GENERIC_OBJECT_ID); + expected = String.format("stringId:%s AND NOT title.keyword:test", GENERIC_OBJECT_ID); assertEquals(expected, actual); // or comparison actual = evaluateQuery(String.format("case: id eq '%s' or title eq 'test'", GENERIC_OBJECT_ID)).getFullElasticQuery(); - expected = String.format("stringId:%s OR title:test", GENERIC_OBJECT_ID); + expected = String.format("stringId:%s OR title.keyword:test", GENERIC_OBJECT_ID); assertEquals(expected, actual); // or not comparison actual = evaluateQuery(String.format("case: id eq '%s' or title not eq 'test'", GENERIC_OBJECT_ID)).getFullElasticQuery(); - expected = String.format("stringId:%s OR NOT title:test", GENERIC_OBJECT_ID); + expected = String.format("stringId:%s OR NOT title.keyword:test", GENERIC_OBJECT_ID); assertEquals(expected, actual); actual = evaluateQuery(String.format("case: id eq '%s' or title neq 'test'", GENERIC_OBJECT_ID)).getFullElasticQuery(); - expected = String.format("stringId:%s OR NOT title:test", GENERIC_OBJECT_ID); + expected = String.format("stringId:%s OR NOT title.keyword:test", GENERIC_OBJECT_ID); assertEquals(expected, actual); // parenthesis comparison actual = evaluateQuery(String.format("case: id eq '%s' and (title eq 'test' or title eq 'test1')", GENERIC_OBJECT_ID)).getFullElasticQuery(); - expected = String.format("stringId:%s AND (title:test OR title:test1)", GENERIC_OBJECT_ID); + expected = String.format("stringId:%s AND (title.keyword:test OR title.keyword:test1)", GENERIC_OBJECT_ID); assertEquals(expected, actual); // nested parenthesis comparison actual = evaluateQuery(String.format("case: id eq '%s' and (title eq 'test' or (title eq 'test1' and processIdentifier eq 'test'))", GENERIC_OBJECT_ID)).getFullElasticQuery(); - expected = String.format("stringId:%s AND (title:test OR (title:test1 AND processIdentifier:test))", GENERIC_OBJECT_ID); + expected = String.format("stringId:%s AND (title.keyword:test OR (title.keyword:test1 AND processIdentifier:test))", GENERIC_OBJECT_ID); assertEquals(expected, actual); } From 6a1a0bad0cb46e9b64415fcf8f43504aec6d0cd2 Mon Sep 17 00:00:00 2001 From: chvostek Date: Mon, 20 Jul 2026 08:04:42 +0200 Subject: [PATCH 04/20] [NAE-2466] Advanced search for PFQL - fix string values with white spaces --- .../engine/pfql/service/utils/SearchUtils.java | 6 ++---- .../application/engine/pfql/QueryLangTest.java | 12 ++++-------- 2 files changed, 6 insertions(+), 12 deletions(-) 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 9d0231eef2..b0eb94b9cb 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 @@ -180,7 +180,8 @@ public static String explainQuery(String input) { } public static String getStringValue(String queryLangString) { - return queryLangString.replace("'", ""); + String result = queryLangString.replace("'", ""); + return result.contains(" ") ? "\"" + result + "\"" : result; } public static ObjectId getObjectIdValue(String queryLangString) { @@ -400,9 +401,6 @@ public static Predicate buildDateTimePredicateInRange(DateTimePath mongoDbUtils, String res private static void checkStringComparisonElastic(String resource, String attribute, String resultAttribute) { String actual = evaluateQuery(String.format("%s: %s eq 'test'", resource, attribute)).getFullElasticQuery(); - String expected; - if (resultAttribute.matches("dataSet\\.[^.]*\\.fulltextValue")) { - expected = String.format("%s.keyword:test", resultAttribute); - } else { - expected = String.format("%s:test", resultAttribute); - } + String expectedWithKeyword = String.format("%s.keyword:test", resultAttribute); + String expectedWithoutKeyword = String.format("%s:test", resultAttribute); - assertEquals(expected, actual); + assertTrue(actual.equals(expectedWithKeyword) || actual.equals(expectedWithoutKeyword)); actual = evaluateQuery(String.format("%s: %s contains 'test'", resource, attribute)).getFullElasticQuery(); - expected = String.format("%s:*test*", resultAttribute); + String expected = String.format("%s:*test*", resultAttribute); assertEquals(expected, actual); From f87cb3e906fe0b5faf1606d432c051ff19ded500 Mon Sep 17 00:00:00 2001 From: chvostek Date: Tue, 21 Jul 2026 10:55:43 +0200 Subject: [PATCH 05/20] [NAE-2466] Advanced search for PFQL - implement null comparison in PFQL --- .../engine/pfql/domain/antlr4/QueryLang.g4 | 45 +++- .../pfql/domain/enums/ComparisonType.java | 3 +- .../pfql/service/QueryLangEvaluator.java | 246 ++++++++++++++++++ .../pfql/service/utils/SearchUtils.java | 3 +- .../engine/pfql/QueryLangTest.java | 242 ++++++++++++++++- src/test/resources/petriNets/pfql.xml | 5 + 6 files changed, 520 insertions(+), 24 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/pfql/domain/antlr4/QueryLang.g4 b/src/main/java/com/netgrif/application/engine/pfql/domain/antlr4/QueryLang.g4 index cf38edbf6e..4eb83ae1fa 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/domain/antlr4/QueryLang.g4 +++ b/src/main/java/com/netgrif/application/engine/pfql/domain/antlr4/QueryLang.g4 @@ -107,27 +107,27 @@ processComparisons: idComparison ; caseComparisons: idComparison - | processIdObjIdComparison // todo 2466 not in FE + | processIdObjIdComparison | processIdentifierComparison | titleComparison | creationDateComparison | authorComparison - | placesComparison // todo 2466 not in FE - | tasksStateComparison // todo 2466 not in FE - | tasksUserIdComparison // todo 2466 not in FE + | placesComparison + | tasksStateComparison + | tasksUserIdComparison | dataValueComparison - | dataOptionsComparison // todo 2466 not in FE + | dataOptionsComparison ; -taskComparisons: idComparison // todo 2466 not in FE +taskComparisons: idComparison | transitionIdComparison - | titleComparison // todo 2466 not in FE - | stateComparison // todo 2466 not in FE + | titleComparison + | stateComparison | userIdComparison - | caseIdComparison // todo 2466 not in FE + | caseIdComparison | processIdComparison - | lastAssignComparison // todo 2466 not in FE - | lastFinishComparison // todo 2466 not in FE + | lastAssignComparison + | lastFinishComparison ; userComparisons: idComparison @@ -139,69 +139,86 @@ userComparisons: idComparison // attribute comparisons idComparison: ID SPACE objectIdComparison # idBasic | ID SPACE inListStringComparison # idList + | ID SPACE nullComparison # idNull ; titleComparison: TITLE SPACE stringComparison # titleBasic | TITLE SPACE inListStringComparison # titleList | TITLE SPACE inRangeStringComparison # titleRange + | TITLE SPACE nullComparison # titleNull ; identifierComparison: IDENTIFIER SPACE stringComparison # identifierBasic | IDENTIFIER SPACE inListStringComparison # identifierList | IDENTIFIER SPACE inRangeStringComparison # identifierRange + | IDENTIFIER SPACE nullComparison # identifierNull ; versionComparison: VERSION SPACE (NOT SPACE?)? op=(EQ | LT | GT | LTE | GTE) SPACE VERSION_NUMBER # versionBasic | VERSION SPACE inListVersionComparison # versionListCmp | VERSION SPACE inRangeVersionComparison # versionRangeCmp + | VERSION SPACE nullComparison # versionNull ; creationDateComparison: CREATION_DATE SPACE dateComparison # cdDateBasic | CREATION_DATE SPACE dateTimeComparison # cdDateTimeBasic | CREATION_DATE SPACE inListDateComparison # cdDateList | CREATION_DATE SPACE inRangeDateComparison # cdDateRange + | CREATION_DATE SPACE nullComparison # cdNull ; processIdComparison: PROCESS_ID SPACE stringComparison # processIdBasic | PROCESS_ID SPACE inListStringComparison # processIdList + | PROCESS_ID SPACE nullComparison # processIdNull ; processIdObjIdComparison: PROCESS_ID SPACE objectIdComparison # processIdObjIdBasic | PROCESS_ID SPACE inListStringComparison # processIdObjIdList + | PROCESS_ID SPACE nullComparison # processIdObjNull ; processIdentifierComparison: PROCESS_IDENTIFIER SPACE stringComparison # processIdentifierBasic | PROCESS_IDENTIFIER SPACE inListStringComparison # processIdentifierList | PROCESS_IDENTIFIER SPACE inRangeStringComparison # processIdentifierRange + | PROCESS_IDENTIFIER SPACE nullComparison # processIdentifierNull ; authorComparison: AUTHOR SPACE stringComparison # authorBasic | AUTHOR SPACE inListStringComparison # authorList + | AUTHOR SPACE nullComparison # authorNull ; transitionIdComparison: TRANSITION_ID SPACE stringComparison # transitionIdBasic | TRANSITION_ID SPACE inListStringComparison # transitionIdList | TRANSITION_ID SPACE inRangeStringComparison # transitionIdRange + | TRANSITION_ID SPACE nullComparison # transitionIdNull ; stateComparison: STATE SPACE EQ SPACE state=(ENABLED | DISABLED) ; userIdComparison: USER_ID SPACE stringComparison # userIdBasic | USER_ID SPACE inListStringComparison # userIdList + | USER_ID SPACE nullComparison # userIdNull ; caseIdComparison: CASE_ID SPACE stringComparison # caseIdBasic | CASE_ID SPACE inListStringComparison # caseIdList + | CASE_ID SPACE nullComparison # caseIdNull ; lastAssignComparison: LAST_ASSIGN SPACE dateComparison # laDateBasic | LAST_ASSIGN SPACE dateTimeComparison # laDateTimeBasic | LAST_ASSIGN SPACE inListDateComparison # laDateList | LAST_ASSIGN SPACE inRangeDateComparison # laDateRange + | LAST_ASSIGN SPACE nullComparison # laNull ; lastFinishComparison: LAST_FINISH SPACE dateComparison # lfDateBasic | LAST_FINISH SPACE dateTimeComparison # lfDateTimeBasic | LAST_FINISH SPACE inListDateComparison # lfDateList | LAST_FINISH SPACE inRangeDateComparison # lfDateRange + | LAST_FINISH SPACE nullComparison # lfNull ; nameComparison: NAME SPACE stringComparison # nameBasic | NAME SPACE inListStringComparison # nameList | NAME SPACE inRangeStringComparison # nameRange + | NAME SPACE nullComparison # nameNull ; surnameComparison: SURNAME SPACE stringComparison # surnameBasic | SURNAME SPACE inListStringComparison # surnameList | SURNAME SPACE inRangeStringComparison # surnameRange + | SURNAME SPACE nullComparison # surnameNull ; emailComparison: EMAIL SPACE stringComparison # emailBasic | EMAIL SPACE inListStringComparison # emailList | EMAIL SPACE inRangeStringComparison # emailRange + | EMAIL SPACE nullComparison # emailNull ; dataValueComparison: dataValue SPACE stringComparison # dataString | dataValue SPACE numberComparison # dataNumber @@ -214,18 +231,22 @@ dataValueComparison: dataValue SPACE stringComparison # dataString | dataValue SPACE inRangeStringComparison # dataStringRange | dataValue SPACE inRangeNumberComparison # dataNumberRange | dataValue SPACE inRangeDateComparison # dataDateRange + | dataValue SPACE nullComparison # dataNull ; dataOptionsComparison: dataOptions SPACE stringComparison # dataOptionsBasic | dataOptions SPACE inListStringComparison # dataOptionsList | dataOptions SPACE inRangeStringComparison # dataOptionsRange + | dataOptions SPACE nullComparison # dataOptionsNull ; placesComparison: places SPACE numberComparison # placesBasic | places SPACE inListNumberComparison # placesList | places SPACE inRangeNumberComparison # placesRange + | places SPACE nullComparison # placesNull ; tasksStateComparison: tasksState SPACE (NOT SPACE?)? op=EQ SPACE state=(ENABLED | DISABLED) ; tasksUserIdComparison: tasksUserId SPACE stringComparison # tasksUserIdBasic | tasksUserId SPACE inListStringComparison # tasksUserIdList + | tasksUserId SPACE nullComparison # tasksUserIdNull ; // basic comparisons @@ -235,6 +256,7 @@ numberComparison: (NOT SPACE?)? op=(EQ | NEQ | LT | GT | LTE | GTE) SPACE number dateComparison: (NOT SPACE?)? op=(EQ | NEQ | LT | GT | LTE | GTE) SPACE DATE ; dateTimeComparison: (NOT SPACE?)? op=(EQ | NEQ | LT | GT | LTE | GTE) SPACE DATETIME ; booleanComparison: (NOT SPACE?)? op=(EQ | NEQ) SPACE (BOOLEAN | LOGGED_USER_ANONYMOUS) ; +nullComparison: (NOT SPACE?)? op=(EQ | NEQ) SPACE NULL ; // in list/in range comparisons inListStringComparison: (NOT SPACE?)? op=IN SPACE? stringList ; @@ -346,6 +368,7 @@ DATETIME: DATE 'T' ([01] DIGIT | '2' [0-3]) ':' [0-5] DIGIT ':' [0-5] DIGIT ('.' DATE: DIGIT DIGIT DIGIT DIGIT '-' ('0' [1-9] | '1' [0-2]) '-' ('0' [1-9] | [12] DIGIT | '3' [01]) ; // 2020-03-03 // todo NAE-1997: format BOOLEAN: T R U E | F A L S E ; VERSION_NUMBER: DIGIT+ '.' DIGIT+ '.' DIGIT+ ; +NULL: N U L L ; JAVA_ID: [a-zA-Z$_] [a-zA-Z0-9$_]* ; SPACE: [ ]+ ; diff --git a/src/main/java/com/netgrif/application/engine/pfql/domain/enums/ComparisonType.java b/src/main/java/com/netgrif/application/engine/pfql/domain/enums/ComparisonType.java index acb085c393..e1d39fddd1 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/domain/enums/ComparisonType.java +++ b/src/main/java/com/netgrif/application/engine/pfql/domain/enums/ComparisonType.java @@ -7,5 +7,6 @@ public enum ComparisonType { DATE, DATETIME, BOOLEAN, - OPTIONS + OPTIONS, + NULL } diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/QueryLangEvaluator.java b/src/main/java/com/netgrif/application/engine/pfql/service/QueryLangEvaluator.java index d53ae38243..da65be1429 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/QueryLangEvaluator.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/QueryLangEvaluator.java @@ -1578,4 +1578,250 @@ public void exitUserSorting(QueryLangParser.UserSortingContext ctx) { sortOrders.add(new Sort.Order(dir, prop)); }); } + + @Override + public void exitIdNull(QueryLangParser.IdNullContext ctx) { + Predicate mongoQuery; + Token op = ctx.nullComparison().op; + boolean isNotNull = shouldBeNotNull(ctx.nullComparison()); + checkOp(ComparisonType.NULL, op); + + switch (resourceType) { + case PROCESS: + mongoQuery = isNotNull ? QPetriNet.petriNet._id.isNotNull() : QPetriNet.petriNet._id.isNull(); + break; + case CASE: + mongoQuery = isNotNull ? QCase.case$._id.isNotNull() : QCase.case$._id.isNull(); + setElasticQuery(ctx, isNotNull ? "_exists_:stringId" : "!(_exists_:stringId)"); + break; + case TASK: + mongoQuery = isNotNull ? QTask.task._id.isNotNull() : QTask.task._id.isNull(); + break; + case USER: + mongoQuery = isNotNull ? QUser.user._id.isNotNull() : QUser.user._id.isNull(); + break; + default: + throw new IllegalArgumentException("Unknown query type: " + resourceType); + } + + setMongoQuery(ctx, mongoQuery); + } + + @Override + public void exitTitleNull(QueryLangParser.TitleNullContext ctx) { + Predicate mongoQuery; + Token op = ctx.nullComparison().op; + boolean isNotNull = shouldBeNotNull(ctx.nullComparison()); + checkOp(ComparisonType.NULL, op); + + switch (resourceType) { + case PROCESS: + mongoQuery = isNotNull ? QPetriNet.petriNet.title.isNotNull() : QPetriNet.petriNet.title.isNull(); + break; + case CASE: + mongoQuery = isNotNull ? QCase.case$.title.isNotNull() : QCase.case$.title.isNull(); + setElasticQuery(ctx, isNotNull ? "_exists_:title" : "!(_exists_:title)"); + break; + case TASK: + mongoQuery = isNotNull ? QTask.task.title.isNotNull() : QTask.task.title.isNull(); + break; + default: + throw new IllegalArgumentException("Unknown query type: " + resourceType); + } + + setMongoQuery(ctx, mongoQuery); + } + + @Override + public void exitIdentifierNull(QueryLangParser.IdentifierNullContext ctx) { + Token op = ctx.nullComparison().op; + checkOp(ComparisonType.NULL, op); + boolean isNotNull = shouldBeNotNull(ctx.nullComparison()); + setMongoQuery(ctx, isNotNull ? QPetriNet.petriNet.identifier.isNotNull() : QPetriNet.petriNet.identifier.isNull()); + } + + @Override + public void exitVersionNull(QueryLangParser.VersionNullContext ctx) { + Token op = ctx.nullComparison().op; + checkOp(ComparisonType.NULL, op); + boolean isNotNull = shouldBeNotNull(ctx.nullComparison()); + setMongoQuery(ctx, isNotNull ? QPetriNet.petriNet.version.isNotNull() : QPetriNet.petriNet.version.isNull()); + } + + @Override + public void exitCdNull(QueryLangParser.CdNullContext ctx) { + Token op = ctx.nullComparison().op; + checkOp(ComparisonType.NULL, op); + boolean isNotNull = shouldBeNotNull(ctx.nullComparison()); + + Predicate mongoQuery; + switch (resourceType) { + case PROCESS: + mongoQuery = isNotNull ? QPetriNet.petriNet.creationDate.isNotNull() : QPetriNet.petriNet.creationDate.isNull(); + break; + case CASE: + mongoQuery = isNotNull ? QCase.case$.creationDate.isNotNull() : QCase.case$.creationDate.isNull(); + setElasticQuery(ctx, isNotNull ? "_exists_:creationDateSortable" : "!(_exists_:creationDateSortable)"); + break; + default: + throw new IllegalArgumentException("Unknown query type: " + resourceType); + } + + setMongoQuery(ctx, mongoQuery); + } + + @Override + public void exitProcessIdNull(QueryLangParser.ProcessIdNullContext ctx) { + Token op = ctx.nullComparison().op; + checkOp(ComparisonType.NULL, op); + boolean isNotNull = shouldBeNotNull(ctx.nullComparison()); + setMongoQuery(ctx, isNotNull ? QTask.task.processId.isNotNull() : QTask.task.processId.isNull() ); + } + + @Override + public void exitProcessIdObjNull(QueryLangParser.ProcessIdObjNullContext ctx) { + Token op = ctx.nullComparison().op; + checkOp(ComparisonType.NULL, op); + boolean isNotNull = shouldBeNotNull(ctx.nullComparison()); + setMongoQuery(ctx, isNotNull ? QCase.case$.petriNetObjectId.isNotNull() : QCase.case$.petriNetObjectId.isNull()); + setElasticQuery(ctx, isNotNull ? "_exists_:processId" : "!(_exists_:processId)"); + } + + @Override + public void exitProcessIdentifierNull(QueryLangParser.ProcessIdentifierNullContext ctx) { + Token op = ctx.nullComparison().op; + checkOp(ComparisonType.NULL, op); + boolean isNotNull = shouldBeNotNull(ctx.nullComparison()); + setMongoQuery(ctx, isNotNull ? QCase.case$.processIdentifier.isNotNull() : QCase.case$.processIdentifier.isNull()); + setElasticQuery(ctx, isNotNull ? "_exists_:processIdentifier" : "!(_exists_:processIdentifier)"); + } + + @Override + public void exitAuthorNull(QueryLangParser.AuthorNullContext ctx) { + Token op = ctx.nullComparison().op; + checkOp(ComparisonType.NULL, op); + boolean isNotNull = shouldBeNotNull(ctx.nullComparison()); + setMongoQuery(ctx, isNotNull ? QCase.case$.author.id.isNotNull() : QCase.case$.author.id.isNull()); + setElasticQuery(ctx, isNotNull ? "_exists_:author" : "!(_exists_:author)"); + } + + @Override + public void exitTransitionIdNull(QueryLangParser.TransitionIdNullContext ctx) { + Token op = ctx.nullComparison().op; + checkOp(ComparisonType.NULL, op); + boolean isNotNull = shouldBeNotNull(ctx.nullComparison()); + setMongoQuery(ctx, isNotNull ? QTask.task.transitionId.isNotNull() : QTask.task.transitionId.isNull()); + } + + @Override + public void exitUserIdNull(QueryLangParser.UserIdNullContext ctx) { + Token op = ctx.nullComparison().op; + checkOp(ComparisonType.NULL, op); + boolean isNotNull = shouldBeNotNull(ctx.nullComparison()); + setMongoQuery(ctx, isNotNull ? QTask.task.userId.isNotNull() : QTask.task.userId.isNull()); + } + + @Override + public void exitLfNull(QueryLangParser.LfNullContext ctx) { + // todo implement lastFinished + } + + @Override + public void exitCaseIdNull(QueryLangParser.CaseIdNullContext ctx) { + Token op = ctx.nullComparison().op; + checkOp(ComparisonType.NULL, op); + boolean isNotNull = shouldBeNotNull(ctx.nullComparison()); + setMongoQuery(ctx, isNotNull ? QTask.task.caseId.isNotNull() : QTask.task.caseId.isNull()); + } + + @Override + public void exitLaNull(QueryLangParser.LaNullContext ctx) { + // todo implement lastAssigned + } + + @Override + public void exitNameNull(QueryLangParser.NameNullContext ctx) { + Token op = ctx.nullComparison().op; + checkOp(ComparisonType.NULL, op); + boolean isNotNull = shouldBeNotNull(ctx.nullComparison()); + setMongoQuery(ctx, isNotNull ? QUser.user.name.isNotNull() : QUser.user.name.isNull()); + } + + @Override + public void exitSurnameNull(QueryLangParser.SurnameNullContext ctx) { + Token op = ctx.nullComparison().op; + checkOp(ComparisonType.NULL, op); + boolean isNotNull = shouldBeNotNull(ctx.nullComparison()); + setMongoQuery(ctx, isNotNull ? QUser.user.surname.isNotNull() : QUser.user.surname.isNull()); + } + + @Override + public void exitEmailNull(QueryLangParser.EmailNullContext ctx) { + Token op = ctx.nullComparison().op; + checkOp(ComparisonType.NULL, op); + boolean isNotNull = shouldBeNotNull(ctx.nullComparison()); + setMongoQuery(ctx, isNotNull ? QUser.user.email.isNotNull() : QUser.user.email.isNull()); + } + + @Override + public void exitDataNull(QueryLangParser.DataNullContext ctx) { + Token op = ctx.nullComparison().op; + checkOp(ComparisonType.NULL, op); + boolean isNotNull = shouldBeNotNull(ctx.nullComparison()); + String fieldId = ctx.dataValue().fieldId.getText(); + String elasticAttribute = "dataSet." + fieldId + ".fulltextValue"; + + setMongoQuery(ctx, null); + setElasticQuery(ctx, isNotNull ? "_exists_:" + elasticAttribute : "!(_exists_:" + elasticAttribute + ")"); + this.searchWithElastic = true; + } + + @Override + public void exitDataOptionsNull(QueryLangParser.DataOptionsNullContext ctx) { + Token op = ctx.nullComparison().op; + checkOp(ComparisonType.NULL, op); + boolean isNotNull = shouldBeNotNull(ctx.nullComparison()); + String fieldId = ctx.dataOptions().fieldId.getText(); + String elasticAttribute = "dataSet." + fieldId + ".options"; + + setMongoQuery(ctx, null); + setElasticQuery(ctx, isNotNull ? "_exists_:" + elasticAttribute : "!(_exists_:" + elasticAttribute + ")"); + this.searchWithElastic = true; + } + + @Override + public void exitPlacesNull(QueryLangParser.PlacesNullContext ctx) { + Token op = ctx.nullComparison().op; + checkOp(ComparisonType.NULL, op); + boolean isNotNull = shouldBeNotNull(ctx.nullComparison()); + String placeId = ctx.places().placeId.getText(); + String elasticAttribute = "places." + placeId + ".marking"; + + setMongoQuery(ctx, null); + setElasticQuery(ctx, isNotNull ? "_exists_:" + elasticAttribute : "!(_exists_:" + elasticAttribute + ")"); + this.searchWithElastic = true; + } + + @Override + public void exitTasksUserIdNull(QueryLangParser.TasksUserIdNullContext ctx) { + Token op = ctx.nullComparison().op; + checkOp(ComparisonType.NULL, op); + boolean isNotNull = shouldBeNotNull(ctx.nullComparison()); + String taskId = ctx.tasksUserId().taskId.getText(); + String elasticAttribute = "tasks." + taskId + ".userId"; + + setMongoQuery(ctx, null); + setElasticQuery(ctx, isNotNull ? "_exists_:" + elasticAttribute : "!(_exists_:" + elasticAttribute + ")"); + this.searchWithElastic = true; + } + + private boolean shouldBeNotNull(QueryLangParser.NullComparisonContext ctx) { + if (ctx == null) { + throw new IllegalArgumentException("Null comparison context must be provided"); + } + if (ctx.EQ() == null && ctx.NEQ() == null) { + throw new IllegalArgumentException("Any of the operators EQ or NEQ must be used"); + } + return ctx.NOT() != null && ctx.EQ() != null || ctx.NOT() == null && ctx.NEQ() != null; + } } 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 b0eb94b9cb..a779452f2c 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 @@ -42,7 +42,8 @@ public class SearchUtils { ComparisonType.NUMBER, List.of(QueryLangParser.EQ, QueryLangParser.NEQ, QueryLangParser.LT, QueryLangParser.LTE, QueryLangParser.GT, QueryLangParser.GTE), ComparisonType.DATE, List.of(QueryLangParser.EQ, QueryLangParser.NEQ, QueryLangParser.LT, QueryLangParser.LTE, QueryLangParser.GT, QueryLangParser.GTE), ComparisonType.DATETIME, List.of(QueryLangParser.EQ, QueryLangParser.NEQ, QueryLangParser.LT, QueryLangParser.LTE, QueryLangParser.GT, QueryLangParser.GTE), - ComparisonType.BOOLEAN, List.of(QueryLangParser.EQ, QueryLangParser.NEQ) + ComparisonType.BOOLEAN, List.of(QueryLangParser.EQ, QueryLangParser.NEQ), + ComparisonType.NULL, List.of(QueryLangParser.EQ, QueryLangParser.NEQ) ); public static final Map processAttrToSortPropMapping = Map.of( diff --git a/src/test/java/com/netgrif/application/engine/pfql/QueryLangTest.java b/src/test/java/com/netgrif/application/engine/pfql/QueryLangTest.java index 227a00e54f..c3c4b5c12e 100644 --- a/src/test/java/com/netgrif/application/engine/pfql/QueryLangTest.java +++ b/src/test/java/com/netgrif/application/engine/pfql/QueryLangTest.java @@ -108,6 +108,16 @@ public void testSearchService() throws InterruptedException { assertEquals(Case.class, case5.getClass()); assertEquals(true, ((Case) case5).getFieldValue("boolean_0")); + Object case6 = searchService.search("case: processIdentifier eq 'query_test' and data.text_1.value neq null"); + assertNotNull(case6); + assertEquals(Case.class, case6.getClass()); + assertNotNull(((Case) case6).getFieldValue("text_1")); + + Object case7 = searchService.search("case: processIdentifier eq 'query_test' and data.text_1.value eq null"); + assertNotNull(case7); + assertEquals(Case.class, case7.getClass()); + assertNull(((Case) case7).getFieldValue("text_1")); + cases = searchService.search("cases: processIdentifier eq 'query_test' and data.boolean_0.value == true"); assertEquals(5, ((Page) cases).getTotalElements()); @@ -139,6 +149,16 @@ public void testSimpleMongodbProcessQuery() { compareMongoQueries(mongoDbUtils, actual, expected); + actual = evaluateQuery("process: id eq null").getFullMongoQuery(); + expected = QPetriNet.petriNet._id.isNull(); + + compareMongoQueries(mongoDbUtils, actual, expected); + + actual = evaluateQuery("process: id neq null").getFullMongoQuery(); + expected = QPetriNet.petriNet._id.isNotNull(); + + compareMongoQueries(mongoDbUtils, actual, expected); + // identifier comparison checkStringComparison(mongoDbUtils, "process", "identifier", QPetriNet.petriNet.identifier); @@ -225,6 +245,16 @@ public void testSimpleMongodbProcessQuery() { compareMongoQueries(mongoDbUtils, actual, expected); + actual = evaluateQuery("process: version not neq null").getFullMongoQuery(); // double negation -> should be null + expected = QPetriNet.petriNet.version.isNull(); + + compareMongoQueries(mongoDbUtils, actual, expected); + + actual = evaluateQuery("process: version not eq null").getFullMongoQuery(); + expected = QPetriNet.petriNet.version.isNotNull(); + + compareMongoQueries(mongoDbUtils, actual, expected); + // title comparison checkStringComparison(mongoDbUtils, "process", "title", QPetriNet.petriNet.title.defaultValue); @@ -337,6 +367,16 @@ public void testSimpleMongodbCaseQuery() { compareMongoQueries(mongoDbUtils, actual, expected); + actual = evaluateQuery("case: id eq null").getFullMongoQuery(); + expected = QCase.case$._id.isNull(); + + compareMongoQueries(mongoDbUtils, actual, expected); + + actual = evaluateQuery("case: id neq null").getFullMongoQuery(); + expected = QCase.case$._id.isNotNull(); + + compareMongoQueries(mongoDbUtils, actual, expected); + // processId comparison actual = evaluateQuery(String.format("case: processId eq '%s'", GENERIC_OBJECT_ID)).getFullMongoQuery(); expected = QCase.case$.petriNetObjectId.eq(GENERIC_OBJECT_ID); @@ -348,6 +388,16 @@ public void testSimpleMongodbCaseQuery() { compareMongoQueries(mongoDbUtils, actual, expected); + actual = evaluateQuery("case: processId eq null").getFullMongoQuery(); + expected = QCase.case$.petriNetObjectId.isNull(); + + compareMongoQueries(mongoDbUtils, actual, expected); + + actual = evaluateQuery("case: processId neq null").getFullMongoQuery(); + expected = QCase.case$.petriNetObjectId.isNotNull(); + + compareMongoQueries(mongoDbUtils, actual, expected); + // processIdentifier comparison checkStringComparison(mongoDbUtils, "case", "processIdentifier", QCase.case$.processIdentifier); @@ -378,6 +428,16 @@ public void testSimpleMongodbCaseQuery() { compareMongoQueries(mongoDbUtils, actual, expected); + actual = evaluateQuery("case: author eq null").getFullMongoQuery(); + expected = QCase.case$.author.id.isNull(); + + compareMongoQueries(mongoDbUtils, actual, expected); + + actual = evaluateQuery("case: author neq null").getFullMongoQuery(); + expected = QCase.case$.author.id.isNotNull(); + + compareMongoQueries(mongoDbUtils, actual, expected); + actual = evaluateQuery("cases: title in ('test1' : loggedUser.username)").getFullMongoQuery(); expected = QCase.case$.title.gt("test1").and(QCase.case$.title.lt(systemUser.getUsername())); @@ -450,6 +510,12 @@ public void testSimpleMongodbCaseQuery() { actual = evaluateQuery("case: data.field1.value eq true").getFullMongoQuery(); assertNull(actual); + actual = evaluateQuery("case: data.field1.value eq null").getFullMongoQuery(); + assertNull(actual); + + actual = evaluateQuery("case: data.field1.value neq null").getFullMongoQuery(); + assertNull(actual); + // data options comparison actual = evaluateQuery("case: data.field1.options eq 'test'").getFullMongoQuery(); assertNull(actual); @@ -547,6 +613,16 @@ public void testSimpleMongodbTaskQuery() { compareMongoQueries(mongoDbUtils, actual, expected); + actual = evaluateQuery("task: id eq null").getFullMongoQuery(); + expected = QTask.task._id.isNull(); + + compareMongoQueries(mongoDbUtils, actual, expected); + + actual = evaluateQuery("task: id neq null").getFullMongoQuery(); + expected = QTask.task._id.isNotNull(); + + compareMongoQueries(mongoDbUtils, actual, expected); + // transitionId comparison checkStringComparison(mongoDbUtils, "task", "transitionId", QTask.task.transitionId); @@ -581,6 +657,16 @@ public void testSimpleMongodbTaskQuery() { compareMongoQueries(mongoDbUtils, actual, expected); + actual = evaluateQuery("task: userId eq null").getFullMongoQuery(); + expected = QTask.task.userId.isNull(); + + compareMongoQueries(mongoDbUtils, actual, expected); + + actual = evaluateQuery("task: userId neq null").getFullMongoQuery(); + expected = QTask.task.userId.isNotNull(); + + compareMongoQueries(mongoDbUtils, actual, expected); + // caseId comparison actual = evaluateQuery("task: caseId eq 'test'").getFullMongoQuery(); expected = QTask.task.caseId.eq("test"); @@ -597,6 +683,16 @@ public void testSimpleMongodbTaskQuery() { compareMongoQueries(mongoDbUtils, actual, expected); + actual = evaluateQuery("task: caseId eq null").getFullMongoQuery(); + expected = QTask.task.caseId.isNull(); + + compareMongoQueries(mongoDbUtils, actual, expected); + + actual = evaluateQuery("task: caseId neq null").getFullMongoQuery(); + expected = QTask.task.caseId.isNotNull(); + + compareMongoQueries(mongoDbUtils, actual, expected); + // processId comparison actual = evaluateQuery("task: processId eq 'test'").getFullMongoQuery(); expected = QTask.task.processId.eq("test"); @@ -613,6 +709,16 @@ public void testSimpleMongodbTaskQuery() { compareMongoQueries(mongoDbUtils, actual, expected); + actual = evaluateQuery("task: processId eq null").getFullMongoQuery(); + expected = QTask.task.processId.isNull(); + + compareMongoQueries(mongoDbUtils, actual, expected); + + actual = evaluateQuery("task: processId neq null").getFullMongoQuery(); + expected = QTask.task.processId.isNotNull(); + + compareMongoQueries(mongoDbUtils, actual, expected); + // lastAssign comparison // TODO: fix // checkDateComparison(mongoDbUtils, "task", "lastAssign", QTask.task.lastAssigned); @@ -718,6 +824,16 @@ public void testSimpleMongodbUserQuery() { compareMongoQueries(mongoDbUtils, actual, expected); + actual = evaluateQuery("user: id eq null").getFullMongoQuery(); + expected = QUser.user._id.isNull(); + + compareMongoQueries(mongoDbUtils, actual, expected); + + actual = evaluateQuery("user: id neq null").getFullMongoQuery(); + expected = QUser.user._id.isNotNull(); + + compareMongoQueries(mongoDbUtils, actual, expected); + // name comparison checkStringComparison(mongoDbUtils, "user", "name", QUser.user.name); @@ -832,41 +948,32 @@ public void testSimpleElasticProcessQuery() { actual = evaluateQuery("process: version lte 1.1.1").getFullElasticQuery(); assertNull(actual); - actual = evaluateQuery("process: version gt 1.1.1").getFullElasticQuery(); assertNull(actual); - actual = evaluateQuery("process: version gte 1.1.1").getFullElasticQuery(); assertNull(actual); - // title comparison actual = evaluateQuery("process: title eq 'test'").getFullElasticQuery(); assertNull(actual); - actual = evaluateQuery("process: title contains 'test'").getFullElasticQuery(); assertNull(actual); - // creationDate comparison actual = evaluateQuery("process: creationDate eq 2011-12-03T10:15:30").getFullElasticQuery(); assertNull(actual); - actual = evaluateQuery("process: creationDate lt 2011-12-03T10:15:30").getFullElasticQuery(); assertNull(actual); - actual = evaluateQuery("process: creationDate lte 2011-12-03T10:15:30").getFullElasticQuery(); assertNull(actual); - actual = evaluateQuery("process: creationDate gt 2011-12-03T10:15:30").getFullElasticQuery(); assertNull(actual); - actual = evaluateQuery("process: creationDate gte 2011-12-03T10:15:30").getFullElasticQuery(); assertNull(actual); } @@ -877,7 +984,6 @@ public void testComplexElasticProcessQuery() { // not comparison String actual = evaluateQuery(String.format("process: id not eq '%s'", GENERIC_OBJECT_ID)).getFullElasticQuery(); assertNull(actual); - assertNull(actual); actual = evaluateQuery(String.format("process: id neq '%s'", GENERIC_OBJECT_ID)).getFullElasticQuery(); assertNull(actual); @@ -912,7 +1018,6 @@ public void testComplexElasticProcessQuery() { actual = evaluateQuery(String.format("process: id eq '%s' and not (title eq 'test' or title eq 'test1')", GENERIC_OBJECT_ID)).getFullElasticQuery(); assertNull(actual); - // nested parenthesis comparison actual = evaluateQuery(String.format("process: id eq '%s' and (title eq 'test' or (title eq 'test1' and identifier eq 'test'))", GENERIC_OBJECT_ID)).getFullElasticQuery(); assertNull(actual); @@ -945,6 +1050,14 @@ public void testSimpleElasticCaseQuery() { expected = String.format("stringId:(%s OR %s)", GENERIC_OBJECT_ID, GENERIC_OBJECT_ID); assertEquals(expected, actual); + actual = evaluateQuery("case: id eq null").getFullElasticQuery(); + expected = "!(_exists_:stringId)"; + assertEquals(expected, actual); + + actual = evaluateQuery("case: id neq null").getFullElasticQuery(); + expected = "_exists_:stringId"; + assertEquals(expected, actual); + // processId comparison actual = evaluateQuery(String.format("case: processId eq '%s'", GENERIC_OBJECT_ID)).getFullElasticQuery(); expected = String.format("processId:%s", GENERIC_OBJECT_ID); @@ -954,6 +1067,14 @@ public void testSimpleElasticCaseQuery() { expected = String.format("processId:(%s OR %s)", GENERIC_OBJECT_ID, GENERIC_OBJECT_ID); assertEquals(expected, actual); + actual = evaluateQuery("case: processId not neq null").getFullElasticQuery(); // double negation -> eq null + expected = "!(_exists_:processId)"; + assertEquals(expected, actual); + + actual = evaluateQuery("case: processId not eq null").getFullElasticQuery(); + expected = "_exists_:processId"; + assertEquals(expected, actual); + // processIdentifier comparison checkStringComparisonElastic("case", "processIdentifier", "processIdentifier"); @@ -985,6 +1106,14 @@ public void testSimpleElasticCaseQuery() { expected = String.format("author:(%s OR %s OR %s)", GENERIC_OBJECT_ID, GENERIC_OBJECT_ID, new ObjectId(systemUser.getId())); assertEquals(expected, actual); + actual = evaluateQuery("case: author eq null").getFullElasticQuery(); + expected = "!(_exists_:author)"; + assertEquals(expected, actual); + + actual = evaluateQuery("case: author neq null").getFullElasticQuery(); + expected = "_exists_:author"; + assertEquals(expected, actual); + // places comparison checkNumberComparisonElastic("case", "places.p1.marking", "places.p1.marking"); @@ -1010,6 +1139,14 @@ public void testSimpleElasticCaseQuery() { expected = String.format("tasks.t1.userId:(%s OR %s)", GENERIC_OBJECT_ID, GENERIC_OBJECT_ID); assertEquals(expected, actual); + actual = evaluateQuery("case: tasks.t1.userId eq null").getFullElasticQuery(); + expected = "!(_exists_:tasks.t1.userId)"; + assertEquals(expected, actual); + + actual = evaluateQuery("case: tasks.t1.userId neq null").getFullElasticQuery(); + expected = "_exists_:tasks.t1.userId"; + assertEquals(expected, actual); + // data value comparison checkStringComparisonElastic("case", "data.field1.value", "dataSet.field1.fulltextValue"); @@ -1029,6 +1166,14 @@ public void testSimpleElasticCaseQuery() { expected = "dataSet.field1.booleanValue:false"; assertEquals(expected, actual); + actual = evaluateQuery("case: data.field1.value eq null").getFullElasticQuery(); + expected = "!(_exists_:dataSet.field1.fulltextValue)"; + assertEquals(expected, actual); + + actual = evaluateQuery("case: data.field1.value neq null").getFullElasticQuery(); + expected = "_exists_:dataSet.field1.fulltextValue"; + assertEquals(expected, actual); + // data options comparison checkStringComparisonElastic("case", "data.field1.options", "dataSet.field1.options"); @@ -1046,6 +1191,14 @@ public void testSimpleElasticCaseQuery() { expected = String.format("(title:>%s AND title:<%s)", systemUser.getUsername(), systemUser.getFullName()); assertEquals(expected, actual); + + actual = evaluateQuery("case: title eq null").getFullElasticQuery(); + expected = "!(_exists_:title)"; + assertEquals(expected, actual); + + actual = evaluateQuery("case: title neq null").getFullElasticQuery(); + expected = "_exists_:title"; + assertEquals(expected, actual); } @Test @@ -1800,6 +1953,8 @@ public void testProcessQueriesFail() { assertThrows(IllegalArgumentException.class, () -> evaluateQuery("process email eq 'test'")); assertThrows(IllegalArgumentException.class, () -> evaluateQuery("process page 2")); assertThrows(IllegalArgumentException.class, () -> evaluateQuery("process:")); + assertThrows(IllegalArgumentException.class, () -> evaluateQuery("process: identifier gt null")); + assertThrows(IllegalArgumentException.class, () -> evaluateQuery("process: identifier contains null")); } @Test @@ -1821,6 +1976,8 @@ public void testCaseQueriesFail() { assertThrows(IllegalArgumentException.class, () -> evaluateQuery("case:")); assertThrows(IllegalArgumentException.class, () -> evaluateQuery("case: creationDate eq loggedUser.id")); assertThrows(IllegalArgumentException.class, () -> evaluateQuery("case: processIdentifier eq loggedUser.anonymous")); + assertThrows(IllegalArgumentException.class, () -> evaluateQuery("case: processIdentifier lt null")); + assertThrows(IllegalArgumentException.class, () -> evaluateQuery("case: title contains null")); } @Test @@ -1841,6 +1998,8 @@ public void testTaskQueriesFail() { assertThrows(IllegalArgumentException.class, () -> evaluateQuery("task email eq 'test'")); assertThrows(IllegalArgumentException.class, () -> evaluateQuery("task page 2")); assertThrows(IllegalArgumentException.class, () -> evaluateQuery("task:")); + assertThrows(IllegalArgumentException.class, () -> evaluateQuery("task: id gte null")); + assertThrows(IllegalArgumentException.class, () -> evaluateQuery("task: userId in (null, 'test')")); } @Test @@ -1946,6 +2105,25 @@ private static void checkStringComparison(MongoDbUtils mongoDbUtils, String r expected = stringPath.gt("test1").and(stringPath.loe("test2")).not(); compareMongoQueries(mongoDbUtils, actual, expected); + + actual = evaluateQuery(String.format("%s: %s eq null", resource, attribute)).getFullMongoQuery(); + expected = stringPath.isNull(); + + if (stringPath.toString().contains(".defaultValue")) { + assertEquals(actual.toString(), expected.toString().replaceAll(".defaultValue", "")); + } else { + compareMongoQueries(mongoDbUtils, actual, expected); + } + + actual = evaluateQuery(String.format("%s: %s neq null", resource, attribute)).getFullMongoQuery(); + expected = stringPath.isNotNull(); + + if (stringPath.toString().contains(".defaultValue")) { + assertEquals(actual.toString(), expected.toString().replaceAll(".defaultValue", "")); + } else { + compareMongoQueries(mongoDbUtils, actual, expected); + } + } private static void checkDateComparison(MongoDbUtils mongoDbUtils, String resource, String attribute, DateTimePath dateTimePath) { @@ -2030,6 +2208,16 @@ private static void checkDateComparison(MongoDbUtils mongoDbUtils, String res expected = dateTimePath.gt(date4).and(dateTimePath.loe(date5)).not(); compareMongoQueries(mongoDbUtils, actual, expected); + + actual = evaluateQuery(String.format("%s: %s eq null", resource, attribute)).getFullMongoQuery(); + expected = dateTimePath.isNull(); + + compareMongoQueries(mongoDbUtils, actual, expected); + + actual = evaluateQuery(String.format("%s: %s neq null", resource, attribute)).getFullMongoQuery(); + expected = dateTimePath.isNotNull(); + + compareMongoQueries(mongoDbUtils, actual, expected); } private static void checkStringComparisonElastic(String resource, String attribute, String resultAttribute) { @@ -2088,6 +2276,16 @@ private static void checkStringComparisonElastic(String resource, String attribu expected = String.format("NOT (%s:>test1 AND %s:<=test2)", resultAttribute, resultAttribute); assertEquals(expected, actual); + + actual = evaluateQuery(String.format("%s: %s eq null", resource, attribute)).getFullElasticQuery(); + expected = String.format("!(_exists_:%s)", resultAttribute); + + assertEquals(expected, actual); + + actual = evaluateQuery(String.format("%s: %s neq null", resource, attribute)).getFullElasticQuery(); + expected = String.format("_exists_:%s", resultAttribute); + + assertEquals(expected, actual); } private static void checkNumberComparisonElastic(String resource, String attribute, String resultAttribute) { @@ -2140,6 +2338,17 @@ private static void checkNumberComparisonElastic(String resource, String attribu expected = String.format("NOT (%s:>1 AND %s:<=2)", resultAttribute, resultAttribute); assertEquals(expected, actual); + + String resultAttributeWithFulltextValue = resultAttribute.replaceAll(".numberValue", ".fulltextValue"); + actual = evaluateQuery(String.format("%s: %s eq null", resource, attribute)).getFullElasticQuery(); + expected = String.format("!(_exists_:%s)", resultAttributeWithFulltextValue); + + assertEquals(expected, actual); + + actual = evaluateQuery(String.format("%s: %s neq null", resource, attribute)).getFullElasticQuery(); + expected = String.format("_exists_:%s", resultAttributeWithFulltextValue); + + assertEquals(expected, actual); } private static void checkDateComparisonElastic(String resource, String attribute, String resultAttribute) { @@ -2224,6 +2433,17 @@ private static void checkDateComparisonElastic(String resource, String attribute expected = String.format("NOT (%s:>%s AND %s:<=%s)", resultAttribute, Timestamp.valueOf(date4).getTime(), resultAttribute, Timestamp.valueOf(date5).getTime()); assertEquals(expected, actual); + + String resultAttributeWithFulltextValue = resultAttribute.replaceAll(".timestampValue", ".fulltextValue"); + actual = evaluateQuery(String.format("%s: %s eq null", resource, attribute)).getFullElasticQuery(); + expected = String.format("!(_exists_:%s)", resultAttributeWithFulltextValue); + + assertEquals(expected, actual); + + actual = evaluateQuery(String.format("%s: %s not eq null", resource, attribute)).getFullElasticQuery(); + expected = String.format("_exists_:%s", resultAttributeWithFulltextValue); + + assertEquals(expected, actual); } private static void compareMongoQueries(MongoDbUtils mongoDbUtils, Predicate actual, Predicate expected) { diff --git a/src/test/resources/petriNets/pfql.xml b/src/test/resources/petriNets/pfql.xml index 1dd151d819..5f8fd2f677 100644 --- a/src/test/resources/petriNets/pfql.xml +++ b/src/test/resources/petriNets/pfql.xml @@ -27,6 +27,11 @@ Text params["id"] + + text_1 + Text + params["id"] == "0" ? null : params["id"] + t1 176 From b8a98f72a7a109d240e627995bd807a764db8ed1 Mon Sep 17 00:00:00 2001 From: chvostek Date: Tue, 21 Jul 2026 13:38:32 +0200 Subject: [PATCH 06/20] [NAE-2466] Advanced search for PFQL - fix handling blank string values --- .../application/engine/pfql/service/utils/SearchUtils.java | 3 +++ 1 file changed, 3 insertions(+) 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 a779452f2c..1f15a9aeea 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 @@ -182,6 +182,9 @@ public static String explainQuery(String input) { public static String getStringValue(String queryLangString) { String result = queryLangString.replace("'", ""); + if (result.isBlank()) { + return "\"\""; + } return result.contains(" ") ? "\"" + result + "\"" : result; } From 79249ffe4fb65ec7090c87aa33d2358ede6626f1 Mon Sep 17 00:00:00 2001 From: chvostek Date: Tue, 21 Jul 2026 16:24:44 +0200 Subject: [PATCH 07/20] [NAE-2466] Advanced search for PFQL - implement fuzzy search in PFQL --- .../engine/pfql/domain/antlr4/QueryLang.g4 | 6 ++- .../pfql/service/QueryLangEvaluator.java | 41 +++++++++++++++++++ .../engine/pfql/QueryLangTest.java | 24 +++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/netgrif/application/engine/pfql/domain/antlr4/QueryLang.g4 b/src/main/java/com/netgrif/application/engine/pfql/domain/antlr4/QueryLang.g4 index 4eb83ae1fa..f1b5d5533d 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/domain/antlr4/QueryLang.g4 +++ b/src/main/java/com/netgrif/application/engine/pfql/domain/antlr4/QueryLang.g4 @@ -142,6 +142,7 @@ idComparison: ID SPACE objectIdComparison # idBasic | ID SPACE nullComparison # idNull ; titleComparison: TITLE SPACE stringComparison # titleBasic + | TITLE SPACE stringLikeComparison # titleLike | TITLE SPACE inListStringComparison # titleList | TITLE SPACE inRangeStringComparison # titleRange | TITLE SPACE nullComparison # titleNull @@ -221,6 +222,7 @@ emailComparison: EMAIL SPACE stringComparison # emailBasic | EMAIL SPACE nullComparison # emailNull ; dataValueComparison: dataValue SPACE stringComparison # dataString + | dataValue SPACE stringLikeComparison # dataStringLike | dataValue SPACE numberComparison # dataNumber | dataValue SPACE dateComparison # dataDate | dataValue SPACE dateTimeComparison # dataDatetime @@ -252,6 +254,7 @@ tasksUserIdComparison: tasksUserId SPACE stringComparison # tasksUserIdBasic // basic comparisons objectIdComparison: (NOT SPACE?)? op=(EQ | NEQ) SPACE (STRING | LOGGED_USER_ID) ; stringComparison: (NOT SPACE?)? op=(EQ | NEQ | CONTAINS | LT | GT | LTE | GTE) SPACE (STRING | loggedUserStringAttribute) ; +stringLikeComparison: stringComparison LIKE ; numberComparison: (NOT SPACE?)? op=(EQ | NEQ | LT | GT | LTE | GTE) SPACE number=(INT | DOUBLE) ; dateComparison: (NOT SPACE?)? op=(EQ | NEQ | LT | GT | LTE | GTE) SPACE DATE ; dateTimeComparison: (NOT SPACE?)? op=(EQ | NEQ | LT | GT | LTE | GTE) SPACE DATETIME ; @@ -299,7 +302,7 @@ LT: L T | '<' ; GT: G T | '>' ; LTE: L T E | '<=' ; GTE: G T E | '>=' ; -CONTAINS: C O N T A I N S | '~'; +CONTAINS: C O N T A I N S | '~' ; IN: I N ; // resurces @@ -369,6 +372,7 @@ DATE: DIGIT DIGIT DIGIT DIGIT '-' ('0' [1-9] | '1' [0-2]) '-' ('0' [1-9] | [12] BOOLEAN: T R U E | F A L S E ; VERSION_NUMBER: DIGIT+ '.' DIGIT+ '.' DIGIT+ ; NULL: N U L L ; +LIKE: '*' ; JAVA_ID: [a-zA-Z$_] [a-zA-Z0-9$_]* ; SPACE: [ ]+ ; diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/QueryLangEvaluator.java b/src/main/java/com/netgrif/application/engine/pfql/service/QueryLangEvaluator.java index da65be1429..723e43de73 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/QueryLangEvaluator.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/QueryLangEvaluator.java @@ -40,6 +40,7 @@ public class QueryLangEvaluator extends QueryLangBaseListener { private final ParseTreeProperty elasticQuery = new ParseTreeProperty<>(); private final ParseTreeProperty mongoQuery = new ParseTreeProperty<>(); + private final String elasticFuzzyMaxDistance = "2"; private final IUserService userService; @@ -1815,6 +1816,46 @@ public void exitTasksUserIdNull(QueryLangParser.TasksUserIdNullContext ctx) { this.searchWithElastic = true; } + @Override + public void exitTitleLike(QueryLangParser.TitleLikeContext ctx) { + StringPath stringPath; + Token op = ctx.stringLikeComparison().stringComparison().op; + boolean not = ctx.stringLikeComparison().stringComparison().NOT() != null; + String string = handleStringComparisonWithPlaceholders(ctx.stringLikeComparison().stringComparison()); + + switch (resourceType) { + case PROCESS: + stringPath = QPetriNet.petriNet.title.defaultValue; + break; + case CASE: + stringPath = QCase.case$.title; + setElasticQuery(ctx, buildElasticQuery("title", op.getType(), string + "~" + elasticFuzzyMaxDistance, not)); + break; + case TASK: + stringPath = QTask.task.title.defaultValue; + break; + default: + throw new IllegalArgumentException("Unknown query type: " + resourceType); + } + + Predicate mongoQuery = stringPath.likeIgnoreCase("%" + string + "%"); + setMongoQuery(ctx, not ? mongoQuery.not() : mongoQuery); + } + + @Override + public void exitDataStringLike(QueryLangParser.DataStringLikeContext ctx) { + String fieldId = ctx.dataValue().fieldId.getText(); + Token op = ctx.stringLikeComparison().stringComparison().op; + checkOp(ComparisonType.STRING, op); + boolean not = ctx.stringLikeComparison().stringComparison().NOT() != null; + String string = handleStringComparisonWithPlaceholders(ctx.stringLikeComparison().stringComparison()); + + setMongoQuery(ctx, null); + setElasticQuery(ctx, buildElasticQuery("dataSet." + fieldId + ".fulltextValue", op.getType(), + string + "~" + elasticFuzzyMaxDistance, not)); + this.searchWithElastic = true; + } + private boolean shouldBeNotNull(QueryLangParser.NullComparisonContext ctx) { if (ctx == null) { throw new IllegalArgumentException("Null comparison context must be provided"); diff --git a/src/test/java/com/netgrif/application/engine/pfql/QueryLangTest.java b/src/test/java/com/netgrif/application/engine/pfql/QueryLangTest.java index c3c4b5c12e..a7de64fe3f 100644 --- a/src/test/java/com/netgrif/application/engine/pfql/QueryLangTest.java +++ b/src/test/java/com/netgrif/application/engine/pfql/QueryLangTest.java @@ -126,6 +126,9 @@ public void testSearchService() throws InterruptedException { cases = searchService.search("cases: processIdentifier eq 'query_test' and author eq loggedUser.id"); assertEquals(10, ((Page) cases).getTotalElements()); + + cases = searchService.search("cases: processIdentifier eq 'query_test' and data.text_0.value eq 'x'*"); + assertEquals(10, ((Page) cases).getTotalElements()); } @Test @@ -258,6 +261,11 @@ public void testSimpleMongodbProcessQuery() { // title comparison checkStringComparison(mongoDbUtils, "process", "title", QPetriNet.petriNet.title.defaultValue); + actual = evaluateQuery("process: title eq 'somxthing'*").getFullMongoQuery(); + expected = QPetriNet.petriNet.title.defaultValue.likeIgnoreCase("%somxthing%"); + + compareMongoQueries(mongoDbUtils, actual, expected); + // creationDate comparison checkDateComparison(mongoDbUtils, "process", "creationDate", QPetriNet.petriNet.creationDate); } @@ -1154,6 +1162,14 @@ public void testSimpleElasticCaseQuery() { checkDateComparisonElastic("case", "data.field3.value", "dataSet.field3.timestampValue"); + actual = evaluateQuery("case: data.field1.value eq 'somxthing'*").getFullElasticQuery(); + expected = "dataSet.field1.fulltextValue:somxthing~2"; + assertEquals(expected, actual); + + actual = evaluateQuery("case: data.field1.value neq 'somxthing'*").getFullElasticQuery(); + expected = "NOT dataSet.field1.fulltextValue:somxthing~2"; + assertEquals(expected, actual); + actual = evaluateQuery("case: data.field1.value eq true").getFullElasticQuery(); expected = "dataSet.field1.booleanValue:true"; assertEquals(expected, actual); @@ -1199,6 +1215,14 @@ public void testSimpleElasticCaseQuery() { actual = evaluateQuery("case: title neq null").getFullElasticQuery(); expected = "_exists_:title"; assertEquals(expected, actual); + + actual = evaluateQuery("case: title eq 'somxthing'*").getFullElasticQuery(); + expected = "title:somxthing~2"; + assertEquals(expected, actual); + + actual = evaluateQuery("case: title not eq 'somxthing anxthing'*").getFullElasticQuery(); + expected = "NOT title:\"somxthing anxthing\"~2"; + assertEquals(expected, actual); } @Test From 2aae341eeb15fce593a193f87f7500039a139a45 Mon Sep 17 00:00:00 2001 From: chvostek Date: Wed, 22 Jul 2026 12:25:10 +0200 Subject: [PATCH 08/20] [NAE-2466] Advanced search for PFQL - update compilation of PFQL task queries to generate also Elasticsearch query --- .../pfql/service/QueryLangEvaluator.java | 33 ++++- .../workflow/web/AbstractTaskController.java | 23 ++++ .../workflow/web/WorkflowController.java | 23 ++++ .../engine/pfql/QueryLangTest.java | 117 ++++++++++++++---- 4 files changed, 168 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/QueryLangEvaluator.java b/src/main/java/com/netgrif/application/engine/pfql/service/QueryLangEvaluator.java index 723e43de73..6281a61c02 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/QueryLangEvaluator.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/QueryLangEvaluator.java @@ -538,6 +538,7 @@ public void exitIdBasic(QueryLangParser.IdBasicContext ctx) { break; case TASK: qObjectId = QTask.task._id; + setElasticQuery(ctx, buildElasticQuery("stringId", op.getType(), objectId.toString(), not)); break; case USER: qObjectId = QUser.user._id; @@ -556,6 +557,7 @@ public void exitIdList(QueryLangParser.IdListContext ctx) { boolean not = ctx.inListStringComparison().NOT() != null; checkOp(ComparisonType.ID, op); List objectIdList = handleObjectIdListComparison(ctx.inListStringComparison().stringList()); + List stringIdList = objectIdList.stream().map(ObjectId::toString).collect(Collectors.toList()); switch (resourceType) { case PROCESS: @@ -563,11 +565,11 @@ public void exitIdList(QueryLangParser.IdListContext ctx) { break; case CASE: qObjectId = QCase.case$._id; - List stringIdList = objectIdList.stream().map(ObjectId::toString).collect(Collectors.toList()); setElasticQuery(ctx, buildElasticQueryInList("stringId", stringIdList, not)); break; case TASK: qObjectId = QTask.task._id; + setElasticQuery(ctx, buildElasticQueryInList("stringId", stringIdList, not)); break; case USER: qObjectId = QUser.user._id; @@ -585,6 +587,10 @@ public void exitTitleBasic(QueryLangParser.TitleBasicContext ctx) { Token op = ctx.stringComparison().op; boolean not = ctx.stringComparison().NOT() != null; String string = handleStringComparisonWithPlaceholders(ctx.stringComparison()); + String elasticAttribute = "title"; + if (op.getType() == QueryLangParser.EQ || op.getType() == QueryLangParser.NEQ) { + elasticAttribute += ".keyword"; + } switch (resourceType) { case PROCESS: @@ -592,14 +598,11 @@ public void exitTitleBasic(QueryLangParser.TitleBasicContext ctx) { break; case CASE: stringPath = QCase.case$.title; - String elasticAttribute = "title"; - if (op.getType() == QueryLangParser.EQ || op.getType() == QueryLangParser.NEQ) { - elasticAttribute += ".keyword"; - } setElasticQuery(ctx, buildElasticQuery(elasticAttribute, op.getType(), string, not)); break; case TASK: stringPath = QTask.task.title.defaultValue; + setElasticQuery(ctx, buildElasticQuery(elasticAttribute, op.getType(), string, not)); break; default: throw new IllegalArgumentException("Unknown query type: " + resourceType); @@ -624,6 +627,7 @@ public void exitTitleList(QueryLangParser.TitleListContext ctx) { break; case TASK: stringPath = QTask.task.title.defaultValue; + setElasticQuery(ctx, buildElasticQueryInList("title", stringList, not)); break; default: throw new IllegalArgumentException("Unknown query type: " + resourceType); @@ -651,6 +655,8 @@ public void exitTitleRange(QueryLangParser.TitleRangeContext ctx) { break; case TASK: stringPath = QTask.task.title.defaultValue; + setElasticQuery(ctx, buildElasticQueryInRange("title", leftAndRightStrings.getFirst(), + leftEndpointOpen, leftAndRightStrings.getSecond(), rightEndpointOpen, not)); break; default: throw new IllegalArgumentException("Unknown query type: " + resourceType); @@ -833,6 +839,7 @@ public void exitProcessIdBasic(QueryLangParser.ProcessIdBasicContext ctx) { String string = handleStringComparisonWithPlaceholders(ctx.stringComparison()); setMongoQuery(ctx, buildStringPredicate(stringPath, op.getType(), string, not)); + setElasticQuery(ctx, buildElasticQuery("processId", op.getType(), string, not)); } @Override @@ -842,6 +849,7 @@ public void exitProcessIdList(QueryLangParser.ProcessIdListContext ctx) { List stringList = handleStringListComparison(ctx.inListStringComparison().stringList()); setMongoQuery(ctx, buildStringPredicateInList(stringPath, stringList, not)); + setElasticQuery(ctx, buildElasticQueryInList("processId", stringList, not)); } @Override @@ -930,6 +938,7 @@ public void exitTransitionIdBasic(QueryLangParser.TransitionIdBasicContext ctx) String string = handleStringComparisonWithPlaceholders(ctx.stringComparison()); setMongoQuery(ctx, buildStringPredicate(stringPath, op.getType(), string, not)); + setElasticQuery(ctx, buildElasticQuery("transitionId", op.getType(), string, not)); } @Override @@ -939,6 +948,7 @@ public void exitTransitionIdList(QueryLangParser.TransitionIdListContext ctx) { List stringList = handleStringListComparison(ctx.inListStringComparison().stringList()); setMongoQuery(ctx, buildStringPredicateInList(stringPath, stringList, not)); + setElasticQuery(ctx, buildElasticQueryInList("transitionId", stringList, not)); } @Override @@ -951,6 +961,8 @@ public void exitTransitionIdRange(QueryLangParser.TransitionIdRangeContext ctx) setMongoQuery(ctx, buildStringPredicateInRange(stringPath, leftAndRightStrings.getFirst(), leftEndpointOpen, leftAndRightStrings.getSecond(), rightEndpointOpen, not)); + setElasticQuery(ctx, buildElasticQueryInRange("transitionId", leftAndRightStrings.getFirst(), + leftEndpointOpen, leftAndRightStrings.getSecond(), rightEndpointOpen, not)); } @Override @@ -974,6 +986,7 @@ public void exitUserIdBasic(QueryLangParser.UserIdBasicContext ctx) { String string = handleStringComparisonWithPlaceholders(ctx.stringComparison()); setMongoQuery(ctx, buildStringPredicate(stringPath, op.getType(), string, not)); + setElasticQuery(ctx, buildElasticQuery("userId", op.getType(), string, not)); } @Override @@ -983,6 +996,7 @@ public void exitUserIdList(QueryLangParser.UserIdListContext ctx) { List stringList = handleStringListComparison(ctx.inListStringComparison().stringList()); setMongoQuery(ctx, buildStringPredicateInList(stringPath, stringList, not)); + setElasticQuery(ctx, buildElasticQueryInList("userId", stringList, not)); } @Override @@ -993,6 +1007,7 @@ public void exitCaseIdBasic(QueryLangParser.CaseIdBasicContext ctx) { String string = handleStringComparisonWithPlaceholders(ctx.stringComparison()); setMongoQuery(ctx, buildStringPredicate(stringPath, op.getType(), string, not)); + setElasticQuery(ctx, buildElasticQuery("caseId", op.getType(), string, not)); } @Override @@ -1002,6 +1017,7 @@ public void exitCaseIdList(QueryLangParser.CaseIdListContext ctx) { List stringList = handleStringListComparison(ctx.inListStringComparison().stringList()); setMongoQuery(ctx, buildStringPredicateInList(stringPath, stringList, not)); + setElasticQuery(ctx, buildElasticQueryInList("caseId", stringList, not)); } @Override @@ -1597,6 +1613,7 @@ public void exitIdNull(QueryLangParser.IdNullContext ctx) { break; case TASK: mongoQuery = isNotNull ? QTask.task._id.isNotNull() : QTask.task._id.isNull(); + setElasticQuery(ctx, isNotNull ? "_exists_:stringId" : "!(_exists_:stringId)"); break; case USER: mongoQuery = isNotNull ? QUser.user._id.isNotNull() : QUser.user._id.isNull(); @@ -1625,6 +1642,7 @@ public void exitTitleNull(QueryLangParser.TitleNullContext ctx) { break; case TASK: mongoQuery = isNotNull ? QTask.task.title.isNotNull() : QTask.task.title.isNull(); + setElasticQuery(ctx, isNotNull ? "_exists_:title" : "!(_exists_:title)"); break; default: throw new IllegalArgumentException("Unknown query type: " + resourceType); @@ -1677,6 +1695,7 @@ public void exitProcessIdNull(QueryLangParser.ProcessIdNullContext ctx) { checkOp(ComparisonType.NULL, op); boolean isNotNull = shouldBeNotNull(ctx.nullComparison()); setMongoQuery(ctx, isNotNull ? QTask.task.processId.isNotNull() : QTask.task.processId.isNull() ); + setElasticQuery(ctx, isNotNull ? "_exists_:processId" : "!(_exists_:processId)"); } @Override @@ -1712,6 +1731,7 @@ public void exitTransitionIdNull(QueryLangParser.TransitionIdNullContext ctx) { checkOp(ComparisonType.NULL, op); boolean isNotNull = shouldBeNotNull(ctx.nullComparison()); setMongoQuery(ctx, isNotNull ? QTask.task.transitionId.isNotNull() : QTask.task.transitionId.isNull()); + setElasticQuery(ctx, isNotNull ? "_exists_:transitionId" : "!(_exists_:transitionId)"); } @Override @@ -1720,6 +1740,7 @@ public void exitUserIdNull(QueryLangParser.UserIdNullContext ctx) { checkOp(ComparisonType.NULL, op); boolean isNotNull = shouldBeNotNull(ctx.nullComparison()); setMongoQuery(ctx, isNotNull ? QTask.task.userId.isNotNull() : QTask.task.userId.isNull()); + setElasticQuery(ctx, isNotNull ? "_exists_:userId" : "!(_exists_:userId)"); } @Override @@ -1733,6 +1754,7 @@ public void exitCaseIdNull(QueryLangParser.CaseIdNullContext ctx) { checkOp(ComparisonType.NULL, op); boolean isNotNull = shouldBeNotNull(ctx.nullComparison()); setMongoQuery(ctx, isNotNull ? QTask.task.caseId.isNotNull() : QTask.task.caseId.isNull()); + setElasticQuery(ctx, isNotNull ? "_exists_:caseId" : "!(_exists_:caseId)"); } @Override @@ -1833,6 +1855,7 @@ public void exitTitleLike(QueryLangParser.TitleLikeContext ctx) { break; case TASK: stringPath = QTask.task.title.defaultValue; + setElasticQuery(ctx, buildElasticQuery("title", op.getType(), string + "~" + elasticFuzzyMaxDistance, not)); break; default: throw new IllegalArgumentException("Unknown query type: " + resourceType); diff --git a/src/main/java/com/netgrif/application/engine/workflow/web/AbstractTaskController.java b/src/main/java/com/netgrif/application/engine/workflow/web/AbstractTaskController.java index b03a22608b..23ede05375 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/web/AbstractTaskController.java +++ b/src/main/java/com/netgrif/application/engine/workflow/web/AbstractTaskController.java @@ -181,6 +181,14 @@ public PagedModel searchPublic(LoggedUser loggedUser, Pag } public PagedModel searchElastic(Authentication auth, Pageable pageable, SingleElasticTaskSearchRequestAsList searchBody, MergeFilterOperation operation, PagedResourcesAssembler assembler, Locale locale) { + searchBody.getList().forEach((request) -> { + // todo: temporary for loop until the frontend works fully with PFQL + if (!isPfqlQuery(request.query)) { + return; + } + QueryLangEvaluator evaluator = SearchUtils.evaluateQuery(request.query); + request.query = evaluator.getFullElasticQuery(); + }); Page tasks = elasticTaskService.search(searchBody.getList(), (LoggedUser) auth.getPrincipal(), pageable, locale, operation == MergeFilterOperation.AND); Link selfLink = WebMvcLinkBuilder.linkTo(WebMvcLinkBuilder.methodOn(TaskController.class) .searchElastic(auth, pageable, searchBody, operation, assembler, locale)).withRel("search_es"); @@ -372,4 +380,19 @@ protected SetDataEventOutcome handleMainSetDataEventOutcome(SetDataEventOutcome return mainOutcome; } } + + protected boolean isPfqlQuery(String query) { + // todo: temporary until the frontend works fully with PFQL + return query != null && !query.isBlank() && ( + startsWithPfqlPrefix("case", query) || startsWithPfqlPrefix("cases", query) + || startsWithPfqlPrefix("task", query) || startsWithPfqlPrefix("tasks", query) + || startsWithPfqlPrefix("process", query) || startsWithPfqlPrefix("processes", query) + || startsWithPfqlPrefix("user", query) || startsWithPfqlPrefix("users", query) + ); + } + + protected boolean startsWithPfqlPrefix(String prefix, String query) { + // todo: temporary until the frontend works fully with PFQL + return query.startsWith(prefix + ":") || query.startsWith(prefix + " where"); + } } diff --git a/src/main/java/com/netgrif/application/engine/workflow/web/WorkflowController.java b/src/main/java/com/netgrif/application/engine/workflow/web/WorkflowController.java index b82324e200..14cc96e65c 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/web/WorkflowController.java +++ b/src/main/java/com/netgrif/application/engine/workflow/web/WorkflowController.java @@ -104,6 +104,14 @@ public PagedModel getAll(Pageable pageable, PagedResourcesAssemble @PostMapping(value = "/case/search", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaTypes.HAL_JSON_VALUE) public PagedModel search(@RequestBody SingleCaseSearchRequestAsList searchBody, @RequestParam(defaultValue = "OR") MergeFilterOperation operation, Pageable pageable, PagedResourcesAssembler assembler, Authentication auth, Locale locale) { LoggedUser user = (LoggedUser) auth.getPrincipal(); + searchBody.getList().forEach((request) -> { + // todo: temporary for loop until the frontend works fully with PFQL + if (!isPfqlQuery(request.query)) { + return; + } + QueryLangEvaluator evaluator = SearchUtils.evaluateQuery(request.query); + request.query = evaluator.getFullElasticQuery(); + }); Page cases = elasticCaseService.search(searchBody.getList(), user, pageable, locale, operation == MergeFilterOperation.AND); Link selfLink = WebMvcLinkBuilder.linkTo(WebMvcLinkBuilder.methodOn(WorkflowController.class) @@ -292,4 +300,19 @@ public ResponseEntity getFileByName(@PathVariable("id") String caseId, .headers(headers) .body(new InputStreamResource(fileFieldInputStream.getInputStream())); } + + private boolean isPfqlQuery(String query) { + // todo: temporary until the frontend works fully with PFQL + return query != null && !query.isBlank() && ( + startsWithPfqlPrefix("case", query) || startsWithPfqlPrefix("cases", query) + || startsWithPfqlPrefix("task", query) || startsWithPfqlPrefix("tasks", query) + || startsWithPfqlPrefix("process", query) || startsWithPfqlPrefix("processes", query) + || startsWithPfqlPrefix("user", query) || startsWithPfqlPrefix("users", query) + ); + } + + private boolean startsWithPfqlPrefix(String prefix, String query) { + // todo: temporary until the frontend works fully with PFQL + return query.startsWith(prefix + ":") || query.startsWith(prefix + " where"); + } } diff --git a/src/test/java/com/netgrif/application/engine/pfql/QueryLangTest.java b/src/test/java/com/netgrif/application/engine/pfql/QueryLangTest.java index a7de64fe3f..9f6b28e5f0 100644 --- a/src/test/java/com/netgrif/application/engine/pfql/QueryLangTest.java +++ b/src/test/java/com/netgrif/application/engine/pfql/QueryLangTest.java @@ -1279,26 +1279,56 @@ public void testComplexElasticCaseQuery() { public void testSimpleElasticTaskQuery() { // without comparison String actual = evaluateQuery("tasks").getFullElasticQuery(); - assertEquals("*", actual); + String expected = "*"; + assertEquals(expected, actual); // elastic query should be always null // id comparison actual = evaluateQuery(String.format("task: id eq '%s'", GENERIC_OBJECT_ID)).getFullElasticQuery(); - assertNull(actual); + expected = String.format("stringId:%s", GENERIC_OBJECT_ID); + assertEquals(expected, actual); + + actual = evaluateQuery("task: id eq null").getFullElasticQuery(); + expected = "!(_exists_:stringId)"; + assertEquals(expected, actual); + + actual = evaluateQuery("task: id neq null").getFullElasticQuery(); + expected = "_exists_:stringId"; + assertEquals(expected, actual); // transitionId comparison actual = evaluateQuery("task: transitionId eq 'test'").getFullElasticQuery(); - assertNull(actual); + expected = "transitionId:test"; + assertEquals(expected, actual); actual = evaluateQuery("task: transitionId contains 'test'").getFullElasticQuery(); - assertNull(actual); + expected = "transitionId:*test*"; + assertEquals(expected, actual); + + actual = evaluateQuery("task: transitionId eq null").getFullElasticQuery(); + expected = "!(_exists_:transitionId)"; + assertEquals(expected, actual); + + actual = evaluateQuery("task: transitionId neq null").getFullElasticQuery(); + expected = "_exists_:transitionId"; + assertEquals(expected, actual); // title comparison actual = evaluateQuery("task: title eq 'test'").getFullElasticQuery(); - assertNull(actual); + expected = "title.keyword:test"; + assertEquals(expected, actual); actual = evaluateQuery("task: title contains 'test'").getFullElasticQuery(); - assertNull(actual); + expected = "title:*test*"; + assertEquals(expected, actual); + + actual = evaluateQuery("task: title eq null").getFullElasticQuery(); + expected = "!(_exists_:title)"; + assertEquals(expected, actual); + + actual = evaluateQuery("task: title neq null").getFullElasticQuery(); + expected = "_exists_:title"; + assertEquals(expected, actual); // state comparison actual = evaluateQuery("task: state eq enabled").getFullElasticQuery(); @@ -1309,24 +1339,54 @@ public void testSimpleElasticTaskQuery() { // userId comparison actual = evaluateQuery("task: userId eq 'test'").getFullElasticQuery(); - assertNull(actual); + expected = "userId:test"; + assertEquals(expected, actual); actual = evaluateQuery("task: userId contains 'test'").getFullElasticQuery(); - assertNull(actual); + expected = "userId:*test*"; + assertEquals(expected, actual); + + actual = evaluateQuery("task: userId eq null").getFullElasticQuery(); + expected = "!(_exists_:userId)"; + assertEquals(expected, actual); + + actual = evaluateQuery("task: userId neq null").getFullElasticQuery(); + expected = "_exists_:userId"; + assertEquals(expected, actual); // caseId comparison actual = evaluateQuery("task: caseId eq 'test'").getFullElasticQuery(); - assertNull(actual); + expected = "caseId:test"; + assertEquals(expected, actual); actual = evaluateQuery("task: caseId contains 'test'").getFullElasticQuery(); - assertNull(actual); + expected = "caseId:*test*"; + assertEquals(expected, actual); + + actual = evaluateQuery("task: caseId eq null").getFullElasticQuery(); + expected = "!(_exists_:caseId)"; + assertEquals(expected, actual); + + actual = evaluateQuery("task: caseId neq null").getFullElasticQuery(); + expected = "_exists_:caseId"; + assertEquals(expected, actual); // processId comparison actual = evaluateQuery("task: processId eq 'test'").getFullElasticQuery(); - assertNull(actual); + expected = "processId:test"; + assertEquals(expected, actual); actual = evaluateQuery("task: processId contains 'test'").getFullElasticQuery(); - assertNull(actual); + expected = "processId:*test*"; + assertEquals(expected, actual); + + actual = evaluateQuery("task: processId eq null").getFullElasticQuery(); + expected = "!(_exists_:processId)"; + assertEquals(expected, actual); + + actual = evaluateQuery("task: processId neq null").getFullElasticQuery(); + expected = "_exists_:processId"; + assertEquals(expected, actual); // lastAssign comparison actual = evaluateQuery("task: lastAssign eq 2011-12-03T10:15:30").getFullElasticQuery(); @@ -1366,44 +1426,55 @@ public void testComplexElasticTaskQuery() { // elastic query should be always null // not comparison String actual = evaluateQuery(String.format("task: id not eq '%s'", GENERIC_OBJECT_ID)).getFullElasticQuery(); - assertNull(actual); + String expected = String.format("NOT stringId:%s", GENERIC_OBJECT_ID); + assertEquals(expected, actual); actual = evaluateQuery(String.format("task: id neq '%s'", GENERIC_OBJECT_ID)).getFullElasticQuery(); - assertNull(actual); + expected = String.format("NOT stringId:%s", GENERIC_OBJECT_ID); + assertEquals(expected, actual); // and comparison actual = evaluateQuery(String.format("task: id eq '%s' and title eq 'test'", GENERIC_OBJECT_ID)).getFullElasticQuery(); - assertNull(actual); + expected = String.format("stringId:%s AND title.keyword:test", GENERIC_OBJECT_ID); + assertEquals(expected, actual); // and not comparison actual = evaluateQuery(String.format("task: id eq '%s' and title not eq 'test'", GENERIC_OBJECT_ID)).getFullElasticQuery(); - assertNull(actual); + expected = String.format("stringId:%s AND NOT title.keyword:test", GENERIC_OBJECT_ID); + assertEquals(expected, actual); actual = evaluateQuery(String.format("task: id eq '%s' and title != 'test'", GENERIC_OBJECT_ID)).getFullElasticQuery(); - assertNull(actual); + expected = String.format("stringId:%s AND NOT title.keyword:test", GENERIC_OBJECT_ID); + assertEquals(expected, actual); // or comparison actual = evaluateQuery(String.format("task: id eq '%s' or title eq 'test'", GENERIC_OBJECT_ID)).getFullElasticQuery(); - assertNull(actual); + expected = String.format("stringId:%s OR title.keyword:test", GENERIC_OBJECT_ID); + assertEquals(expected, actual); // or not comparison actual = evaluateQuery(String.format("task: id eq '%s' or title not eq 'test'", GENERIC_OBJECT_ID)).getFullElasticQuery(); - assertNull(actual); + expected = String.format("stringId:%s OR NOT title.keyword:test", GENERIC_OBJECT_ID); + assertEquals(expected, actual); actual = evaluateQuery(String.format("task: id eq '%s' or title neq 'test'", GENERIC_OBJECT_ID)).getFullElasticQuery(); - assertNull(actual); + expected = String.format("stringId:%s OR NOT title.keyword:test", GENERIC_OBJECT_ID); + assertEquals(expected, actual); // parenthesis comparison actual = evaluateQuery(String.format("task: id eq '%s' and (title eq 'test' or title eq 'test1')", GENERIC_OBJECT_ID)).getFullElasticQuery(); - assertNull(actual); + expected = String.format("stringId:%s AND (title.keyword:test OR title.keyword:test1)", GENERIC_OBJECT_ID); + assertEquals(expected, actual); // parenthesis not comparison actual = evaluateQuery(String.format("task: id eq '%s' and not (title eq 'test' or title eq 'test1')", GENERIC_OBJECT_ID)).getFullElasticQuery(); - assertNull(actual); + expected = String.format("stringId:%s AND NOT (title.keyword:test OR title.keyword:test1)", GENERIC_OBJECT_ID); + assertEquals(expected, actual); // nested parenthesis comparison actual = evaluateQuery(String.format("task: id eq '%s' and (title eq 'test' or (title eq 'test1' and processId eq 'test'))", GENERIC_OBJECT_ID)).getFullElasticQuery(); - assertNull(actual); + expected = String.format("stringId:%s AND (title.keyword:test OR (title.keyword:test1 AND processId:test))", GENERIC_OBJECT_ID); + assertEquals(expected, actual); } @Test From 61efd201a12fb1ba227317bcba4fc54f97ecfb5a Mon Sep 17 00:00:00 2001 From: chvostek Date: Fri, 14 Aug 2026 14:27:47 +0200 Subject: [PATCH 09/20] [NAE-2466] Advanced search for PFQL - remove component force in Localised filter fields --- .../workflow/web/responsebodies/LocalisedCaseFilterField.java | 2 -- .../web/responsebodies/LocalisedProcessFilterField.java | 2 -- .../workflow/web/responsebodies/LocalisedTaskFilterField.java | 2 -- 3 files changed, 6 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/workflow/web/responsebodies/LocalisedCaseFilterField.java b/src/main/java/com/netgrif/application/engine/workflow/web/responsebodies/LocalisedCaseFilterField.java index d63e8d1d94..48d020bfe9 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/web/responsebodies/LocalisedCaseFilterField.java +++ b/src/main/java/com/netgrif/application/engine/workflow/web/responsebodies/LocalisedCaseFilterField.java @@ -1,6 +1,5 @@ package com.netgrif.application.engine.workflow.web.responsebodies; -import com.netgrif.application.engine.petrinet.domain.Component; import com.netgrif.application.engine.petrinet.domain.dataset.CaseFilterField; import lombok.Data; import lombok.EqualsAndHashCode; @@ -13,6 +12,5 @@ public class LocalisedCaseFilterField extends LocalisedField { public LocalisedCaseFilterField(CaseFilterField field, Locale locale) { super(field, locale); - setComponent(new Component("string_query")); } } diff --git a/src/main/java/com/netgrif/application/engine/workflow/web/responsebodies/LocalisedProcessFilterField.java b/src/main/java/com/netgrif/application/engine/workflow/web/responsebodies/LocalisedProcessFilterField.java index 70886f8caa..6455f65ce6 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/web/responsebodies/LocalisedProcessFilterField.java +++ b/src/main/java/com/netgrif/application/engine/workflow/web/responsebodies/LocalisedProcessFilterField.java @@ -1,6 +1,5 @@ package com.netgrif.application.engine.workflow.web.responsebodies; -import com.netgrif.application.engine.petrinet.domain.Component; import com.netgrif.application.engine.petrinet.domain.dataset.ProcessFilterField; import lombok.Data; import lombok.EqualsAndHashCode; @@ -13,6 +12,5 @@ public class LocalisedProcessFilterField extends LocalisedField { public LocalisedProcessFilterField(ProcessFilterField field, Locale locale) { super(field, locale); - setComponent(new Component("string_query")); } } diff --git a/src/main/java/com/netgrif/application/engine/workflow/web/responsebodies/LocalisedTaskFilterField.java b/src/main/java/com/netgrif/application/engine/workflow/web/responsebodies/LocalisedTaskFilterField.java index 1a04a13a75..d0909ff0e5 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/web/responsebodies/LocalisedTaskFilterField.java +++ b/src/main/java/com/netgrif/application/engine/workflow/web/responsebodies/LocalisedTaskFilterField.java @@ -1,6 +1,5 @@ package com.netgrif.application.engine.workflow.web.responsebodies; -import com.netgrif.application.engine.petrinet.domain.Component; import com.netgrif.application.engine.petrinet.domain.dataset.TaskFilterField; import lombok.Data; import lombok.EqualsAndHashCode; @@ -13,6 +12,5 @@ public class LocalisedTaskFilterField extends LocalisedField { public LocalisedTaskFilterField(TaskFilterField field, Locale locale) { super(field, locale); - setComponent(new Component("string_query")); } } From 583275b4724f4716f204c39e1869210f22d676b4 Mon Sep 17 00:00:00 2001 From: chvostek Date: Mon, 17 Aug 2026 09:57:17 +0200 Subject: [PATCH 10/20] [NAE-2475] Advanced search is broken - ensure PFQL validation on filter fields --- .../models/CaseFilterFieldValidation.groovy | 12 ++++ .../models/FilterFieldValidation.groovy | 25 +++++++ .../models/ProcessFilterValidation.groovy | 12 ++++ .../models/TaskFilterFieldValidation.groovy | 12 ++++ .../service/ValidationService.groovy | 12 +++- .../engine/importer/service/FieldFactory.java | 12 ++++ .../petriNets/engine-processes/org_group.xml | 2 +- .../CaseFilterFieldValidationTest.groovy | 72 +++++++++++++++++++ .../ProcessFilterFieldValidationTest.groovy | 72 +++++++++++++++++++ .../TaskFilterFieldValidationTest.groovy | 72 +++++++++++++++++++ 10 files changed, 299 insertions(+), 4 deletions(-) create mode 100644 src/main/groovy/com/netgrif/application/engine/validation/models/CaseFilterFieldValidation.groovy create mode 100644 src/main/groovy/com/netgrif/application/engine/validation/models/FilterFieldValidation.groovy create mode 100644 src/main/groovy/com/netgrif/application/engine/validation/models/ProcessFilterValidation.groovy create mode 100644 src/main/groovy/com/netgrif/application/engine/validation/models/TaskFilterFieldValidation.groovy create mode 100644 src/test/groovy/com/netgrif/application/engine/validation/CaseFilterFieldValidationTest.groovy create mode 100644 src/test/groovy/com/netgrif/application/engine/validation/ProcessFilterFieldValidationTest.groovy create mode 100644 src/test/groovy/com/netgrif/application/engine/validation/TaskFilterFieldValidationTest.groovy diff --git a/src/main/groovy/com/netgrif/application/engine/validation/models/CaseFilterFieldValidation.groovy b/src/main/groovy/com/netgrif/application/engine/validation/models/CaseFilterFieldValidation.groovy new file mode 100644 index 0000000000..62ea151c48 --- /dev/null +++ b/src/main/groovy/com/netgrif/application/engine/validation/models/CaseFilterFieldValidation.groovy @@ -0,0 +1,12 @@ +package com.netgrif.application.engine.validation.models + +import com.netgrif.application.engine.pfql.domain.enums.QueryType +import com.netgrif.application.engine.validation.domain.ValidationDataInput + +class CaseFilterFieldValidation extends FilterFieldValidation { + + @Override + void query(ValidationDataInput validationData) { + doValidation(validationData, QueryType.CASE) + } +} diff --git a/src/main/groovy/com/netgrif/application/engine/validation/models/FilterFieldValidation.groovy b/src/main/groovy/com/netgrif/application/engine/validation/models/FilterFieldValidation.groovy new file mode 100644 index 0000000000..d7ef18f3ae --- /dev/null +++ b/src/main/groovy/com/netgrif/application/engine/validation/models/FilterFieldValidation.groovy @@ -0,0 +1,25 @@ +package com.netgrif.application.engine.validation.models + +import com.netgrif.application.engine.pfql.domain.enums.QueryType +import com.netgrif.application.engine.pfql.service.QueryLangEvaluator +import com.netgrif.application.engine.pfql.service.utils.SearchUtils +import com.netgrif.application.engine.validation.domain.ValidationDataInput + +abstract class FilterFieldValidation extends AbstractFieldValidation { + // todo 2466 generate doc + abstract void query(ValidationDataInput validationData) + + // todo 2466 generate doc + protected void doValidation(ValidationDataInput validationData, QueryType queryType) { + if (validationData.getData().getValue() != null && validationData.getData().getValue() != "") { + try { + QueryLangEvaluator evaluator = SearchUtils.evaluateQuery(validationData.getData().getValue() as String) + if (evaluator.resourceType !== queryType) { + throw new IllegalArgumentException(validationData.getValidationMessage().getTranslation(validationData.getLocale())) + } + } catch (Exception ignore) { + throw new IllegalArgumentException(validationData.getValidationMessage().getTranslation(validationData.getLocale())) + } + } + } +} \ No newline at end of file diff --git a/src/main/groovy/com/netgrif/application/engine/validation/models/ProcessFilterValidation.groovy b/src/main/groovy/com/netgrif/application/engine/validation/models/ProcessFilterValidation.groovy new file mode 100644 index 0000000000..df8a47bef6 --- /dev/null +++ b/src/main/groovy/com/netgrif/application/engine/validation/models/ProcessFilterValidation.groovy @@ -0,0 +1,12 @@ +package com.netgrif.application.engine.validation.models + +import com.netgrif.application.engine.pfql.domain.enums.QueryType +import com.netgrif.application.engine.validation.domain.ValidationDataInput + +class ProcessFilterValidation extends FilterFieldValidation { + + @Override + void query(ValidationDataInput validationData) { + doValidation(validationData, QueryType.PROCESS) + } +} diff --git a/src/main/groovy/com/netgrif/application/engine/validation/models/TaskFilterFieldValidation.groovy b/src/main/groovy/com/netgrif/application/engine/validation/models/TaskFilterFieldValidation.groovy new file mode 100644 index 0000000000..b1d333b05d --- /dev/null +++ b/src/main/groovy/com/netgrif/application/engine/validation/models/TaskFilterFieldValidation.groovy @@ -0,0 +1,12 @@ +package com.netgrif.application.engine.validation.models + +import com.netgrif.application.engine.pfql.domain.enums.QueryType +import com.netgrif.application.engine.validation.domain.ValidationDataInput + +class TaskFilterFieldValidation extends FilterFieldValidation{ + + @Override + void query(ValidationDataInput validationData) { + doValidation(validationData, QueryType.TASK) + } +} diff --git a/src/main/groovy/com/netgrif/application/engine/validation/service/ValidationService.groovy b/src/main/groovy/com/netgrif/application/engine/validation/service/ValidationService.groovy index d15959dd44..952386b3ef 100644 --- a/src/main/groovy/com/netgrif/application/engine/validation/service/ValidationService.groovy +++ b/src/main/groovy/com/netgrif/application/engine/validation/service/ValidationService.groovy @@ -17,7 +17,7 @@ import java.util.stream.Collectors class ValidationService implements IValidationService { @Override - public void valid(Field field, DataField dataField) { + void valid(Field field, DataField dataField) { if (field.getValidations() == null) { return } @@ -36,6 +36,13 @@ class ValidationService implements IValidationService { instance = new BooleanFieldValidation() } else if (field instanceof DateField) { instance = new DateFieldValidation() + } else if (field instanceof CaseFilterField) { + instance = new CaseFilterFieldValidation() + } else if (field instanceof TaskFilterField) { + instance = new TaskFilterFieldValidation() + } else if (field instanceof ProcessFilterField) { + instance = new ProcessFilterValidation() + } // } else if (field instanceof DateTimeField) { // instance = new DateTimeFieldValidation() // } else if (field instanceof ButtonField) { @@ -61,13 +68,12 @@ class ValidationService implements IValidationService { // } else if (field instanceof UserListField) { // // } else if (field instanceof I18nField) { - } MetaMethod method = instance.metaClass.getMethods().find { it.name.toLowerCase() == rules.first().toLowerCase() } if (method != null) { I18nString validMessage = validation.getValidationMessage() != null ? validation.getValidationMessage() : new I18nString("Invalid Field value") method.invoke(instance, new ValidationDataInput(dataField, validMessage, LocaleContextHolder.getLocale(), rules.stream().skip(1).collect(Collectors.joining(" ")))) } else { - log.warn("Method [" + rules.first() + "] in dataField " + field.getImportId() + " not found") + log.warn("Method [{}] in dataField {} not found", rules.first(), field.getImportId()) } } }) diff --git a/src/main/java/com/netgrif/application/engine/importer/service/FieldFactory.java b/src/main/java/com/netgrif/application/engine/importer/service/FieldFactory.java index 850e4dbe0b..b6dddb4db7 100644 --- a/src/main/java/com/netgrif/application/engine/importer/service/FieldFactory.java +++ b/src/main/java/com/netgrif/application/engine/importer/service/FieldFactory.java @@ -33,6 +33,8 @@ @Slf4j public final class FieldFactory { + private static final String FILTER_FIELD_VALIDATION_RULE = "query"; + @Value("${nae.storage.default-type:local}") private String defaultStorageType; @@ -559,18 +561,21 @@ private FileListField buildFileListField(Data data) { private CaseFilterField buildCaseFilterField(Data data) { CaseFilterField field = new CaseFilterField(); setDefaultValue(field, data, field::setDefaultValue); + field.setValidations(getFilterValidationAsList()); return field; } private TaskFilterField buildTaskFilterField(Data data) { TaskFilterField field = new TaskFilterField(); setDefaultValue(field, data, field::setDefaultValue); + field.setValidations(getFilterValidationAsList()); return field; } private ProcessFilterField buildProcessFilterField(Data data) { ProcessFilterField field = new ProcessFilterField(); setDefaultValue(field, data, field::setDefaultValue); + field.setValidations(getFilterValidationAsList()); return field; } @@ -863,4 +868,11 @@ private Map getFieldOptions(MapOptionsField field, Cas private void resolveStorage(Data data, StorageField field) { field.setStorage(StorageFactory.createStorage(data, storageResolverService, defaultStorageType)); } + + private List getFilterValidationAsList() { + com.netgrif.application.engine.petrinet.domain.dataset.logic.validation.Validation defaultValidation = makeValidation(FILTER_FIELD_VALIDATION_RULE, null, false); + List validations = new ArrayList<>(); + validations.add(defaultValidation); + return validations; + } } diff --git a/src/main/resources/petriNets/engine-processes/org_group.xml b/src/main/resources/petriNets/engine-processes/org_group.xml index 426eae220d..ff4d6850b3 100644 --- a/src/main/resources/petriNets/engine-processes/org_group.xml +++ b/src/main/resources/petriNets/engine-processes/org_group.xml @@ -90,7 +90,7 @@ invite_by_mail Add e-mail address example@example.com - Add e-meail address to send invitation + Add e-mail address to send invitation email diff --git a/src/test/groovy/com/netgrif/application/engine/validation/CaseFilterFieldValidationTest.groovy b/src/test/groovy/com/netgrif/application/engine/validation/CaseFilterFieldValidationTest.groovy new file mode 100644 index 0000000000..52a168063f --- /dev/null +++ b/src/test/groovy/com/netgrif/application/engine/validation/CaseFilterFieldValidationTest.groovy @@ -0,0 +1,72 @@ +package com.netgrif.application.engine.validation + +import com.netgrif.application.engine.TestHelper +import com.netgrif.application.engine.petrinet.domain.I18nString +import com.netgrif.application.engine.validation.domain.ValidationDataInput +import com.netgrif.application.engine.validation.models.CaseFilterFieldValidation +import com.netgrif.application.engine.workflow.domain.DataField +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.context.i18n.LocaleContextHolder +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.context.junit.jupiter.SpringExtension + +import java.util.stream.Collectors + +import static org.junit.jupiter.api.Assertions.assertThrows + +@SpringBootTest +@ActiveProfiles(["test"]) +@ExtendWith(SpringExtension.class) +class CaseFilterFieldValidationTest { + + public static final String ErrorMessage = "Invalid Field value" + @Autowired + private TestHelper testHelper + + @BeforeEach + void setup() { + testHelper.truncateDbs() + } + + @Test + void pfqlSuccessTest() { + CaseFilterFieldValidation validation = new CaseFilterFieldValidation() + DataField dataField = new DataField() + dataField.setValue("cases: title eq 'myTitle'") + I18nString validMessage = new I18nString(ErrorMessage) + List rules = [] + ValidationDataInput input = new ValidationDataInput(dataField, validMessage, LocaleContextHolder.getLocale(), rules.stream().skip(1).collect(Collectors.joining(" "))) + + validation.query(input) + } + + @Test + void pfqlWrongResourceTypeTest() { + CaseFilterFieldValidation validation = new CaseFilterFieldValidation() + DataField dataField = new DataField() + dataField.setValue("tasks: caseId eq 'someCaseId'") + I18nString validMessage = new I18nString(ErrorMessage) + List rules = [] + ValidationDataInput input = new ValidationDataInput(dataField, validMessage, LocaleContextHolder.getLocale(), rules.stream().skip(1).collect(Collectors.joining(" "))) + + assertThrows(IllegalArgumentException.class, () -> validation.query(input)) + } + + + @Test + void pfqlWrongQueryTest() { + CaseFilterFieldValidation validation = new CaseFilterFieldValidation() + DataField dataField = new DataField() + dataField.setValue("cases: titleeeee eq 'myTitle'") + I18nString validMessage = new I18nString(ErrorMessage) + List rules = [] + ValidationDataInput input = new ValidationDataInput(dataField, validMessage, LocaleContextHolder.getLocale(), rules.stream().skip(1).collect(Collectors.joining(" "))) + + assertThrows(IllegalArgumentException.class, () -> validation.query(input)) + } + +} diff --git a/src/test/groovy/com/netgrif/application/engine/validation/ProcessFilterFieldValidationTest.groovy b/src/test/groovy/com/netgrif/application/engine/validation/ProcessFilterFieldValidationTest.groovy new file mode 100644 index 0000000000..e53b40fc5b --- /dev/null +++ b/src/test/groovy/com/netgrif/application/engine/validation/ProcessFilterFieldValidationTest.groovy @@ -0,0 +1,72 @@ +package com.netgrif.application.engine.validation + +import com.netgrif.application.engine.TestHelper +import com.netgrif.application.engine.petrinet.domain.I18nString +import com.netgrif.application.engine.validation.domain.ValidationDataInput +import com.netgrif.application.engine.validation.models.ProcessFilterValidation +import com.netgrif.application.engine.workflow.domain.DataField +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.context.i18n.LocaleContextHolder +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.context.junit.jupiter.SpringExtension + +import java.util.stream.Collectors + +import static org.junit.jupiter.api.Assertions.assertThrows + +@SpringBootTest +@ActiveProfiles(["test"]) +@ExtendWith(SpringExtension.class) +class ProcessFilterFieldValidationTest { + + public static final String ErrorMessage = "Invalid Field value" + @Autowired + private TestHelper testHelper + + @BeforeEach + void setup() { + testHelper.truncateDbs() + } + + @Test + void pfqlSuccessTest() { + ProcessFilterValidation validation = new ProcessFilterValidation() + DataField dataField = new DataField() + dataField.setValue("process: identifier eq 'myIdentifier'") + I18nString validMessage = new I18nString(ErrorMessage) + List rules = [] + ValidationDataInput input = new ValidationDataInput(dataField, validMessage, LocaleContextHolder.getLocale(), rules.stream().skip(1).collect(Collectors.joining(" "))) + + validation.query(input) + } + + @Test + void pfqlWrongResourceTypeTest() { + ProcessFilterValidation validation = new ProcessFilterValidation() + DataField dataField = new DataField() + dataField.setValue("cases: title eq 'myTitle'") + I18nString validMessage = new I18nString(ErrorMessage) + List rules = [] + ValidationDataInput input = new ValidationDataInput(dataField, validMessage, LocaleContextHolder.getLocale(), rules.stream().skip(1).collect(Collectors.joining(" "))) + + assertThrows(IllegalArgumentException.class, () -> validation.query(input)) + } + + + @Test + void pfqlWrongQueryTest() { + ProcessFilterValidation validation = new ProcessFilterValidation() + DataField dataField = new DataField() + dataField.setValue("process: identifierrrrrrrr eq 'myIdentifier'") + I18nString validMessage = new I18nString(ErrorMessage) + List rules = [] + ValidationDataInput input = new ValidationDataInput(dataField, validMessage, LocaleContextHolder.getLocale(), rules.stream().skip(1).collect(Collectors.joining(" "))) + + assertThrows(IllegalArgumentException.class, () -> validation.query(input)) + } + +} diff --git a/src/test/groovy/com/netgrif/application/engine/validation/TaskFilterFieldValidationTest.groovy b/src/test/groovy/com/netgrif/application/engine/validation/TaskFilterFieldValidationTest.groovy new file mode 100644 index 0000000000..30eb6c01c6 --- /dev/null +++ b/src/test/groovy/com/netgrif/application/engine/validation/TaskFilterFieldValidationTest.groovy @@ -0,0 +1,72 @@ +package com.netgrif.application.engine.validation + +import com.netgrif.application.engine.TestHelper +import com.netgrif.application.engine.petrinet.domain.I18nString +import com.netgrif.application.engine.validation.domain.ValidationDataInput +import com.netgrif.application.engine.validation.models.TaskFilterFieldValidation +import com.netgrif.application.engine.workflow.domain.DataField +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.context.i18n.LocaleContextHolder +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.context.junit.jupiter.SpringExtension + +import java.util.stream.Collectors + +import static org.junit.jupiter.api.Assertions.assertThrows + +@SpringBootTest +@ActiveProfiles(["test"]) +@ExtendWith(SpringExtension.class) +class TaskFilterFieldValidationTest { + + public static final String ErrorMessage = "Invalid Field value" + @Autowired + private TestHelper testHelper + + @BeforeEach + void setup() { + testHelper.truncateDbs() + } + + @Test + void querySuccessTest() { + TaskFilterFieldValidation validation = new TaskFilterFieldValidation() + DataField dataField = new DataField() + dataField.setValue("tasks: caseId eq 'someCaseId'") + I18nString validMessage = new I18nString(ErrorMessage) + List rules = [] + ValidationDataInput input = new ValidationDataInput(dataField, validMessage, LocaleContextHolder.getLocale(), rules.stream().skip(1).collect(Collectors.joining(" "))) + + validation.query(input) + } + + @Test + void queryWrongResourceTypeTest() { + TaskFilterFieldValidation validation = new TaskFilterFieldValidation() + DataField dataField = new DataField() + dataField.setValue("cases: title eq 'myTitle'") + I18nString validMessage = new I18nString(ErrorMessage) + List rules = [] + ValidationDataInput input = new ValidationDataInput(dataField, validMessage, LocaleContextHolder.getLocale(), rules.stream().skip(1).collect(Collectors.joining(" "))) + + assertThrows(IllegalArgumentException.class, () -> validation.query(input)) + } + + + @Test + void queryWrongQueryTest() { + TaskFilterFieldValidation validation = new TaskFilterFieldValidation() + DataField dataField = new DataField() + dataField.setValue("tasks: caseIddddddd eq 'someCaseId'") + I18nString validMessage = new I18nString(ErrorMessage) + List rules = [] + ValidationDataInput input = new ValidationDataInput(dataField, validMessage, LocaleContextHolder.getLocale(), rules.stream().skip(1).collect(Collectors.joining(" "))) + + assertThrows(IllegalArgumentException.class, () -> validation.query(input)) + } + +} From 0873f41dc4addfdec62e035ae2162359f8827a87 Mon Sep 17 00:00:00 2001 From: chvostek Date: Mon, 17 Aug 2026 09:58:56 +0200 Subject: [PATCH 11/20] [NAE-2475] Advanced search is broken - add javadoc --- .../models/FilterFieldValidation.groovy | 33 +++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/src/main/groovy/com/netgrif/application/engine/validation/models/FilterFieldValidation.groovy b/src/main/groovy/com/netgrif/application/engine/validation/models/FilterFieldValidation.groovy index d7ef18f3ae..4a3f5ae891 100644 --- a/src/main/groovy/com/netgrif/application/engine/validation/models/FilterFieldValidation.groovy +++ b/src/main/groovy/com/netgrif/application/engine/validation/models/FilterFieldValidation.groovy @@ -5,12 +5,39 @@ import com.netgrif.application.engine.pfql.service.QueryLangEvaluator import com.netgrif.application.engine.pfql.service.utils.SearchUtils import com.netgrif.application.engine.validation.domain.ValidationDataInput +/** + * Abstract base class for validating filter field values against PFQL queries. + *

+ * This class provides common validation logic for filter fields that need to verify + * whether their values represent valid PFQL queries of a specific type. + *

+ */ abstract class FilterFieldValidation extends AbstractFieldValidation { - // todo 2466 generate doc + + /** + * Validates the filter field value as a PFQL query. + *

+ * Implementations must specify the expected query type for validation. + *

+ * + * @param validationData the validation data input containing the field value and validation context + */ abstract void query(ValidationDataInput validationData) - // todo 2466 generate doc - protected void doValidation(ValidationDataInput validationData, QueryType queryType) { + /** + * Performs validation of the field value against a specific PFQL query type. + *

+ * This method checks if the field value is a valid PFQL query and verifies that + * it matches the expected query type. If the value is null or empty, no validation + * is performed. If the query is invalid or does not match the expected type, + * an IllegalArgumentException is thrown with the localized validation message. + *

+ * + * @param validationData the validation data input containing the field value, locale, and validation message + * @param queryType the expected PFQL query type that the field value should match + * @throws IllegalArgumentException if the query is invalid or does not match the expected type + */ + protected static void doValidation(ValidationDataInput validationData, QueryType queryType) { if (validationData.getData().getValue() != null && validationData.getData().getValue() != "") { try { QueryLangEvaluator evaluator = SearchUtils.evaluateQuery(validationData.getData().getValue() as String) From f09bd4c32b4a14087ffd2fdda7a1192c70ab7c80 Mon Sep 17 00:00:00 2001 From: chvostek Date: Fri, 28 Aug 2026 13:15:48 +0200 Subject: [PATCH 12/20] [NAE-2466] Advanced search for PFQL - remove filter import and export implementation --- .../workflow/domain/filter/Configuration.java | 48 ----- .../domain/filter/DoubleValueHolder.java | 15 -- .../domain/filter/FilterImportExport.java | 73 ------- .../domain/filter/FilterImportExportList.java | 49 ----- .../domain/filter/FilterMetadataExport.java | 104 --------- .../workflow/domain/filter/Predicate.java | 157 -------------- .../domain/filter/PredicateArray.java | 70 ------ .../domain/filter/PredicateValue.java | 82 ------- .../engine/menu/MenuImportExportTest.groovy | 202 ------------------ src/test/resources/menu_file_test.xml | 191 ----------------- 10 files changed, 991 deletions(-) delete mode 100644 src/main/java/com/netgrif/application/engine/workflow/domain/filter/Configuration.java delete mode 100644 src/main/java/com/netgrif/application/engine/workflow/domain/filter/DoubleValueHolder.java delete mode 100644 src/main/java/com/netgrif/application/engine/workflow/domain/filter/FilterImportExport.java delete mode 100644 src/main/java/com/netgrif/application/engine/workflow/domain/filter/FilterImportExportList.java delete mode 100644 src/main/java/com/netgrif/application/engine/workflow/domain/filter/FilterMetadataExport.java delete mode 100644 src/main/java/com/netgrif/application/engine/workflow/domain/filter/Predicate.java delete mode 100644 src/main/java/com/netgrif/application/engine/workflow/domain/filter/PredicateArray.java delete mode 100644 src/main/java/com/netgrif/application/engine/workflow/domain/filter/PredicateValue.java delete mode 100644 src/test/groovy/com/netgrif/application/engine/menu/MenuImportExportTest.groovy delete mode 100644 src/test/resources/menu_file_test.xml diff --git a/src/main/java/com/netgrif/application/engine/workflow/domain/filter/Configuration.java b/src/main/java/com/netgrif/application/engine/workflow/domain/filter/Configuration.java deleted file mode 100644 index 00b190e017..0000000000 --- a/src/main/java/com/netgrif/application/engine/workflow/domain/filter/Configuration.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.netgrif.application.engine.workflow.domain.filter; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -import java.util.HashMap; -import java.util.Map; - -/** - * Class represents configuration part on predicate. - * Class holds operator, which provides operation for value comparison. - * When creating filter for datafield value, datafield attribute is also used, - * to represent which datafield from which process is used in comparison. - */ -@EqualsAndHashCode -@NoArgsConstructor -@Getter -@Setter -public class Configuration { - protected String operator; - protected String datafield; - - public Configuration(Map value) { - value.forEach((k, v) -> { - switch (k) { - case "operator": - operator = (String) v; - break; - case "datafield": - datafield = (String) v; - break; - } - }); - } - - @JsonIgnore - public Map getMapObject() { - Map mapObject = new HashMap<>(); - mapObject.put("operator", operator); - if (datafield != null) { - mapObject.put("datafield", datafield); - } - return mapObject; - } -} diff --git a/src/main/java/com/netgrif/application/engine/workflow/domain/filter/DoubleValueHolder.java b/src/main/java/com/netgrif/application/engine/workflow/domain/filter/DoubleValueHolder.java deleted file mode 100644 index 47633a6343..0000000000 --- a/src/main/java/com/netgrif/application/engine/workflow/domain/filter/DoubleValueHolder.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.netgrif.application.engine.workflow.domain.filter; - -public abstract class DoubleValueHolder { - protected Double convertObjectToDouble(Object val) { - if (val instanceof Double) - return (Double) val; - else if (val instanceof Integer) - return new Double((Integer) val); - else if (val instanceof Float) - return new Double((Float) val); - else if (val instanceof String) - return Double.parseDouble((String) val); - throw new IllegalArgumentException("The provided Object (" + val.toString() + ") cannot be converted to Double"); - } -} diff --git a/src/main/java/com/netgrif/application/engine/workflow/domain/filter/FilterImportExport.java b/src/main/java/com/netgrif/application/engine/workflow/domain/filter/FilterImportExport.java deleted file mode 100644 index edb11fbc67..0000000000 --- a/src/main/java/com/netgrif/application/engine/workflow/domain/filter/FilterImportExport.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.netgrif.application.engine.workflow.domain.filter; - -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; -import com.netgrif.application.engine.petrinet.domain.I18nString; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -import java.util.List; -import java.util.Map; -import java.util.Objects; - -/** - * Class that represents one exported filter. - * This class holds all information about one filter, so filter can be fully build from this class. - * This class is represented by tag in exported xml file. - */ -@EqualsAndHashCode -@NoArgsConstructor -@Getter -@Setter -public class FilterImportExport { - @EqualsAndHashCode.Exclude - protected String caseId; - @EqualsAndHashCode.Exclude - protected String parentCaseId; - protected String parentViewId; - protected I18nString filterName; - protected String filterValue; - protected String visibility; - protected String type; - protected String icon; - @JacksonXmlElementWrapper(localName = "allowedNets") - @JacksonXmlProperty(localName = "allowedNet") - protected List allowedNets; - @JacksonXmlProperty(localName = "filterMetadata") - protected FilterMetadataExport filterMetadataExport; - - public void setFilterMetadataExport(Map value) { - this.filterMetadataExport = new FilterMetadataExport(value); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - FilterImportExport that = (FilterImportExport) o; - - if (!Objects.equals(parentViewId, that.parentViewId)) return false; - if (!Objects.equals(filterName, that.filterName)) return false; - if (!Objects.equals(filterValue, that.filterValue)) return false; - if (!Objects.equals(visibility, that.visibility)) return false; - if (!Objects.equals(type, that.type)) return false; - if (!Objects.equals(allowedNets, that.allowedNets)) return false; - return Objects.equals(filterMetadataExport, that.filterMetadataExport); - } - - @Override - public int hashCode() { - int result = parentViewId != null ? parentViewId.hashCode() : 0; - result = 31 * result + (filterName != null ? filterName.hashCode() : 0); - result = 31 * result + (filterValue != null ? filterValue.hashCode() : 0); - result = 31 * result + (visibility != null ? visibility.hashCode() : 0); - result = 31 * result + (type != null ? type.hashCode() : 0); - result = 31 * result + (icon != null ? icon.hashCode() : 0); - result = 31 * result + (allowedNets != null ? allowedNets.hashCode() : 0); - result = 31 * result + (filterMetadataExport != null ? filterMetadataExport.hashCode() : 0); - return result; - } -} diff --git a/src/main/java/com/netgrif/application/engine/workflow/domain/filter/FilterImportExportList.java b/src/main/java/com/netgrif/application/engine/workflow/domain/filter/FilterImportExportList.java deleted file mode 100644 index 36551dc496..0000000000 --- a/src/main/java/com/netgrif/application/engine/workflow/domain/filter/FilterImportExportList.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.netgrif.application.engine.workflow.domain.filter; - -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.Setter; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -/** - * Class that wraps and holds list of filters, which are meant to be exported/imported. - * This is the root class of xml file, that is created after exporting of filters. - * The root tag of the xml file is: - * tag is followed by list of tags. - * Whole schema for the xml file is on the path: resources/petriNets/filter_export_schema.xsd - */ -@EqualsAndHashCode -@Getter -@Setter -@JacksonXmlRootElement(localName = "filters") -public class FilterImportExportList { - - @JacksonXmlElementWrapper(useWrapping = false) - @JacksonXmlProperty(localName = "filter") - protected List filters; - - public FilterImportExportList() { - this.filters = new ArrayList<>(); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - FilterImportExportList that = (FilterImportExportList) o; - - return Objects.equals(filters, that.filters); - } - - @Override - public int hashCode() { - return filters != null ? filters.hashCode() : 0; - } -} \ No newline at end of file diff --git a/src/main/java/com/netgrif/application/engine/workflow/domain/filter/FilterMetadataExport.java b/src/main/java/com/netgrif/application/engine/workflow/domain/filter/FilterMetadataExport.java deleted file mode 100644 index d06229e370..0000000000 --- a/src/main/java/com/netgrif/application/engine/workflow/domain/filter/FilterMetadataExport.java +++ /dev/null @@ -1,104 +0,0 @@ -package com.netgrif.application.engine.workflow.domain.filter; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -import java.util.*; - -/** - * This class represents complex structure of filter field metadata object. - * This class is represented by tag in xml document. - * While exporting, class is created from complex structure (map) which consists of: - * keys as strings - * values as objects - * This structure needs to be recreated when importing filter by method getMapObject(). - */ -@EqualsAndHashCode -@NoArgsConstructor -@Getter -@Setter -public class FilterMetadataExport { - protected String filterType; - protected boolean defaultSearchCategories; - protected boolean inheritAllowedNets; - @JacksonXmlElementWrapper(localName = "searchCategories") - @JacksonXmlProperty(localName = "searchCategory") - protected List searchCategories; - @JacksonXmlElementWrapper(localName = "predicateMetadata") - @JacksonXmlProperty(localName = "predicateMetadataItem") - protected List predicateMetadata; - - public FilterMetadataExport(Map value) { - value.forEach((k, v) -> { - switch (k) { - case "filterType": - filterType = (String) v; - break; - case "defaultSearchCategories": - defaultSearchCategories = String.valueOf(v).equals("true"); - break; - case "inheritAllowedNets": - inheritAllowedNets = String.valueOf(v).equals("true"); - break; - case "searchCategories": - searchCategories = (List) v; - break; - case "predicateMetadata": - predicateMetadata = new ArrayList<>(); - List list = (List) v; - for (Object val : list) { - if (val instanceof List) { - predicateMetadata.add(new PredicateArray((List) val)); - } else { - predicateMetadata.add(new PredicateArray((Map) val)); - } - } - break; - } - }); - } - - @JsonIgnore - public Map getMapObject() { - Map mapObject = new HashMap<>(); - List listPredicateMetadata = new ArrayList<>(); - if (predicateMetadata != null) { - for (PredicateArray val : predicateMetadata) { - listPredicateMetadata.add(val.getMapObject()); - } - } - mapObject.put("predicateMetadata", listPredicateMetadata); - mapObject.put("searchCategories", searchCategories != null ? searchCategories : new ArrayList()); - return mapObject; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - FilterMetadataExport that = (FilterMetadataExport) o; - - if (defaultSearchCategories != that.defaultSearchCategories) return false; - if (inheritAllowedNets != that.inheritAllowedNets) return false; - if (!Objects.equals(filterType, that.filterType)) return false; - if (!Objects.equals(searchCategories, that.searchCategories)) - return false; - return Objects.equals(predicateMetadata, that.predicateMetadata); - } - - @Override - public int hashCode() { - int result = filterType != null ? filterType.hashCode() : 0; - result = 31 * result + (defaultSearchCategories ? 1 : 0); - result = 31 * result + (inheritAllowedNets ? 1 : 0); - result = 31 * result + (searchCategories != null ? searchCategories.hashCode() : 0); - result = 31 * result + (predicateMetadata != null ? predicateMetadata.hashCode() : 0); - return result; - } -} diff --git a/src/main/java/com/netgrif/application/engine/workflow/domain/filter/Predicate.java b/src/main/java/com/netgrif/application/engine/workflow/domain/filter/Predicate.java deleted file mode 100644 index af4319187e..0000000000 --- a/src/main/java/com/netgrif/application/engine/workflow/domain/filter/Predicate.java +++ /dev/null @@ -1,157 +0,0 @@ -package com.netgrif.application.engine.workflow.domain.filter; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * Predicate class represents one search predicate (search term). - * In the xml document, this class is represented with tag. - * Depending on search category and configuration, there could be 5 different - * types of values. - * Same as the PredicateArray class, this one needs to be converted into map object - * when importing filter. - */ -@EqualsAndHashCode -@NoArgsConstructor -@Getter -@Setter -public class Predicate extends DoubleValueHolder { - protected String category; - protected Configuration configuration; - @JacksonXmlElementWrapper(localName = "stringValues") - @JacksonXmlProperty(localName = "stringValue") - protected List stringValues; - @JacksonXmlElementWrapper(localName = "doubleValues") - @JacksonXmlProperty(localName = "doubleValue") - protected List doubleValues; - @JacksonXmlElementWrapper(localName = "booleanValues") - @JacksonXmlProperty(localName = "booleanValue") - protected List booleanValues; - @JacksonXmlElementWrapper(localName = "mapValues") - @JacksonXmlProperty(localName = "mapValue") - protected List mapValues; - @JacksonXmlElementWrapper(localName = "longValues") - @JacksonXmlProperty(localName = "longValue") - protected List longValues; - - public Predicate(Map value) { - value.forEach((k, v) -> { - switch (k) { - case "category": - category = (String) v; - break; - case "configuration": - configuration = new Configuration((Map) v); - break; - case "values": - List list = (List) v; - if (list.get(0) instanceof String) { - stringValues = new ArrayList<>(); - for (Object val : list) { - stringValues.add((String) val); - } - } else if (list.get(0) instanceof Boolean) { - booleanValues = new ArrayList<>(); - for (Object val : list) { - booleanValues.add((Boolean) val); - } - } else if (list.get(0) instanceof Integer || list.get(0) instanceof Double || list.get(0) instanceof Float) { - doubleValues = new ArrayList<>(); - for (Object val : list) { - doubleValues.add(convertObjectToDouble(val)); - } - } else if (list.get(0) instanceof Long) { - longValues = new ArrayList<>(); - for (Object val : list) { - longValues.add((Long) val); - } - } else { - mapValues = new ArrayList<>(); - for (Object val : list) { - mapValues.add(new PredicateValue((Map) val)); - } - } - break; - case "stringValues": - stringValues = new ArrayList<>(); - List stringList = (List) v; - stringValues.addAll(stringList); - break; - case "doubleValues": - doubleValues = new ArrayList<>(); - for (Object val : (List) v) { - doubleValues.add(convertObjectToDouble(val)); - } - break; - case "longValues": - longValues = new ArrayList<>(); - for (Object val : (List) v) { - if (val instanceof Long) { - longValues.add((Long) val); - continue; - } else if (val instanceof String) { - longValues.add(Long.parseLong((String) val)); - continue; - } - throw new IllegalArgumentException("The provided Object (" + val.toString() + ") cannot be converted to Long"); - } - break; - case "booleanValues": - booleanValues = new ArrayList<>(); - for (Object val : (List) v) { - if (val instanceof Boolean) { - booleanValues.add((Boolean) val); - continue; - } else if (val instanceof String) { - booleanValues.add(Boolean.parseBoolean((String) val)); - continue; - } - throw new IllegalArgumentException("The provided Object (" + val.toString() + ") cannot be converted to Boolean"); - } - break; - case "mapValues": - mapValues = new ArrayList<>(); - List> mapList = (List>) v; - for (Map val : mapList) { - mapValues.add(new PredicateValue(val)); - } - break; - } - }); - } - - @JsonIgnore - public Map getMapObject() { - Map mapObject = new HashMap<>(); - mapObject.put("category", category); - mapObject.put("configuration", configuration.getMapObject()); - if (mapValues != null) { - List tmpList = new ArrayList<>(); - for (PredicateValue val : mapValues) { - tmpList.add(val.getMapObject()); - } - mapObject.put("values", tmpList); - } else { - if (stringValues != null) { - mapObject.put("values", stringValues); - } else if (doubleValues != null) { - mapObject.put("values", doubleValues); - } else if (booleanValues != null) { - mapObject.put("values", booleanValues); - } else if (longValues != null) { - mapObject.put("values", longValues); - } - } - return mapObject; - } -} diff --git a/src/main/java/com/netgrif/application/engine/workflow/domain/filter/PredicateArray.java b/src/main/java/com/netgrif/application/engine/workflow/domain/filter/PredicateArray.java deleted file mode 100644 index 6f60120d15..0000000000 --- a/src/main/java/com/netgrif/application/engine/workflow/domain/filter/PredicateArray.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.netgrif.application.engine.workflow.domain.filter; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Map; - -/** - * This class wraps and holds list of predicates. - * In the xml structure class is represented by tag. - * Same as the FilterMetadataExport class, this one needs to be converted into - * map object while importing filter too. - */ -@EqualsAndHashCode -@NoArgsConstructor -@Getter -@Setter -public class PredicateArray { - @JacksonXmlElementWrapper(useWrapping = false) - @JacksonXmlProperty(localName = "predicate") - protected List predicates; - - public PredicateArray(List value) { - predicates = new ArrayList<>(); - for (Object val : value) { - predicates.add(new Predicate((Map) val)); - } - } - - public PredicateArray(Map value) { - predicates = new ArrayList<>(); - value.forEach((k, v) -> { - for (Object val : ((Collection) v)) { - predicates.add(new Predicate((Map) val)); - } - }); - } - - @JsonIgnore - public List getMapObject() { - List mapObject = new ArrayList<>(); - for (Predicate val : predicates) { - mapObject.add(val.getMapObject()); - } - return mapObject; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - PredicateArray that = (PredicateArray) o; - - return predicates.size() == that.predicates.size(); // TODO implements better comparison of two PredicateArrays - } - - @Override - public int hashCode() { - return predicates != null ? predicates.hashCode() : 0; - } -} diff --git a/src/main/java/com/netgrif/application/engine/workflow/domain/filter/PredicateValue.java b/src/main/java/com/netgrif/application/engine/workflow/domain/filter/PredicateValue.java deleted file mode 100644 index a67c52b8bf..0000000000 --- a/src/main/java/com/netgrif/application/engine/workflow/domain/filter/PredicateValue.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.netgrif.application.engine.workflow.domain.filter; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * Class holds values of some search predicates (mainly searching for author). - * Values can be integer of author id or other search predicate as a text (<>). - */ -@EqualsAndHashCode -@NoArgsConstructor -@Getter -@Setter -public class PredicateValue extends DoubleValueHolder { - - protected String text; - @JacksonXmlElementWrapper(localName = "stringValues") - @JacksonXmlProperty(localName = "stringValue") - protected List stringValues; - @JacksonXmlElementWrapper(localName = "doubleValues") - @JacksonXmlProperty(localName = "doubleValue") - protected List doubleValues; - - public PredicateValue(Map value) { - value.forEach((k, v) -> { - switch (k) { - case "text": - text = (String) v; - break; - case "value": - List list = (List) v; - if (list.get(0) instanceof String) { - stringValues = new ArrayList<>(); - for (Object val : list) { - stringValues.add((String) val); - } - } else if (list.get(0) instanceof Integer || list.get(0) instanceof Double || list.get(0) instanceof Float) { - doubleValues = new ArrayList<>(); - for (Object val : list) { - doubleValues.add(convertObjectToDouble(val)); - } - } - break; - case "stringValues": - stringValues = new ArrayList<>(); - List stringList = (List) v; - for (Object val : stringList) { - stringValues.add((String) val); - } - break; - case "doubleValues": - doubleValues = new ArrayList<>(); - for (Object val : (List) v) { - doubleValues.add(convertObjectToDouble(val)); - } - break; - } - }); - } - - @JsonIgnore - public Map getMapObject() { - Map mapObject = new HashMap<>(); - mapObject.put("text", text); - if (doubleValues != null) { - mapObject.put("value", doubleValues); - } else if (stringValues != null) { - mapObject.put("value", stringValues); - } - return mapObject; - } -} diff --git a/src/test/groovy/com/netgrif/application/engine/menu/MenuImportExportTest.groovy b/src/test/groovy/com/netgrif/application/engine/menu/MenuImportExportTest.groovy deleted file mode 100644 index 98d06dc2b9..0000000000 --- a/src/test/groovy/com/netgrif/application/engine/menu/MenuImportExportTest.groovy +++ /dev/null @@ -1,202 +0,0 @@ -package com.netgrif.application.engine.menu - -import com.netgrif.application.engine.TestHelper -import com.netgrif.application.engine.auth.domain.Authority -import com.netgrif.application.engine.auth.domain.User -import com.netgrif.application.engine.auth.domain.UserState -import com.netgrif.application.engine.auth.service.UserService -import com.netgrif.application.engine.orgstructure.groups.NextGroupService -import com.netgrif.application.engine.petrinet.domain.I18nString -import com.netgrif.application.engine.petrinet.domain.dataset.FileFieldValue -import com.netgrif.application.engine.petrinet.domain.roles.ProcessRole -import com.netgrif.application.engine.startup.* -import com.netgrif.application.engine.workflow.domain.Case -import com.netgrif.application.engine.workflow.domain.QCase -import com.netgrif.application.engine.workflow.domain.QTask -import com.netgrif.application.engine.workflow.domain.Task -import com.netgrif.application.engine.workflow.domain.eventoutcomes.dataoutcomes.SetDataEventOutcome -import com.netgrif.application.engine.menu.domain.MenuAndFilters -import com.netgrif.application.engine.workflow.domain.repositories.CaseRepository -import com.netgrif.application.engine.workflow.service.interfaces.IDataService -import com.netgrif.application.engine.workflow.service.interfaces.ITaskService -import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Disabled -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.context.SpringBootTest -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken -import org.springframework.security.core.Authentication -import org.springframework.security.core.context.SecurityContextHolder -import org.springframework.test.context.ActiveProfiles -import org.springframework.test.context.junit.jupiter.SpringExtension - - -@ExtendWith(SpringExtension.class) -@ActiveProfiles(["test"]) -@SpringBootTest -class MenuImportExportTest { - - public static final String DUMMY_USER_MAIL = "dummy@netgrif.com" - public static final String DUMMY_USER_PASSWORD = "password" - public static final String DUMMY_USER_GROUP_TITLE = "Dummy User" - - private static final String TEST_NET = "mortgage_net.xml" - private static final String TEST_XML_FILE_PATH = "src/test/resources/menu_file_test.xml" - - private static final String GROUP_NAV_TASK = "navigationMenuConfig" - private static final String IMPORT_FILE_FIELD = "import_menu_file" - private static final String EXPORT_FILE_FIELD = "export_menu_file" - - private static final String IMPORT_BUTTON_FIELD = "import_menu_btn" - private static final String EXPORT_BUTTON_FIELD = "export_menu_btn" - private static final String IMPORT_RESULTS_FIELD = "import_results" - private static final String EXPORT_MENUS_FIELD = "menus_for_export" - private static final String IMPORTED_IDS_FIELD = "imported_menu_ids" - private static final String MENU_NAME_FIELD = "menu_identifier" - - private static final String EXPECTED_RESULTS = "\n" + - "IMPORTING MENU \"defaultMenu\":\n" + - "\n" + - "Menu entry \"My cases\": OK\n" + - "\n" + - "Menu entry \"All cases\": OK\n" + - "\n" + - "IMPORTING MENU \"defaultMenu\":\n" + - "\n" + - "Menu entry \"All tasks\": OK\n" + - "\n" + - "Menu entry \"My tasks\": OK\n" - - @Autowired - MenuRunner menuRunner - - @Autowired - TestHelper testHelper - - @Autowired - private CaseRepository repository; - - @Autowired - IWorkflowService workflowService - - @Autowired - ImportHelper importHelper - - @Autowired - ITaskService taskService - - @Autowired - private IDataService dataService - - @Autowired - private GroupRunner groupRunner - - @Autowired - private UserService userService - - @Autowired - private CaseRepository caseRepository - - @Autowired - private NextGroupService nextGroupService - - @Autowired - private SuperCreator superCreator - - private User dummyUser; - - private Authentication userAuth - - @BeforeEach - void beforeTest() { - this.testHelper.truncateDbs(); - this.dummyUser = createDummyUser(); - } - - - @Test - @Disabled("Fix IllegalArgument") - void testMenuImportExport() { - userAuth = new UsernamePasswordAuthenticationToken(dummyUser.transformToLoggedUser(), DUMMY_USER_PASSWORD) - SecurityContextHolder.getContext().setAuthentication(userAuth) - - def testNet = importHelper.createNet(TEST_NET) - assert testNet.isPresent() - - Optional caseOptional = caseRepository.findOne(QCase.case$.title.eq(DUMMY_USER_GROUP_TITLE)); - assert caseOptional.isPresent() - Case groupCase = caseOptional.get() - - File testXmlMenu = new File(TEST_XML_FILE_PATH); - - groupCase.dataSet[IMPORT_FILE_FIELD].value = FileFieldValue.fromString(testXmlMenu.getName() + ":" + testXmlMenu.getPath()) - workflowService.save(groupCase) - - QTask qTask = new QTask("task"); - Task task = taskService.searchOne(qTask.transitionId.eq(GROUP_NAV_TASK).and(qTask.caseId.eq(groupCase.stringId))); - dataService.setData(task, ImportHelper.populateDataset([ - (IMPORT_BUTTON_FIELD): [ - "value": "1", - "type" : "button" - ] - ])) - Optional caseOpt = caseRepository.findOne(QCase.case$.title.eq(DUMMY_USER_GROUP_TITLE)) - assert caseOpt.isPresent() - groupCase = caseOpt.get() - - String importResults = groupCase.getDataField(IMPORT_RESULTS_FIELD).getValue().toString() - assert importResults <=> EXPECTED_RESULTS - - ArrayList imported_ids_list = groupCase.getDataSet().get(IMPORTED_IDS_FIELD).getValue() as ArrayList - assert imported_ids_list.size() == 4 - - Map menusForExportOptions = new LinkedHashMap<>() - String[] split1 = imported_ids_list.get(0).split(",") - String[] split2 = imported_ids_list.get(1).split(",") - String[] split3 = imported_ids_list.get(2).split(",") - String[] split4 = imported_ids_list.get(3).split(",") - - String menuName1 = workflowService.findOne(split1[0]).getDataSet().get(MENU_NAME_FIELD).getValue().toString() - String menuName2 = workflowService.findOne(split3[0]).getDataSet().get(MENU_NAME_FIELD).getValue().toString() - assert menuName1 == "defaultMenu" - assert menuName2 == "newMenu" - - menusForExportOptions.put(split1[0] + "," + split2[0], new I18nString(menuName1)) - menusForExportOptions.put(split3[0] + "," + split4[0], new I18nString(menuName2)) - - groupCase.dataSet[EXPORT_MENUS_FIELD].setOptions(menusForExportOptions) - workflowService.save(groupCase) - - task = taskService.searchOne(qTask.transitionId.eq(GROUP_NAV_TASK).and(qTask.caseId.eq(groupCase.stringId))); - setData(task, [(EXPORT_BUTTON_FIELD): ["type": "button", "value": "1"]]) - - caseOpt = caseRepository.findOne(QCase.case$.title.eq(DUMMY_USER_GROUP_TITLE)) - assert caseOpt.isPresent() - groupCase = caseOpt.get() - - FileFieldValue exportFileField = groupCase.getDataField(EXPORT_FILE_FIELD).getValue() as FileFieldValue - File exportedFiltersFile = new File(exportFileField.getPath()) - assert exportedFiltersFile.exists() - - MenuAndFilters original = menuImportExportService.invokeMethod("loadFromXML", [FileFieldValue.fromString(testXmlMenu.getName() + ":" + testXmlMenu.getPath())] as Object[]) as MenuAndFilters - MenuAndFilters exported = menuImportExportService.invokeMethod("loadFromXML", [exportFileField] as Object[]) as MenuAndFilters - - assert Objects.equals(original, exported); - } - - private User createDummyUser() { - def auths = importHelper.createAuthorities(["user": Authority.user, "admin": Authority.admin]) - return importHelper.createUser(new User(name: "Dummy", surname: "User", email: DUMMY_USER_MAIL, password: DUMMY_USER_PASSWORD, state: UserState.ACTIVE), - [auths.get("user")] as Authority[], - [] as ProcessRole[]) - } - - - private SetDataEventOutcome setData(task, Map> values) { - return dataService.setData(task, ImportHelper.populateDataset(values)) - } - - -} \ No newline at end of file diff --git a/src/test/resources/menu_file_test.xml b/src/test/resources/menu_file_test.xml deleted file mode 100644 index eed7edc13a..0000000000 --- a/src/test/resources/menu_file_test.xml +++ /dev/null @@ -1,191 +0,0 @@ - - - - - - My cases - 618b9afb5f420c67b6d6ce66 - - client - mortgage - - - account_clerk - mortgage - - - loan_officer - mortgage - - - property_appraiser - mortgage - - - - All cases - 618b9afc5f420c67b6d6d36b - - client - mortgage - - - account_clerk - mortgage - - - loan_officer - mortgage - - - property_appraiser - mortgage - - - - - - All tasks - 618b9af95f420c67b6d6c45c - - client - mortgage - - - account_clerk - mortgage - - - loan_officer - mortgage - - - property_appraiser - mortgage - - - - My tasks - 618b9afa5f420c67b6d6c961 - - - - - - 618b9afb5f420c67b6d6ce66 - - My cases - - Meine Fälle - Moje prípady - - - (author:<<me>>) - public - Case - assignment_ind - - Case - true - true - - case_author - - - - - case_author - - equals - - - - search.category.userMe - - <<me>> - - - - - - - - - - 618b9afc5f420c67b6d6d36b - - All cases - - Alle Fälle - Všetky prípady - - - public - Case - assignment - - Case - true - true - - - - 618b9af95f420c67b6d6c45c - - All tasks - - Alle Aufgaben - Všetky úlohy - - - public - Task - library_add_check - - Task - true - true - - - - 618b9afa5f420c67b6d6c961 - - My tasks - - Meine Aufgaben - Moje úlohy - - - (userId:<<me>>) - public - Task - account_box - - Task - true - true - - task_assignee - - - - - task_assignee - - equals - - - - search.category.userMe - - <<me>> - - - - - - - - - - From ac0aa6e491e5c2b36a2add0c67ec3fa841a53da4 Mon Sep 17 00:00:00 2001 From: chvostek Date: Mon, 31 Aug 2026 17:05:22 +0200 Subject: [PATCH 13/20] [NAE-2466] Advanced search for PFQL - remove export classes --- .../application/engine/menu/domain/Menu.java | 51 ---------------- .../engine/menu/domain/MenuAndFilters.java | 54 ----------------- .../engine/menu/domain/MenuEntry.java | 58 ------------------- .../engine/menu/domain/MenuEntryRole.java | 51 ---------------- .../engine/menu/domain/MenuList.java | 47 --------------- 5 files changed, 261 deletions(-) delete mode 100644 src/main/java/com/netgrif/application/engine/menu/domain/Menu.java delete mode 100644 src/main/java/com/netgrif/application/engine/menu/domain/MenuAndFilters.java delete mode 100644 src/main/java/com/netgrif/application/engine/menu/domain/MenuEntry.java delete mode 100644 src/main/java/com/netgrif/application/engine/menu/domain/MenuEntryRole.java delete mode 100644 src/main/java/com/netgrif/application/engine/menu/domain/MenuList.java diff --git a/src/main/java/com/netgrif/application/engine/menu/domain/Menu.java b/src/main/java/com/netgrif/application/engine/menu/domain/Menu.java deleted file mode 100644 index cce9388ba2..0000000000 --- a/src/main/java/com/netgrif/application/engine/menu/domain/Menu.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.netgrif.application.engine.menu.domain; - -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlRootElement; -import javax.xml.bind.annotation.XmlType; -import java.util.List; -import java.util.Objects; - -@Data -@NoArgsConstructor -@AllArgsConstructor -@XmlAccessorType(XmlAccessType.FIELD) -@XmlType(name = "", propOrder = { - "menuEntries", -}) - -@XmlRootElement(name = "menu") -public class Menu { - - @JacksonXmlElementWrapper(useWrapping = false) - @JacksonXmlProperty(localName = "menuEntry") - protected List menuEntries; - - @JacksonXmlProperty(isAttribute = true, localName = "name") - protected String menuIdentifier; - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - Menu menu = (Menu) o; - - if (!Objects.equals(menuEntries, menu.menuEntries)) return false; - return Objects.equals(menuIdentifier, menu.menuIdentifier); - } - - @Override - public int hashCode() { - int result = menuEntries != null ? menuEntries.hashCode() : 0; - result = 31 * result + (menuIdentifier != null ? menuIdentifier.hashCode() : 0); - return result; - } -} diff --git a/src/main/java/com/netgrif/application/engine/menu/domain/MenuAndFilters.java b/src/main/java/com/netgrif/application/engine/menu/domain/MenuAndFilters.java deleted file mode 100644 index e12f019fab..0000000000 --- a/src/main/java/com/netgrif/application/engine/menu/domain/MenuAndFilters.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.netgrif.application.engine.menu.domain; - -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; -import com.netgrif.application.engine.workflow.domain.filter.FilterImportExportList; -import lombok.AllArgsConstructor; -import lombok.Data; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlType; -import java.util.Objects; - -@Data -@AllArgsConstructor -@XmlAccessorType(XmlAccessType.FIELD) -@XmlType(name = "", propOrder = { - "menuList", - "filterList" -}) - -@JacksonXmlRootElement(localName = "menusWithFilters") -public class MenuAndFilters { - @JacksonXmlElementWrapper(useWrapping = false) - @JacksonXmlProperty(localName = "menus") - protected MenuList menuList; - @JacksonXmlElementWrapper(useWrapping = false) - @JacksonXmlProperty(localName = "filters") - protected FilterImportExportList filterList; - - public MenuAndFilters() { - this.menuList = new MenuList(); - this.filterList = new FilterImportExportList(); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - MenuAndFilters that = (MenuAndFilters) o; - - if (!Objects.equals(menuList, that.menuList)) return false; - return Objects.equals(filterList, that.filterList); - } - - @Override - public int hashCode() { - int result = menuList != null ? menuList.hashCode() : 0; - result = 31 * result + (filterList != null ? filterList.hashCode() : 0); - return result; - } -} diff --git a/src/main/java/com/netgrif/application/engine/menu/domain/MenuEntry.java b/src/main/java/com/netgrif/application/engine/menu/domain/MenuEntry.java deleted file mode 100644 index c3900ddf51..0000000000 --- a/src/main/java/com/netgrif/application/engine/menu/domain/MenuEntry.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.netgrif.application.engine.menu.domain; - -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlType; -import java.util.List; -import java.util.Objects; - -@Data -@NoArgsConstructor -@AllArgsConstructor -@XmlAccessorType(XmlAccessType.FIELD) -@XmlType(name = "menuEntry", propOrder = { - "entryName", - "menuEntryRoleList", -}) -public class MenuEntry { - - @XmlElement(required = true) - protected String entryName; - @EqualsAndHashCode.Exclude - @XmlElement(required = true) - protected String filterCaseId; - @JacksonXmlElementWrapper(useWrapping = false) - @JacksonXmlProperty(localName = "entryRole") - protected List menuEntryRoleList; - @JacksonXmlProperty(isAttribute = true) - protected Boolean useIcon; - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - MenuEntry menuEntry = (MenuEntry) o; - - if (!Objects.equals(entryName, menuEntry.entryName)) return false; - if (!Objects.equals(menuEntryRoleList, menuEntry.menuEntryRoleList)) - return false; - return Objects.equals(useIcon, menuEntry.useIcon); - } - - @Override - public int hashCode() { - int result = entryName != null ? entryName.hashCode() : 0; - result = 31 * result + (menuEntryRoleList != null ? menuEntryRoleList.hashCode() : 0); - result = 31 * result + (useIcon != null ? useIcon.hashCode() : 0); - return result; - } -} \ No newline at end of file diff --git a/src/main/java/com/netgrif/application/engine/menu/domain/MenuEntryRole.java b/src/main/java/com/netgrif/application/engine/menu/domain/MenuEntryRole.java deleted file mode 100644 index dd39ea86e8..0000000000 --- a/src/main/java/com/netgrif/application/engine/menu/domain/MenuEntryRole.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.netgrif.application.engine.menu.domain; - -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; -import com.netgrif.application.engine.workflow.domain.AuthorizationType; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlType; -import java.util.Objects; - -@Data -@NoArgsConstructor -@AllArgsConstructor -@XmlAccessorType(XmlAccessType.FIELD) -@XmlType(name = "", propOrder = { - "roleImportId", - "netImportId" -}) -public class MenuEntryRole { - - @XmlElement(required = true) - protected String roleImportId; - @XmlElement(required = true) - protected String netImportId; - @JacksonXmlProperty(localName = "type", isAttribute = true) - protected AuthorizationType authorizationType; - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - MenuEntryRole that = (MenuEntryRole) o; - - if (!Objects.equals(roleImportId, that.roleImportId)) return false; - if (!Objects.equals(netImportId, that.netImportId)) return false; - return authorizationType == that.authorizationType; - } - - @Override - public int hashCode() { - int result = roleImportId != null ? roleImportId.hashCode() : 0; - result = 31 * result + (netImportId != null ? netImportId.hashCode() : 0); - result = 31 * result + (authorizationType != null ? authorizationType.hashCode() : 0); - return result; - } -} \ No newline at end of file diff --git a/src/main/java/com/netgrif/application/engine/menu/domain/MenuList.java b/src/main/java/com/netgrif/application/engine/menu/domain/MenuList.java deleted file mode 100644 index e76f0f7e40..0000000000 --- a/src/main/java/com/netgrif/application/engine/menu/domain/MenuList.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.netgrif.application.engine.menu.domain; - -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; -import lombok.AllArgsConstructor; -import lombok.Data; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlRootElement; -import javax.xml.bind.annotation.XmlType; -import java.util.ArrayList; -import java.util.List; - -@Data -@AllArgsConstructor -@XmlAccessorType(XmlAccessType.FIELD) -@XmlType(name = "", propOrder = { - "menus", -}) - -@XmlRootElement(name = "menus") -public class MenuList { - - @JacksonXmlElementWrapper(useWrapping = false) - @JacksonXmlProperty(localName = "menu") - protected List menus; - - public MenuList() { - this.menus = new ArrayList<>(); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - MenuList menuList = (MenuList) o; - - return menus.equals(menuList.menus); - } - - @Override - public int hashCode() { - return menus.hashCode(); - } -} From 6ce1542c5ed61368e5eb8bc20d61d3c5bad65ac4 Mon Sep 17 00:00:00 2001 From: chvostek Date: Wed, 2 Sep 2026 16:16:40 +0200 Subject: [PATCH 14/20] [NAE-2466] Advanced search for PFQL - resolve PR comments - fix QueryLangTest --- .../engine/startup/FinisherRunner.groovy | 12 ++++ .../pfql/domain/enums/ComparisonType.java | 3 +- .../pfql/service/QueryLangEvaluator.java | 9 ++- .../pfql/service/utils/SearchUtils.java | 57 +++++++++++++------ .../workflow/web/WorkflowController.java | 1 - .../engine/pfql/QueryLangTest.java | 2 +- 6 files changed, 62 insertions(+), 22 deletions(-) diff --git a/src/main/groovy/com/netgrif/application/engine/startup/FinisherRunner.groovy b/src/main/groovy/com/netgrif/application/engine/startup/FinisherRunner.groovy index 25f1ccd520..3fe16e48df 100644 --- a/src/main/groovy/com/netgrif/application/engine/startup/FinisherRunner.groovy +++ b/src/main/groovy/com/netgrif/application/engine/startup/FinisherRunner.groovy @@ -1,7 +1,10 @@ package com.netgrif.application.engine.startup +import com.netgrif.application.engine.menu.service.MenuItemService +import com.netgrif.application.engine.menu.service.MenuItemTemplateHolder import org.slf4j.Logger import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Component @Component @@ -9,8 +12,17 @@ class FinisherRunner extends AbstractOrderedCommandLineRunner { private static final Logger log = LoggerFactory.getLogger(FinisherRunner) + @Autowired + private MenuItemService menuItemService + @Override void run(String... strings) throws Exception { +// menuItemService.createOrIgnoreMenuItem(MenuItemTemplateHolder.get("tabbed_case_view", "/", "tabbed_case_view").get()) +// menuItemService.createOrIgnoreMenuItem(MenuItemTemplateHolder.get("tabbed_task_view", "/", "tabbed_task_view").get()) +// menuItemService.createOrIgnoreMenuItem(MenuItemTemplateHolder.get("tabbed_ticket_view", "/", "tabbed_ticket_view").get()) +// menuItemService.createOrIgnoreMenuItem(MenuItemTemplateHolder.get("simple_case_view", "/", "simple_case_view").get()) +// menuItemService.createOrIgnoreMenuItem(MenuItemTemplateHolder.get("simple_task_view", "/", "simple_task_view").get()) +// menuItemService.createOrIgnoreMenuItem(MenuItemTemplateHolder.get("single_task_view", "/", "single_task_view").get()) log.info("+----------------------------+") log.info("| Netgrif Application Engine |") log.info("+----------------------------+") diff --git a/src/main/java/com/netgrif/application/engine/pfql/domain/enums/ComparisonType.java b/src/main/java/com/netgrif/application/engine/pfql/domain/enums/ComparisonType.java index e1d39fddd1..00394b679b 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/domain/enums/ComparisonType.java +++ b/src/main/java/com/netgrif/application/engine/pfql/domain/enums/ComparisonType.java @@ -8,5 +8,6 @@ public enum ComparisonType { DATETIME, BOOLEAN, OPTIONS, - NULL + NULL, + LIKE } diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/QueryLangEvaluator.java b/src/main/java/com/netgrif/application/engine/pfql/service/QueryLangEvaluator.java index 6281a61c02..0171d6c45e 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/QueryLangEvaluator.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/QueryLangEvaluator.java @@ -1482,7 +1482,8 @@ public void exitPlacesRange(QueryLangParser.PlacesRangeContext ctx) { } setMongoQuery(ctx, null); - setElasticQuery(ctx, buildElasticQueryInRange("places." + placeId + ".marking", leftNumberAsString, leftEndpointOpen, rightNumberAsString, rightEndpointOpen, not)); + setElasticQuery(ctx, buildElasticQueryInRange("places." + placeId + ".marking", leftNumberAsString, + leftEndpointOpen, rightNumberAsString, rightEndpointOpen, not)); this.searchWithElastic = true; } @@ -1842,6 +1843,7 @@ public void exitTasksUserIdNull(QueryLangParser.TasksUserIdNullContext ctx) { public void exitTitleLike(QueryLangParser.TitleLikeContext ctx) { StringPath stringPath; Token op = ctx.stringLikeComparison().stringComparison().op; + checkOp(ComparisonType.LIKE, op); boolean not = ctx.stringLikeComparison().stringComparison().NOT() != null; String string = handleStringComparisonWithPlaceholders(ctx.stringLikeComparison().stringComparison()); @@ -1861,15 +1863,16 @@ public void exitTitleLike(QueryLangParser.TitleLikeContext ctx) { throw new IllegalArgumentException("Unknown query type: " + resourceType); } + boolean negate = (op.getType() == QueryLangParser.NEQ) != not; Predicate mongoQuery = stringPath.likeIgnoreCase("%" + string + "%"); - setMongoQuery(ctx, not ? mongoQuery.not() : mongoQuery); + setMongoQuery(ctx, negate ? mongoQuery.not() : mongoQuery); } @Override public void exitDataStringLike(QueryLangParser.DataStringLikeContext ctx) { String fieldId = ctx.dataValue().fieldId.getText(); Token op = ctx.stringLikeComparison().stringComparison().op; - checkOp(ComparisonType.STRING, op); + checkOp(ComparisonType.LIKE, op); boolean not = ctx.stringLikeComparison().stringComparison().NOT() != null; String string = handleStringComparisonWithPlaceholders(ctx.stringLikeComparison().stringComparison()); 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 a56c4a5346..fe8a404fdd 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 @@ -46,7 +46,8 @@ public class SearchUtils { ComparisonType.DATE, List.of(QueryLangParser.EQ, QueryLangParser.NEQ, QueryLangParser.LT, QueryLangParser.LTE, QueryLangParser.GT, QueryLangParser.GTE), ComparisonType.DATETIME, List.of(QueryLangParser.EQ, QueryLangParser.NEQ, QueryLangParser.LT, QueryLangParser.LTE, QueryLangParser.GT, QueryLangParser.GTE), ComparisonType.BOOLEAN, List.of(QueryLangParser.EQ, QueryLangParser.NEQ), - ComparisonType.NULL, List.of(QueryLangParser.EQ, QueryLangParser.NEQ) + ComparisonType.NULL, List.of(QueryLangParser.EQ, QueryLangParser.NEQ), + ComparisonType.LIKE, List.of(QueryLangParser.EQ, QueryLangParser.NEQ) ); public static final Map processAttrToSortPropMapping = Map.of( @@ -184,11 +185,7 @@ public static String explainQuery(String input) { } public static String getStringValue(String queryLangString) { - String result = queryLangString.replace("'", ""); - if (result.isBlank()) { - return "\"\""; - } - return result.contains(" ") ? "\"" + result + "\"" : result; + return queryLangString.replace("'", ""); } public static ObjectId getObjectIdValue(String queryLangString) { @@ -407,6 +404,28 @@ public static Predicate buildDateTimePredicateInRange(DateTimePath values, boolean not) { + values = quoteForElastic(values); + String valuesQuery = "(" + String.join(" OR ", values) + ")"; + return doBuildElasticQuery(attribute, QueryLangParser.IN, valuesQuery, not); + } + + public static String buildElasticQueryInRange(String attribute, String leftValue, boolean leftEndpointOpen, String rightValue, boolean rightEndpointOpen, boolean not) { + leftValue = quoteForElastic(leftValue); + rightValue = quoteForElastic(rightValue); + String query = "(" + + doBuildElasticQuery(attribute, leftEndpointOpen ? QueryLangParser.GT : QueryLangParser.GTE, leftValue, false) + + " AND " + + doBuildElasticQuery(attribute, rightEndpointOpen ? QueryLangParser.LT : QueryLangParser.LTE, rightValue, false) + + ")"; + return not ? "NOT " + query : query; + } + + protected static String doBuildElasticQuery(String attribute, int op, String value, boolean not) { String query = null; switch (op) { case QueryLangParser.EQ: @@ -444,17 +463,23 @@ public static String buildElasticQuery(String attribute, int op, String value, b return query; } - public static String buildElasticQueryInList(String attribute, List values, boolean not) { - String valuesQuery = "(" + String.join(" OR ", values) + ")"; - return buildElasticQuery(attribute, QueryLangParser.IN, valuesQuery, not); + protected static String quoteForElastic(String originValue) { + if (originValue == null || !originValue.contains(" ")) { + return originValue; + } + + String resultValue = originValue; + String fuzzy = ""; + int fuzzyIndex = originValue.indexOf('~'); + if (fuzzyIndex != -1) { + fuzzy = originValue.substring(fuzzyIndex); + resultValue = originValue.substring(0, fuzzyIndex); + } + + return "\"" + resultValue + "\"" + fuzzy; } - public static String buildElasticQueryInRange(String attribute, String leftValue, boolean leftEndpointOpen, String rightValue, boolean rightEndpointOpen, boolean not) { - String query = "(" - + buildElasticQuery(attribute, leftEndpointOpen ? QueryLangParser.GT : QueryLangParser.GTE, leftValue, false) - + " AND " - + buildElasticQuery(attribute, rightEndpointOpen ? QueryLangParser.LT : QueryLangParser.LTE, rightValue, false) - + ")"; - return not ? "NOT " + query : query; + protected static List quoteForElastic(List values) { + return values.stream().map(SearchUtils::quoteForElastic).collect(Collectors.toList()); } } diff --git a/src/main/java/com/netgrif/application/engine/workflow/web/WorkflowController.java b/src/main/java/com/netgrif/application/engine/workflow/web/WorkflowController.java index 8d03880d0f..545cd0e112 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/web/WorkflowController.java +++ b/src/main/java/com/netgrif/application/engine/workflow/web/WorkflowController.java @@ -134,7 +134,6 @@ public PagedModel searchPfql(@RequestBody SingleCaseSearchRequestA ResourceLinkAssembler.addLinks(resources, ElasticCase.class, selfLink.getRel().toString()); return resources; } catch (IllegalArgumentException e) { - log.warn("Bad request when calling PFQL search API: {}", e.getMessage()); throw new BadRequestException("Bad request: " + e.getMessage()); } catch (Exception e) { log.error("Something went wrong while searching cases by PFQL", e); diff --git a/src/test/java/com/netgrif/application/engine/pfql/QueryLangTest.java b/src/test/java/com/netgrif/application/engine/pfql/QueryLangTest.java index 9f6b28e5f0..1ffd16380a 100644 --- a/src/test/java/com/netgrif/application/engine/pfql/QueryLangTest.java +++ b/src/test/java/com/netgrif/application/engine/pfql/QueryLangTest.java @@ -1204,7 +1204,7 @@ public void testSimpleElasticCaseQuery() { assertEquals(expected, actual); actual = evaluateQuery("cases: title in (loggedUser.username : loggedUser.fullName)").getFullElasticQuery(); - expected = String.format("(title:>%s AND title:<%s)", systemUser.getUsername(), systemUser.getFullName()); + expected = String.format("(title:>%s AND title:<%s)", systemUser.getUsername(), "\"" + systemUser.getFullName() + "\""); assertEquals(expected, actual); From b52be07c73b14d37698ce15acbebba82958faad8 Mon Sep 17 00:00:00 2001 From: chvostek Date: Wed, 2 Sep 2026 16:24:47 +0200 Subject: [PATCH 15/20] [NAE-2466] Advanced search for PFQL - revert FinisherRunner --- .../application/engine/startup/FinisherRunner.groovy | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/main/groovy/com/netgrif/application/engine/startup/FinisherRunner.groovy b/src/main/groovy/com/netgrif/application/engine/startup/FinisherRunner.groovy index 3fe16e48df..25f1ccd520 100644 --- a/src/main/groovy/com/netgrif/application/engine/startup/FinisherRunner.groovy +++ b/src/main/groovy/com/netgrif/application/engine/startup/FinisherRunner.groovy @@ -1,10 +1,7 @@ package com.netgrif.application.engine.startup -import com.netgrif.application.engine.menu.service.MenuItemService -import com.netgrif.application.engine.menu.service.MenuItemTemplateHolder import org.slf4j.Logger import org.slf4j.LoggerFactory -import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Component @Component @@ -12,17 +9,8 @@ class FinisherRunner extends AbstractOrderedCommandLineRunner { private static final Logger log = LoggerFactory.getLogger(FinisherRunner) - @Autowired - private MenuItemService menuItemService - @Override void run(String... strings) throws Exception { -// menuItemService.createOrIgnoreMenuItem(MenuItemTemplateHolder.get("tabbed_case_view", "/", "tabbed_case_view").get()) -// menuItemService.createOrIgnoreMenuItem(MenuItemTemplateHolder.get("tabbed_task_view", "/", "tabbed_task_view").get()) -// menuItemService.createOrIgnoreMenuItem(MenuItemTemplateHolder.get("tabbed_ticket_view", "/", "tabbed_ticket_view").get()) -// menuItemService.createOrIgnoreMenuItem(MenuItemTemplateHolder.get("simple_case_view", "/", "simple_case_view").get()) -// menuItemService.createOrIgnoreMenuItem(MenuItemTemplateHolder.get("simple_task_view", "/", "simple_task_view").get()) -// menuItemService.createOrIgnoreMenuItem(MenuItemTemplateHolder.get("single_task_view", "/", "single_task_view").get()) log.info("+----------------------------+") log.info("| Netgrif Application Engine |") log.info("+----------------------------+") From 72d86f11c958397b068ab9367271ec5032d95d12 Mon Sep 17 00:00:00 2001 From: chvostek Date: Wed, 2 Sep 2026 17:46:24 +0200 Subject: [PATCH 16/20] [NAE-2466] Advanced search for PFQL - fix fuzzy search when phrases are encountered in PFQL query --- .../pfql/service/QueryLangEvaluator.java | 2 +- .../pfql/service/utils/SearchUtils.java | 26 ++++++++++++------- .../engine/pfql/QueryLangTest.java | 22 +++++++++------- src/test/resources/petriNets/pfql.xml | 2 +- 4 files changed, 31 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/pfql/service/QueryLangEvaluator.java b/src/main/java/com/netgrif/application/engine/pfql/service/QueryLangEvaluator.java index 0171d6c45e..9b8a397142 100644 --- a/src/main/java/com/netgrif/application/engine/pfql/service/QueryLangEvaluator.java +++ b/src/main/java/com/netgrif/application/engine/pfql/service/QueryLangEvaluator.java @@ -40,7 +40,7 @@ public class QueryLangEvaluator extends QueryLangBaseListener { private final ParseTreeProperty elasticQuery = new ParseTreeProperty<>(); private final ParseTreeProperty mongoQuery = new ParseTreeProperty<>(); - private final String elasticFuzzyMaxDistance = "2"; + private final String elasticFuzzyMaxDistance = "AUTO"; private final IUserService userService; 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 fe8a404fdd..d4a7045e50 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 @@ -463,23 +463,29 @@ protected static String doBuildElasticQuery(String attribute, int op, String val return query; } + protected static List quoteForElastic(List values) { + return values.stream().map(SearchUtils::quoteForElastic).collect(Collectors.toList()); + } + + /** + * Adds quotes for value that is going to be used in the Elasticsearch query. If the value does not contain a fuzzy symbol, + * origin value wrapped in quotes is returned. For example, `someValue anotherValue` -> `"someValue anotherValue"`. If the value + * contains a fuzzy symbol, it places the fuzzy symbol after every term. For example, `someVxlue anotherVxlue~AUTO` -> + * `(someVxlue~AUTO AND anotherVxlue~AUTO)` + */ protected static String quoteForElastic(String originValue) { if (originValue == null || !originValue.contains(" ")) { return originValue; } - String resultValue = originValue; - String fuzzy = ""; int fuzzyIndex = originValue.indexOf('~'); - if (fuzzyIndex != -1) { - fuzzy = originValue.substring(fuzzyIndex); - resultValue = originValue.substring(0, fuzzyIndex); - } - - return "\"" + resultValue + "\"" + fuzzy; + return fuzzyIndex != -1 ? resolvePhraseWithFuzzy(originValue, fuzzyIndex) : "\"" + originValue + "\""; } - protected static List quoteForElastic(List values) { - return values.stream().map(SearchUtils::quoteForElastic).collect(Collectors.toList()); + protected static String resolvePhraseWithFuzzy(String originPhraseWithFuzzy, int fuzzyIndex) { + String fuzzy = originPhraseWithFuzzy.substring(fuzzyIndex); + String originPhraseWithoutFuzzy = originPhraseWithFuzzy.substring(0, fuzzyIndex); + String[] splitPhrase = originPhraseWithoutFuzzy.split(" "); + return "(" + String.join(fuzzy + " AND ", splitPhrase) + fuzzy + ")"; } } diff --git a/src/test/java/com/netgrif/application/engine/pfql/QueryLangTest.java b/src/test/java/com/netgrif/application/engine/pfql/QueryLangTest.java index 1ffd16380a..2b13d26161 100644 --- a/src/test/java/com/netgrif/application/engine/pfql/QueryLangTest.java +++ b/src/test/java/com/netgrif/application/engine/pfql/QueryLangTest.java @@ -98,10 +98,10 @@ public void testSearchService() throws InterruptedException { assertEquals("Test 03", ((Case) case3).getTitle()); assertEquals(3, ((Case) case3).getFieldValue("number_0")); - Object case4 = searchService.search("case: processIdentifier eq 'query_test' and data.text_0.value == '4'"); + Object case4 = searchService.search("case: processIdentifier eq 'query_test' and data.text_0.value == '444'"); assertNotNull(case4); assertEquals(Case.class, case4.getClass()); - assertEquals("4", ((Case) case4).getFieldValue("text_0")); + assertEquals("444", ((Case) case4).getFieldValue("text_0")); Object case5 = searchService.search("case: processIdentifier eq 'query_test' and data.boolean_0.value == true"); assertNotNull(case5); @@ -121,14 +121,14 @@ public void testSearchService() throws InterruptedException { cases = searchService.search("cases: processIdentifier eq 'query_test' and data.boolean_0.value == true"); assertEquals(5, ((Page) cases).getTotalElements()); - cases = searchService.search("cases: processIdentifier eq 'query_test' and data.boolean_0.value == true and data.text_0.value != '4'"); + cases = searchService.search("cases: processIdentifier eq 'query_test' and data.boolean_0.value == true and data.text_0.value != '444'"); assertEquals(4, ((Page) cases).getTotalElements()); cases = searchService.search("cases: processIdentifier eq 'query_test' and author eq loggedUser.id"); assertEquals(10, ((Page) cases).getTotalElements()); - cases = searchService.search("cases: processIdentifier eq 'query_test' and data.text_0.value eq 'x'*"); - assertEquals(10, ((Page) cases).getTotalElements()); + cases = searchService.search("cases: processIdentifier eq 'query_test' and data.text_0.value eq 'x44'*"); + assertEquals(1, ((Page) cases).getTotalElements()); } @Test @@ -1163,11 +1163,11 @@ public void testSimpleElasticCaseQuery() { checkDateComparisonElastic("case", "data.field3.value", "dataSet.field3.timestampValue"); actual = evaluateQuery("case: data.field1.value eq 'somxthing'*").getFullElasticQuery(); - expected = "dataSet.field1.fulltextValue:somxthing~2"; + expected = "dataSet.field1.fulltextValue:somxthing~AUTO"; assertEquals(expected, actual); actual = evaluateQuery("case: data.field1.value neq 'somxthing'*").getFullElasticQuery(); - expected = "NOT dataSet.field1.fulltextValue:somxthing~2"; + expected = "NOT dataSet.field1.fulltextValue:somxthing~AUTO"; assertEquals(expected, actual); actual = evaluateQuery("case: data.field1.value eq true").getFullElasticQuery(); @@ -1217,11 +1217,15 @@ public void testSimpleElasticCaseQuery() { assertEquals(expected, actual); actual = evaluateQuery("case: title eq 'somxthing'*").getFullElasticQuery(); - expected = "title:somxthing~2"; + expected = "title:somxthing~AUTO"; assertEquals(expected, actual); actual = evaluateQuery("case: title not eq 'somxthing anxthing'*").getFullElasticQuery(); - expected = "NOT title:\"somxthing anxthing\"~2"; + expected = "NOT title:(somxthing~AUTO AND anxthing~AUTO)"; + assertEquals(expected, actual); + + actual = evaluateQuery("case: title not eq 'somxthing anxthing everxthing'*").getFullElasticQuery(); + expected = "NOT title:(somxthing~AUTO AND anxthing~AUTO AND everxthing~AUTO)"; assertEquals(expected, actual); } diff --git a/src/test/resources/petriNets/pfql.xml b/src/test/resources/petriNets/pfql.xml index 5f8fd2f677..d477094aef 100644 --- a/src/test/resources/petriNets/pfql.xml +++ b/src/test/resources/petriNets/pfql.xml @@ -25,7 +25,7 @@ text_0 Text - params["id"] + params["id"] + params["id"] + params["id"] text_1 From 938ee39d73e22f18f1c119602690917ff7006076 Mon Sep 17 00:00:00 2001 From: chvostek Date: Wed, 2 Sep 2026 18:05:35 +0200 Subject: [PATCH 17/20] [NAE-2466] Advanced search for PFQL - update splitting on whitespace --- .../application/engine/pfql/service/utils/SearchUtils.java | 2 +- .../java/com/netgrif/application/engine/pfql/QueryLangTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 d4a7045e50..de732f90df 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 @@ -485,7 +485,7 @@ protected static String quoteForElastic(String originValue) { protected static String resolvePhraseWithFuzzy(String originPhraseWithFuzzy, int fuzzyIndex) { String fuzzy = originPhraseWithFuzzy.substring(fuzzyIndex); String originPhraseWithoutFuzzy = originPhraseWithFuzzy.substring(0, fuzzyIndex); - String[] splitPhrase = originPhraseWithoutFuzzy.split(" "); + String[] splitPhrase = originPhraseWithoutFuzzy.trim().split("\\s+"); return "(" + String.join(fuzzy + " AND ", splitPhrase) + fuzzy + ")"; } } diff --git a/src/test/java/com/netgrif/application/engine/pfql/QueryLangTest.java b/src/test/java/com/netgrif/application/engine/pfql/QueryLangTest.java index 2b13d26161..20039d79c9 100644 --- a/src/test/java/com/netgrif/application/engine/pfql/QueryLangTest.java +++ b/src/test/java/com/netgrif/application/engine/pfql/QueryLangTest.java @@ -1224,7 +1224,7 @@ public void testSimpleElasticCaseQuery() { expected = "NOT title:(somxthing~AUTO AND anxthing~AUTO)"; assertEquals(expected, actual); - actual = evaluateQuery("case: title not eq 'somxthing anxthing everxthing'*").getFullElasticQuery(); + actual = evaluateQuery("case: title not eq 'somxthing anxthing everxthing '*").getFullElasticQuery(); expected = "NOT title:(somxthing~AUTO AND anxthing~AUTO AND everxthing~AUTO)"; assertEquals(expected, actual); } From cad62c51350348515d3d1cd5bc3140008840bf35 Mon Sep 17 00:00:00 2001 From: chvostek Date: Sun, 6 Sep 2026 09:30:05 +0200 Subject: [PATCH 18/20] [NAE-2466] Advanced search for PFQL - fix empty string quoting --- .../application/engine/pfql/service/utils/SearchUtils.java | 2 +- .../com/netgrif/application/engine/pfql/QueryLangTest.java | 4 ++++ 2 files changed, 5 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 de732f90df..d6689b1c10 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 @@ -474,7 +474,7 @@ protected static List quoteForElastic(List values) { * `(someVxlue~AUTO AND anotherVxlue~AUTO)` */ protected static String quoteForElastic(String originValue) { - if (originValue == null || !originValue.contains(" ")) { + if (originValue == null || !originValue.contains(" ") && !originValue.isEmpty()) { return originValue; } diff --git a/src/test/java/com/netgrif/application/engine/pfql/QueryLangTest.java b/src/test/java/com/netgrif/application/engine/pfql/QueryLangTest.java index 20039d79c9..c351461827 100644 --- a/src/test/java/com/netgrif/application/engine/pfql/QueryLangTest.java +++ b/src/test/java/com/netgrif/application/engine/pfql/QueryLangTest.java @@ -1190,6 +1190,10 @@ public void testSimpleElasticCaseQuery() { expected = "_exists_:dataSet.field1.fulltextValue"; assertEquals(expected, actual); + actual = evaluateQuery("case: data.field1.value eq ''").getFullElasticQuery(); + expected = "dataSet.field1.fulltextValue.keyword:\"\""; + assertEquals(expected, actual); + // data options comparison checkStringComparisonElastic("case", "data.field1.options", "dataSet.field1.options"); From 243629b72d3255e10a98d7756f7e213c86af2b3c Mon Sep 17 00:00:00 2001 From: chvostek Date: Sun, 6 Sep 2026 11:18:48 +0200 Subject: [PATCH 19/20] [NAE-2466] Advanced search for PFQL - update white space handling for quotes --- .../engine/pfql/service/utils/SearchUtils.java | 11 ++++++++++- .../application/engine/pfql/QueryLangTest.java | 8 ++++++++ 2 files changed, 18 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 d6689b1c10..d832d3ca55 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 @@ -474,7 +474,7 @@ protected static List quoteForElastic(List values) { * `(someVxlue~AUTO AND anotherVxlue~AUTO)` */ protected static String quoteForElastic(String originValue) { - if (originValue == null || !originValue.contains(" ") && !originValue.isEmpty()) { + if (originValue == null || (!containsWhitespace(originValue) && !originValue.isEmpty())) { return originValue; } @@ -488,4 +488,13 @@ protected static String resolvePhraseWithFuzzy(String originPhraseWithFuzzy, int String[] splitPhrase = originPhraseWithoutFuzzy.trim().split("\\s+"); return "(" + String.join(fuzzy + " AND ", splitPhrase) + fuzzy + ")"; } + + protected static boolean containsWhitespace(String value) { + for (int i = 0; i < value.length(); i++) { + if (Character.isWhitespace(value.charAt(i))) { + return true; + } + } + return false; + } } diff --git a/src/test/java/com/netgrif/application/engine/pfql/QueryLangTest.java b/src/test/java/com/netgrif/application/engine/pfql/QueryLangTest.java index c351461827..a5f6b2f20a 100644 --- a/src/test/java/com/netgrif/application/engine/pfql/QueryLangTest.java +++ b/src/test/java/com/netgrif/application/engine/pfql/QueryLangTest.java @@ -1220,6 +1220,14 @@ public void testSimpleElasticCaseQuery() { expected = "_exists_:title"; assertEquals(expected, actual); + actual = evaluateQuery("case: title eq 'white space test 1'").getFullElasticQuery(); + expected = "title.keyword:\"white space test 1\""; + assertEquals(expected, actual); + + actual = evaluateQuery("case: title eq 'white\tspace\ttest\t1'").getFullElasticQuery(); + expected = "title.keyword:\"white\tspace\ttest\t1\""; + assertEquals(expected, actual); + actual = evaluateQuery("case: title eq 'somxthing'*").getFullElasticQuery(); expected = "title:somxthing~AUTO"; assertEquals(expected, actual); From edef739e15ce7cfe8fcec17146e6ae279b0fba23 Mon Sep 17 00:00:00 2001 From: chvostek Date: Tue, 8 Sep 2026 09:50:07 +0200 Subject: [PATCH 20/20] [NAE-2466] Advanced search for PFQL - add value escaping when translating PFQL to Elasticsearch query --- .../pfql/service/utils/SearchUtils.java | 19 +++++++++++-------- .../engine/pfql/QueryLangTest.java | 4 ++++ 2 files changed, 15 insertions(+), 8 deletions(-) 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 d832d3ca55..b7b01c5fed 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 @@ -2,6 +2,7 @@ import com.netgrif.application.engine.auth.service.interfaces.IUserService; import com.netgrif.application.engine.configuration.ApplicationContextProvider; +import com.netgrif.application.engine.elastic.service.ElasticsearchQuerySanitizer; import com.netgrif.application.engine.petrinet.domain.QPetriNet; import com.netgrif.application.engine.petrinet.domain.version.QVersion; import com.netgrif.application.engine.petrinet.domain.version.Version; @@ -97,6 +98,7 @@ public class SearchUtils { public static final String LEFT_OPEN_ENDPOINT = "("; public static final String RIGHT_OPEN_ENDPOINT = ")"; + protected static final String[] ELASTIC_EXCLUDE_FROM_ESCAPING = new String[]{"~", " "}; public static String toDateString(LocalDate localDate) { return localDate.format(DateTimeFormatter.ISO_LOCAL_DATE); @@ -404,19 +406,19 @@ public static Predicate buildDateTimePredicateInRange(DateTimePath values, boolean not) { - values = quoteForElastic(values); + values = quoteAndSanitizeForElastic(values); String valuesQuery = "(" + String.join(" OR ", values) + ")"; return doBuildElasticQuery(attribute, QueryLangParser.IN, valuesQuery, not); } public static String buildElasticQueryInRange(String attribute, String leftValue, boolean leftEndpointOpen, String rightValue, boolean rightEndpointOpen, boolean not) { - leftValue = quoteForElastic(leftValue); - rightValue = quoteForElastic(rightValue); + leftValue = quoteAndSanitizeForElastic(leftValue); + rightValue = quoteAndSanitizeForElastic(rightValue); String query = "(" + doBuildElasticQuery(attribute, leftEndpointOpen ? QueryLangParser.GT : QueryLangParser.GTE, leftValue, false) + " AND " @@ -463,17 +465,18 @@ protected static String doBuildElasticQuery(String attribute, int op, String val return query; } - protected static List quoteForElastic(List values) { - return values.stream().map(SearchUtils::quoteForElastic).collect(Collectors.toList()); + protected static List quoteAndSanitizeForElastic(List values) { + return values.stream().map(SearchUtils::quoteAndSanitizeForElastic).collect(Collectors.toList()); } /** * Adds quotes for value that is going to be used in the Elasticsearch query. If the value does not contain a fuzzy symbol, * origin value wrapped in quotes is returned. For example, `someValue anotherValue` -> `"someValue anotherValue"`. If the value * contains a fuzzy symbol, it places the fuzzy symbol after every term. For example, `someVxlue anotherVxlue~AUTO` -> - * `(someVxlue~AUTO AND anotherVxlue~AUTO)` + * `(someVxlue~AUTO AND anotherVxlue~AUTO)`. The originValue is also sanitized using {@link ElasticsearchQuerySanitizer#sanitize(String, String[])}. */ - protected static String quoteForElastic(String originValue) { + protected static String quoteAndSanitizeForElastic(String originValue) { + originValue = ElasticsearchQuerySanitizer.sanitize(originValue, ELASTIC_EXCLUDE_FROM_ESCAPING); if (originValue == null || (!containsWhitespace(originValue) && !originValue.isEmpty())) { return originValue; } diff --git a/src/test/java/com/netgrif/application/engine/pfql/QueryLangTest.java b/src/test/java/com/netgrif/application/engine/pfql/QueryLangTest.java index a5f6b2f20a..38ea6de5cb 100644 --- a/src/test/java/com/netgrif/application/engine/pfql/QueryLangTest.java +++ b/src/test/java/com/netgrif/application/engine/pfql/QueryLangTest.java @@ -1239,6 +1239,10 @@ public void testSimpleElasticCaseQuery() { actual = evaluateQuery("case: title not eq 'somxthing anxthing everxthing '*").getFullElasticQuery(); expected = "NOT title:(somxthing~AUTO AND anxthing~AUTO AND everxthing~AUTO)"; assertEquals(expected, actual); + + actual = evaluateQuery("case: title eq 'needsTo/BeEscaped'").getFullElasticQuery(); + expected = "title.keyword:needsTo\\/BeEscaped"; + assertEquals(expected, actual); } @Test