From 412eb34600d1e375b943f601f0cbfc918d6bdf24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kov=C3=A1=C4=8Dik?= Date: Tue, 28 Oct 2025 14:26:58 +0100 Subject: [PATCH] [NAE-2243] Dynamic date resolution in Elastic query - implement ElasticQueryFactory to resolve dynamic queries as a valid java code - add boolean switch for dynamic queries --- .../elastic/domain/ElasticQueryConstants.java | 4 + .../elastic/service/ElasticCaseService.java | 18 ++- .../elastic/service/ElasticQueryFactory.java | 41 ++++++ .../interfaces/IElasticQueryFactory.java | 7 + .../web/requestbodies/CaseSearchRequest.java | 2 + .../elastic/ElasticQueryFactoryTest.groovy | 122 ++++++++++++++++++ 6 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 application-engine/src/main/java/com/netgrif/application/engine/elastic/service/ElasticQueryFactory.java create mode 100644 application-engine/src/main/java/com/netgrif/application/engine/elastic/service/interfaces/IElasticQueryFactory.java create mode 100644 application-engine/src/test/groovy/com/netgrif/application/engine/elastic/ElasticQueryFactoryTest.groovy diff --git a/application-engine/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticQueryConstants.java b/application-engine/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticQueryConstants.java index 70d7f005f5e..2e895311827 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticQueryConstants.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/elastic/domain/ElasticQueryConstants.java @@ -5,4 +5,8 @@ public class ElasticQueryConstants { * Should be replaced by user id in elastic query string queries */ public static final String USER_ID_TEMPLATE = "<>"; + public static final String DYNAMIC_USER_ID_TEMPLATE = "me"; + public static final String LOCAL_DATE_NOW_TEMPLATE = "now"; + public static final String LOCAL_DATE_TODAY_TEMPLATE = "today"; + public static final String LOGGED_USER_TEMPLATE = "loggedUser"; } diff --git a/application-engine/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java b/application-engine/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java index 07d69011897..be7aa2455bc 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java @@ -7,6 +7,7 @@ import co.elastic.clients.elasticsearch._types.query_dsl.TermsQueryField; import co.elastic.clients.elasticsearch._types.query_dsl.QueryBuilders; import com.netgrif.application.engine.configuration.properties.DataConfigurationProperties; +import com.netgrif.application.engine.elastic.service.interfaces.IElasticQueryFactory; import com.netgrif.application.engine.objects.auth.domain.LoggedUser; import com.netgrif.application.engine.objects.elastic.domain.ElasticCase; import com.netgrif.application.engine.elastic.domain.ElasticCaseRepository; @@ -39,6 +40,8 @@ import org.springframework.data.elasticsearch.core.query.Order; import org.springframework.stereotype.Service; +import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.*; import java.util.function.BinaryOperator; import java.util.stream.Collectors; @@ -86,6 +89,9 @@ public void setElasticProperties(DataConfigurationProperties.ElasticsearchProper this.elasticProperties = elasticProperties; } + @Autowired + protected IElasticQueryFactory queryFactory; + @Override public void remove(String caseId) { executors.execute(caseId, () -> { @@ -430,7 +436,17 @@ protected void buildStringQuery(CaseSearchRequest request, BoolQuery.Builder que return; } - String populatedQuery = request.query.replaceAll(ElasticQueryConstants.USER_ID_TEMPLATE, user.getId().toString()); + String populatedQuery; + if (request.dynamicQuery) { + populatedQuery = queryFactory.populateQuery(request.query, new HashMap<>(Map.of( + ElasticQueryConstants.DYNAMIC_USER_ID_TEMPLATE, user.getId().toString(), + ElasticQueryConstants.LOCAL_DATE_NOW_TEMPLATE, LocalDateTime.now(), + ElasticQueryConstants.LOCAL_DATE_TODAY_TEMPLATE, LocalDate.now(), + ElasticQueryConstants.LOGGED_USER_TEMPLATE, user + ))); + } else { + populatedQuery = request.query.replaceAll(ElasticQueryConstants.USER_ID_TEMPLATE, user.getId().toString()); + } query.must(QueryStringQuery.of(builder -> builder.query(populatedQuery).allowLeadingWildcard(true).analyzeWildcard(true))._toQuery()); } diff --git a/application-engine/src/main/java/com/netgrif/application/engine/elastic/service/ElasticQueryFactory.java b/application-engine/src/main/java/com/netgrif/application/engine/elastic/service/ElasticQueryFactory.java new file mode 100644 index 00000000000..c3b38a31f0b --- /dev/null +++ b/application-engine/src/main/java/com/netgrif/application/engine/elastic/service/ElasticQueryFactory.java @@ -0,0 +1,41 @@ +package com.netgrif.application.engine.elastic.service; + +import com.netgrif.application.engine.elastic.service.interfaces.IElasticQueryFactory; +import groovy.text.SimpleTemplateEngine; +import groovy.text.Template; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.codehaus.groovy.control.CompilationFailedException; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@Service +public class ElasticQueryFactory implements IElasticQueryFactory { + + private final SimpleTemplateEngine templateEngine; + @Getter + private final Map context; + + public ElasticQueryFactory() { + this.templateEngine = new SimpleTemplateEngine(); + this.context = new HashMap<>(); + } + + @Override + public String populateQuery(String query, Map queryContext) { + String populatedQuery = query; + try { + queryContext.putAll(context); + Template template = templateEngine.createTemplate(query); + populatedQuery = template.make(queryContext).toString(); + } catch (CompilationFailedException | ClassNotFoundException | IOException e) { + log.error("Cannot populate template from string query", e); + } + return populatedQuery; + } + +} diff --git a/application-engine/src/main/java/com/netgrif/application/engine/elastic/service/interfaces/IElasticQueryFactory.java b/application-engine/src/main/java/com/netgrif/application/engine/elastic/service/interfaces/IElasticQueryFactory.java new file mode 100644 index 00000000000..76210558112 --- /dev/null +++ b/application-engine/src/main/java/com/netgrif/application/engine/elastic/service/interfaces/IElasticQueryFactory.java @@ -0,0 +1,7 @@ +package com.netgrif.application.engine.elastic.service.interfaces; + +import java.util.Map; + +public interface IElasticQueryFactory { + String populateQuery(String query, Map queryContext); +} diff --git a/application-engine/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/CaseSearchRequest.java b/application-engine/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/CaseSearchRequest.java index 6702a1da499..440eab0b487 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/CaseSearchRequest.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/CaseSearchRequest.java @@ -44,6 +44,8 @@ public class CaseSearchRequest implements Serializable { public String query; + public boolean dynamicQuery; + @JsonFormat(with = JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY) public List id; diff --git a/application-engine/src/test/groovy/com/netgrif/application/engine/elastic/ElasticQueryFactoryTest.groovy b/application-engine/src/test/groovy/com/netgrif/application/engine/elastic/ElasticQueryFactoryTest.groovy new file mode 100644 index 00000000000..d99d8b3659a --- /dev/null +++ b/application-engine/src/test/groovy/com/netgrif/application/engine/elastic/ElasticQueryFactoryTest.groovy @@ -0,0 +1,122 @@ +package com.netgrif.application.engine.elastic; + +import com.netgrif.application.engine.ApplicationEngine; +import com.netgrif.application.engine.TestHelper +import com.netgrif.application.engine.adapter.spring.auth.domain.AuthorityImpl +import com.netgrif.application.engine.adapter.spring.workflow.domain.QCase +import com.netgrif.application.engine.auth.service.UserService +import com.netgrif.application.engine.elastic.domain.ElasticQueryConstants +import com.netgrif.application.engine.elastic.service.interfaces.IElasticQueryFactory +import com.netgrif.application.engine.objects.auth.domain.AbstractUser +import com.netgrif.application.engine.objects.auth.domain.ActorTransformer +import com.netgrif.application.engine.objects.auth.domain.Authority +import com.netgrif.application.engine.objects.auth.domain.User +import com.netgrif.application.engine.objects.auth.domain.enums.UserState +import com.netgrif.application.engine.objects.petrinet.domain.PetriNet +import com.netgrif.application.engine.objects.petrinet.domain.VersionType +import com.netgrif.application.engine.objects.petrinet.domain.roles.ProcessRole; +import com.netgrif.application.engine.startup.ImportHelper; +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.autoconfigure.web.servlet.AutoConfigureMockMvc; +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 +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.setup.MockMvcBuilders +import org.springframework.web.context.WebApplicationContext + +import java.sql.Timestamp +import java.time.Duration +import java.time.LocalDate +import java.time.LocalDateTime + +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; + +@ExtendWith(SpringExtension.class) +@ActiveProfiles(["test"]) +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = ApplicationEngine.class +) +@AutoConfigureMockMvc +class ElasticQueryFactoryTest { + + public static final String USER_EMAIL = "test@mail.sk" + public static final String USER_PASSWORD = "password" + + @Autowired + private TestHelper testHelper + + @Autowired + protected IElasticQueryFactory queryFactory; + + @Autowired + private UserService userService + + @Autowired + private ImportHelper importHelper + + @Autowired + private WebApplicationContext wac + + private MockMvc mvc + Map auths + + @BeforeEach + void before() { + testHelper.truncateDbs() + + mvc = MockMvcBuilders + .webAppContextSetup(wac) + .apply(springSecurity()) + .build() + + auths = importHelper.createAuthorities(["user": Authority.user, "admin": Authority.admin]) + + importHelper.createUser(new User(firstName: "Test", lastName: "Integration", email: USER_EMAIL, password: USER_PASSWORD, state: UserState.ACTIVE), + [auths.get("user"), auths.get("admin")] as Authority[], [] as ProcessRole[]) + } + + @Test + void dynamicQueryTest() { + AbstractUser realUser = userService.findByEmail(USER_EMAIL, null) + + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(ActorTransformer.toLoggedUser(realUser), ActorTransformer.toLoggedUser(realUser).getPassword(), ActorTransformer.toLoggedUser(realUser).getAuthoritySet() as Set); + SecurityContextHolder.getContext().setAuthentication(token) + + def user = userService.getLoggedUser() + def context = new HashMap<>(Map.of( + ElasticQueryConstants.DYNAMIC_USER_ID_TEMPLATE, user.getId().toString(), + ElasticQueryConstants.LOCAL_DATE_NOW_TEMPLATE, LocalDateTime.now(), + ElasticQueryConstants.LOCAL_DATE_TODAY_TEMPLATE, LocalDate.now(), + ElasticQueryConstants.LOGGED_USER_TEMPLATE, user + )); + + String query1 = '${String.valueOf(java.sql.Timestamp.valueOf(now.plus(java.time.Duration.parse("P1D"))).getTime())}' + String resultQuery1 = String.valueOf(Timestamp.valueOf(LocalDateTime.now() + Duration.parse("P1D")).getTime()) + + assert queryFactory.populateQuery(query1, context) <= resultQuery1; + + String query2 = 'creationDate:${today.toString()}'; + String res2 = LocalDate.now().toString() + String resultQuery2 = "creationDate:" + res2; + + assert queryFactory.populateQuery(query2, context) == resultQuery2; + + String query3 = 'author:${loggedUser.id}' + String resultQuery3 = "author:" + user.id; + + assert queryFactory.populateQuery(query3, context) == resultQuery3; + + String query4 = 'author:${me}'; + String resultQuery4 = "author:" + user.id; + + assert queryFactory.populateQuery(query4, context) == resultQuery4; + } +}