From e20ed9c74365f80c9c2ec27a936f5600156882b6 Mon Sep 17 00:00:00 2001 From: Toshiya Kobayashi Date: Fri, 13 Dec 2024 14:07:45 +0900 Subject: [PATCH 1/3] [incubator-kie-issues-1682] Test cases in kogito repos GHA fail with "pull access denied for vectorized/redpanda" (#2162) --- .../src/test/resources/application.properties | 20 ++++++++++++++++++ .../src/test/resources/application.properties | 4 +++- .../src/test/resources/application.properties | 4 +++- .../src/test/resources/application.properties | 2 ++ .../src/test/resources/application.properties | 4 +++- .../src/test/resources/application.properties | 21 +++++++++++++++++++ .../src/test/resources/application.properties | 2 ++ kogito-apps-build-parent/pom.xml | 1 + .../src/test/resources/application.properties | 4 +++- 9 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 apps-integration-tests/integration-tests-jobs-service/integration-tests-jobs-service-quarkus/integration-tests-jobs-service-quarkus-embedded/src/test/resources/application.properties create mode 100644 jobs-service/jobs-service-messaging-kafka/src/test/resources/application.properties diff --git a/apps-integration-tests/integration-tests-jobs-service/integration-tests-jobs-service-quarkus/integration-tests-jobs-service-quarkus-embedded/src/test/resources/application.properties b/apps-integration-tests/integration-tests-jobs-service/integration-tests-jobs-service-quarkus/integration-tests-jobs-service-quarkus-embedded/src/test/resources/application.properties new file mode 100644 index 0000000000..d0b6aa7199 --- /dev/null +++ b/apps-integration-tests/integration-tests-jobs-service/integration-tests-jobs-service-quarkus/integration-tests-jobs-service-quarkus-embedded/src/test/resources/application.properties @@ -0,0 +1,20 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +quarkus.kafka.devservices.image-name=${container.image.kafka} \ No newline at end of file diff --git a/data-index/data-index-service/data-index-service-infinispan/src/test/resources/application.properties b/data-index/data-index-service/data-index-service-infinispan/src/test/resources/application.properties index 8f8a8ae113..d59fe076d4 100644 --- a/data-index/data-index-service/data-index-service-infinispan/src/test/resources/application.properties +++ b/data-index/data-index-service/data-index-service-infinispan/src/test/resources/application.properties @@ -53,4 +53,6 @@ kogito.data-index.vertx-graphql.ui.path=/graphiql kogito.data-index.vertx-graphql.ui.tenant=web-app-tenant # Not using Dev service in test, but rather org.kie.kogito.testcontainers.quarkus.KeycloakQuarkusTestResource -quarkus.keycloak.devservices.enabled=false \ No newline at end of file +quarkus.keycloak.devservices.enabled=false + +quarkus.kafka.devservices.image-name=${container.image.kafka} \ No newline at end of file diff --git a/data-index/data-index-service/data-index-service-mongodb/src/test/resources/application.properties b/data-index/data-index-service/data-index-service-mongodb/src/test/resources/application.properties index 68347c230c..8cabb28210 100644 --- a/data-index/data-index-service/data-index-service-mongodb/src/test/resources/application.properties +++ b/data-index/data-index-service/data-index-service-mongodb/src/test/resources/application.properties @@ -61,4 +61,6 @@ kogito.data-index.vertx-graphql.ui.path=/graphiql kogito.data-index.vertx-graphql.ui.tenant=web-app-tenant # Not using Dev service in test, but rather org.kie.kogito.testcontainers.quarkus.KeycloakQuarkusTestResource -quarkus.keycloak.devservices.enabled=false \ No newline at end of file +quarkus.keycloak.devservices.enabled=false + +quarkus.kafka.devservices.image-name=${container.image.kafka} \ No newline at end of file diff --git a/jobs-service/jobs-service-infinispan/src/test/resources/application.properties b/jobs-service/jobs-service-infinispan/src/test/resources/application.properties index 50e3609e11..01374c9c65 100644 --- a/jobs-service/jobs-service-infinispan/src/test/resources/application.properties +++ b/jobs-service/jobs-service-infinispan/src/test/resources/application.properties @@ -36,3 +36,5 @@ quarkus.oidc.tenant-enabled=false %kafka-events-support.mp.messaging.outgoing.kogito-job-service-job-request-events-emitter.connector=smallrye-kafka %kafka-events-support.mp.messaging.outgoing.kogito-job-service-job-request-events-emitter.topic=kogito-job-service-job-request-events %kafka-events-support.mp.messaging.outgoing.kogito-job-service-job-request-events-emitter.value.serializer=org.apache.kafka.common.serialization.StringSerializer + +quarkus.kafka.devservices.image-name=${container.image.kafka} \ No newline at end of file diff --git a/jobs-service/jobs-service-inmemory/src/test/resources/application.properties b/jobs-service/jobs-service-inmemory/src/test/resources/application.properties index 9525c74b31..0d809c3523 100644 --- a/jobs-service/jobs-service-inmemory/src/test/resources/application.properties +++ b/jobs-service/jobs-service-inmemory/src/test/resources/application.properties @@ -24,4 +24,6 @@ quarkus.http.test-port=0 %kafka-events-support.mp.messaging.outgoing.kogito-job-service-job-request-events-emitter.value.serializer=org.apache.kafka.common.serialization.StringSerializer quarkus.datasource.devservices.enabled=false -quarkus.kogito.devservices.enabled=false \ No newline at end of file +quarkus.kogito.devservices.enabled=false + +quarkus.kafka.devservices.image-name=${container.image.kafka} \ No newline at end of file diff --git a/jobs-service/jobs-service-messaging-kafka/src/test/resources/application.properties b/jobs-service/jobs-service-messaging-kafka/src/test/resources/application.properties new file mode 100644 index 0000000000..baef842c36 --- /dev/null +++ b/jobs-service/jobs-service-messaging-kafka/src/test/resources/application.properties @@ -0,0 +1,21 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +quarkus.kafka.devservices.image-name=${container.image.kafka} + diff --git a/jobs-service/jobs-service-mongodb/src/test/resources/application.properties b/jobs-service/jobs-service-mongodb/src/test/resources/application.properties index d5ef305323..f04dbe280f 100644 --- a/jobs-service/jobs-service-mongodb/src/test/resources/application.properties +++ b/jobs-service/jobs-service-mongodb/src/test/resources/application.properties @@ -39,3 +39,5 @@ quarkus.mongodb.database=kogito %kafka-events-support.mp.messaging.outgoing.kogito-job-service-job-request-events-emitter.connector=smallrye-kafka %kafka-events-support.mp.messaging.outgoing.kogito-job-service-job-request-events-emitter.topic=kogito-job-service-job-request-events %kafka-events-support.mp.messaging.outgoing.kogito-job-service-job-request-events-emitter.value.serializer=org.apache.kafka.common.serialization.StringSerializer + +quarkus.kafka.devservices.image-name=${container.image.kafka} \ No newline at end of file diff --git a/kogito-apps-build-parent/pom.xml b/kogito-apps-build-parent/pom.xml index 72c89cabf6..daec939b27 100644 --- a/kogito-apps-build-parent/pom.xml +++ b/kogito-apps-build-parent/pom.xml @@ -127,6 +127,7 @@ org.jboss.logmanager.LogManager + ${container.image.kafka} diff --git a/trusty/trusty-service/trusty-service-common/src/test/resources/application.properties b/trusty/trusty-service/trusty-service-common/src/test/resources/application.properties index b885577f37..e574c42334 100644 --- a/trusty/trusty-service/trusty-service-common/src/test/resources/application.properties +++ b/trusty/trusty-service/trusty-service-common/src/test/resources/application.properties @@ -53,4 +53,6 @@ mp.messaging.incoming.trusty-explainability-result.value.deserializer=org.apache mp.messaging.incoming.trusty-explainability-result.auto.offset.reset=earliest # Not using Dev service in test, but rather org.kie.kogito.testcontainers.quarkus.KeycloakQuarkusTestResource -quarkus.keycloak.devservices.enabled=false \ No newline at end of file +quarkus.keycloak.devservices.enabled=false + +quarkus.kafka.devservices.image-name=${container.image.kafka} \ No newline at end of file From f48b51110c3f1c4759df20fac6745c926cb9e011 Mon Sep 17 00:00:00 2001 From: Francisco Javier Tirado Sarti <65240126+fjtirado@users.noreply.github.com> Date: Fri, 13 Dec 2024 17:41:44 +0100 Subject: [PATCH 2/3] [Fix #2158] Adding support for querying workflow variables (#2161) * [Fix #2158] Adding support for querying workflow variables * [Fix #2158] Adding more filter conditions * [Fix #2158] Gonzalos comment Rolling back unneded pom change * [Fix #2158] Gonzalos comment Rolling back unneded pom change --- .../org/kie/kogito/index/json/JsonUtils.java | 8 ++ .../graphql/query/GraphQLQueryMapper.java | 55 ++++++++- .../src/main/resources/basic.schema.graphqls | 1 + .../graphql/query/GraphQLQueryMapperTest.java | 106 +++++++++++++++++ .../graphql/GraphQLSchemaManagerImpl.java | 1 + .../kie/kogito/index/test/QueryTestUtils.java | 5 + .../org/kie/kogito/index/test/TestUtils.java | 16 +++ .../kogito/index/jpa/storage/JPAQuery.java | 111 +++++++++--------- .../storage/ProcessInstanceEntityStorage.java | 3 + .../postgresql/PostgresqlJsonHelper.java | 96 +++++++++++++++ .../postgresql/PostgresqlJsonJPAQuery.java | 42 +++++++ ...ostgresqlProcessInstanceEntityStorage.java | 42 +++++++ .../query/ProcessInstanceEntityQueryIT.java | 90 ++++++++++++++ .../api/query/AttributeFilter.java | 10 ++ .../api/query/QueryFilterFactory.java | 2 +- 15 files changed, 531 insertions(+), 57 deletions(-) create mode 100644 data-index/data-index-graphql/src/test/java/org/kie/kogito/index/graphql/query/GraphQLQueryMapperTest.java create mode 100644 data-index/data-index-storage/data-index-storage-postgresql/src/main/java/org/kie/kogito/index/postgresql/PostgresqlJsonHelper.java create mode 100644 data-index/data-index-storage/data-index-storage-postgresql/src/main/java/org/kie/kogito/index/postgresql/PostgresqlJsonJPAQuery.java create mode 100644 data-index/data-index-storage/data-index-storage-postgresql/src/main/java/org/kie/kogito/index/postgresql/PostgresqlProcessInstanceEntityStorage.java diff --git a/data-index/data-index-common/src/main/java/org/kie/kogito/index/json/JsonUtils.java b/data-index/data-index-common/src/main/java/org/kie/kogito/index/json/JsonUtils.java index c660fb31ce..e8a7a6d30f 100644 --- a/data-index/data-index-common/src/main/java/org/kie/kogito/index/json/JsonUtils.java +++ b/data-index/data-index-common/src/main/java/org/kie/kogito/index/json/JsonUtils.java @@ -21,6 +21,7 @@ import org.kie.kogito.jackson.utils.JsonObjectUtils; import org.kie.kogito.jackson.utils.MergeUtils; import org.kie.kogito.jackson.utils.ObjectMapperFactory; +import org.kie.kogito.persistence.api.query.AttributeFilter; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; @@ -58,4 +59,11 @@ private static ObjectNode createObjectNode(String variableName, Object variableV } return result; } + + public static AttributeFilter jsonFilter(AttributeFilter filter) { + if (filter != null) { + filter.setJson(true); + } + return filter; + } } diff --git a/data-index/data-index-graphql/src/main/java/org/kie/kogito/index/graphql/query/GraphQLQueryMapper.java b/data-index/data-index-graphql/src/main/java/org/kie/kogito/index/graphql/query/GraphQLQueryMapper.java index 20a333d5b0..73c3793ec3 100644 --- a/data-index/data-index-graphql/src/main/java/org/kie/kogito/index/graphql/query/GraphQLQueryMapper.java +++ b/data-index/data-index-graphql/src/main/java/org/kie/kogito/index/graphql/query/GraphQLQueryMapper.java @@ -40,6 +40,7 @@ import static graphql.schema.GraphQLTypeUtil.unwrapNonNull; import static graphql.schema.GraphQLTypeUtil.unwrapOne; import static java.util.stream.Collectors.toList; +import static org.kie.kogito.index.json.JsonUtils.jsonFilter; import static org.kie.kogito.persistence.api.query.FilterCondition.NOT; import static org.kie.kogito.persistence.api.query.QueryFilterFactory.and; import static org.kie.kogito.persistence.api.query.QueryFilterFactory.between; @@ -110,15 +111,65 @@ public GraphQLQueryParser apply(GraphQLInputObjectType type) { case "KogitoMetadataArgument": parser.mapAttribute(field.getName(), mapSubEntityArgument(field.getName(), GraphQLQueryParserRegistry.get().getParser("KogitoMetadataArgument"))); break; + case "JSON": + parser.mapAttribute(field.getName(), mapJsonArgument(field.getName())); + break; default: - parser.mapAttribute(field.getName(), mapSubEntityArgument(field.getName(), new GraphQLQueryMapper().apply((GraphQLInputObjectType) field.getType()))); + if (field.getType() instanceof GraphQLInputObjectType) { + parser.mapAttribute(field.getName(), mapSubEntityArgument(field.getName(), new GraphQLQueryMapper().apply((GraphQLInputObjectType) field.getType()))); + } } } }); - return parser; } + Function>> mapJsonArgument(String attribute) { + return argument -> ((Map) argument).entrySet().stream().map(e -> mapJsonArgument(attribute, e.getKey(), e.getValue())); + } + + private AttributeFilter mapJsonArgument(String attribute, String key, Object value) { + StringBuilder sb = new StringBuilder(attribute); + FilterCondition condition = FilterCondition.fromLabel(key); + while (condition == null && value instanceof Map) { + sb.append('.').append(key); + Map.Entry entry = ((Map) value).entrySet().iterator().next(); + key = entry.getKey(); + value = entry.getValue(); + condition = FilterCondition.fromLabel(key); + } + if (condition != null) { + switch (condition) { + case GT: + return jsonFilter(greaterThan(sb.toString(), value)); + case GTE: + return jsonFilter(greaterThanEqual(sb.toString(), value)); + case LT: + return jsonFilter(lessThan(sb.toString(), value)); + case LTE: + return jsonFilter(lessThanEqual(sb.toString(), value)); + case BETWEEN: + return jsonFilter(filterValueMap(value, val -> between(sb.toString(), val.get("from"), val.get("to")))); + case IN: + return jsonFilter(filterValueList(value, val -> in(sb.toString(), val))); + case IS_NULL: + return jsonFilter(Boolean.TRUE.equals(value) ? isNull(sb.toString()) : notNull(sb.toString())); + case CONTAINS: + return jsonFilter(contains(sb.toString(), value)); + case LIKE: + return jsonFilter(like(sb.toString(), value.toString())); + case CONTAINS_ALL: + return filterValueList(value, val -> containsAll(sb.toString(), val)); + case CONTAINS_ANY: + return filterValueList(value, val -> containsAny(sb.toString(), val)); + case EQUAL: + default: + return jsonFilter(equalTo(sb.toString(), value)); + } + } + return null; + } + private boolean isListOfType(GraphQLInputType source, String type) { if (isList(source)) { return ((GraphQLNamedType) unwrapNonNull(unwrapOne(source))).getName().equals(type); diff --git a/data-index/data-index-graphql/src/main/resources/basic.schema.graphqls b/data-index/data-index-graphql/src/main/resources/basic.schema.graphqls index 0317ec61c5..f30e90c7ed 100644 --- a/data-index/data-index-graphql/src/main/resources/basic.schema.graphqls +++ b/data-index/data-index-graphql/src/main/resources/basic.schema.graphqls @@ -177,6 +177,7 @@ input ProcessInstanceArgument { id: IdArgument processId: StringArgument processName: StringArgument + variables: JSON parentProcessInstanceId: IdArgument rootProcessInstanceId: IdArgument rootProcessId: StringArgument diff --git a/data-index/data-index-graphql/src/test/java/org/kie/kogito/index/graphql/query/GraphQLQueryMapperTest.java b/data-index/data-index-graphql/src/test/java/org/kie/kogito/index/graphql/query/GraphQLQueryMapperTest.java new file mode 100644 index 0000000000..1d9e3fdbee --- /dev/null +++ b/data-index/data-index-graphql/src/test/java/org/kie/kogito/index/graphql/query/GraphQLQueryMapperTest.java @@ -0,0 +1,106 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.kogito.index.graphql.query; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.kie.kogito.index.json.JsonUtils.jsonFilter; +import static org.kie.kogito.persistence.api.query.QueryFilterFactory.*; + +public class GraphQLQueryMapperTest { + + private GraphQLQueryMapper mapper; + + @BeforeEach + void setup() { + mapper = new GraphQLQueryMapper(); + } + + @Test + void testJsonMapperEqual() { + assertThat(mapper.mapJsonArgument("variables").apply(Map.of("workflowdata", Map.of("number", Map.of("equal", 1))))).containsExactly( + jsonFilter(equalTo("variables.workflowdata.number", 1))); + } + + @Test + void testJsonMapperGreater() { + assertThat(mapper.mapJsonArgument("variables").apply(Map.of("workflowdata", Map.of("number", Map.of("greaterThan", 1))))).containsExactly( + jsonFilter(greaterThan("variables.workflowdata.number", 1))); + } + + @Test + void testJsonMapperLess() { + assertThat(mapper.mapJsonArgument("variables").apply(Map.of("workflowdata", Map.of("number", Map.of("lessThan", 1))))).containsExactly( + jsonFilter(lessThan("variables.workflowdata.number", 1))); + } + + @Test + void testJsonMapperGreaterEqual() { + assertThat(mapper.mapJsonArgument("variables").apply(Map.of("workflowdata", Map.of("number", Map.of("greaterThanEqual", 1))))).containsExactly( + jsonFilter(greaterThanEqual("variables.workflowdata.number", 1))); + } + + @Test + void testJsonMapperLessEqual() { + assertThat(mapper.mapJsonArgument("variables").apply(Map.of("workflowdata", Map.of("number", Map.of("lessThanEqual", 1))))).containsExactly( + jsonFilter(lessThanEqual("variables.workflowdata.number", 1))); + } + + @Test + void testJsonMapperBetween() { + assertThat(mapper.mapJsonArgument("variables").apply(Map.of("workflowdata", Map.of("number", Map.of("between", Map.of("from", 1, "to", 3)))))).containsExactly( + jsonFilter(between("variables.workflowdata.number", 1, 3))); + } + + @Test + void testJsonMapperIn() { + assertThat(mapper.mapJsonArgument("variables").apply(Map.of("workflowdata", Map.of("number", Map.of("in", List.of(1, 3)))))).containsExactly( + jsonFilter(in("variables.workflowdata.number", Arrays.asList(1, 3)))); + } + + @Test + void testJsonMapperContains() { + assertThat(mapper.mapJsonArgument("variables").apply(Map.of("workflowdata", Map.of("number", Map.of("contains", 1))))).containsExactly( + jsonFilter(contains("variables.workflowdata.number", 1))); + } + + @Test + void testJsonMapperLike() { + assertThat(mapper.mapJsonArgument("variables").apply(Map.of("workflowdata", Map.of("number", Map.of("like", "kk"))))).containsExactly( + jsonFilter(like("variables.workflowdata.number", "kk"))); + } + + @Test + void testJsonMapperNull() { + assertThat(mapper.mapJsonArgument("variables").apply(Map.of("workflowdata", Map.of("number", Map.of("isNull", true))))).containsExactly( + jsonFilter(isNull("variables.workflowdata.number"))); + } + + @Test + void testJsonMapperNotNull() { + assertThat(mapper.mapJsonArgument("variables").apply(Map.of("workflowdata", Map.of("number", Map.of("isNull", false))))).containsExactly( + jsonFilter(notNull("variables.workflowdata.number"))); + } +} diff --git a/data-index/data-index-service/data-index-service-common/src/main/java/org/kie/kogito/index/service/graphql/GraphQLSchemaManagerImpl.java b/data-index/data-index-service/data-index-service-common/src/main/java/org/kie/kogito/index/service/graphql/GraphQLSchemaManagerImpl.java index af18b2821c..2acaf539ab 100644 --- a/data-index/data-index-service/data-index-service-common/src/main/java/org/kie/kogito/index/service/graphql/GraphQLSchemaManagerImpl.java +++ b/data-index/data-index-service/data-index-service-common/src/main/java/org/kie/kogito/index/service/graphql/GraphQLSchemaManagerImpl.java @@ -76,6 +76,7 @@ public GraphQLSchema createSchema() { typeDefinitionRegistry.merge(loadSchemaDefinitionFile("domain.schema.graphqls")); RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring() + .scalar(ExtendedScalars.Json) .type("Query", builder -> { builder.dataFetcher("ProcessDefinitions", this::getProcessDefinitionsValues); builder.dataFetcher("ProcessInstances", this::getProcessInstancesValues); diff --git a/data-index/data-index-storage/data-index-storage-api/src/test/java/org/kie/kogito/index/test/QueryTestUtils.java b/data-index/data-index-storage/data-index-storage-api/src/test/java/org/kie/kogito/index/test/QueryTestUtils.java index 6477143d26..5e8f305dc8 100644 --- a/data-index/data-index-storage/data-index-storage-api/src/test/java/org/kie/kogito/index/test/QueryTestUtils.java +++ b/data-index/data-index-storage/data-index-storage-api/src/test/java/org/kie/kogito/index/test/QueryTestUtils.java @@ -50,4 +50,9 @@ public static BiConsumer, String[]> assertWithObjectNodeInOrder public static BiConsumer, String[]> assertWithObjectNode() { return (instances, ids) -> assertThat(instances).hasSize(ids == null ? 0 : ids.length).extracting(n -> n.get("id").asText()).containsExactlyInAnyOrder(ids); } + + public static BiConsumer, String[]> assertNotId() { + return (instances, ids) -> assertThat(instances).extracting("id").doesNotContainAnyElementsOf(List.of(ids)); + } + } diff --git a/data-index/data-index-storage/data-index-storage-api/src/test/java/org/kie/kogito/index/test/TestUtils.java b/data-index/data-index-storage/data-index-storage-api/src/test/java/org/kie/kogito/index/test/TestUtils.java index 59ee7601a0..954bbf4a24 100644 --- a/data-index/data-index-storage/data-index-storage-api/src/test/java/org/kie/kogito/index/test/TestUtils.java +++ b/data-index/data-index-storage/data-index-storage-api/src/test/java/org/kie/kogito/index/test/TestUtils.java @@ -42,7 +42,9 @@ import org.kie.kogito.jackson.utils.ObjectMapperFactory; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; import static java.util.Collections.singleton; import static java.util.Collections.singletonList; @@ -83,6 +85,20 @@ public static ProcessInstanceVariableDataEvent createProcessInstanceVariableEven return event; } + public static ProcessInstanceVariableDataEvent createProcessInstanceVariableEvent(String processInstance, + String processId, String name, int age, boolean isMartian, List aliases) { + ProcessInstanceVariableDataEvent event = new ProcessInstanceVariableDataEvent(); + event.setKogitoProcessId(processId); + event.setKogitoProcessInstanceId(processInstance); + ArrayNode node = ObjectMapperFactory.get().createArrayNode(); + aliases.forEach(s -> node.add(new TextNode(s))); + event.setData(ProcessInstanceVariableEventBody.create().processId(processId).processInstanceId(processInstance) + .variableName("traveller").variableValue(ObjectMapperFactory.get().createObjectNode().put("name", name).put("age", age).put("isMartian", isMartian) + .set("aliases", node)) + .build()); + return event; + } + public static ProcessInstanceNodeDataEvent createProcessInstanceNodeDataEvent(String processInstance, String processId, String nodeDefinitionId, String nodeInstanceId, String nodeName, String nodeType, int eventType) { diff --git a/data-index/data-index-storage/data-index-storage-jpa-common/src/main/java/org/kie/kogito/index/jpa/storage/JPAQuery.java b/data-index/data-index-storage/data-index-storage-jpa-common/src/main/java/org/kie/kogito/index/jpa/storage/JPAQuery.java index 3e1fbcade6..4ab1f7d6db 100644 --- a/data-index/data-index-storage/data-index-storage-jpa-common/src/main/java/org/kie/kogito/index/jpa/storage/JPAQuery.java +++ b/data-index/data-index-storage/data-index-storage-jpa-common/src/main/java/org/kie/kogito/index/jpa/storage/JPAQuery.java @@ -43,13 +43,13 @@ public class JPAQuery implements Query { - private PanacheRepositoryBase repository; + protected final PanacheRepositoryBase repository; private Integer limit; private Integer offset; private List> filters; private List sortBy; - private Class entityClass; - private Function mapper; + protected final Class entityClass; + protected final Function mapper; public JPAQuery(PanacheRepositoryBase repository, Function mapper, Class entityClass) { this.repository = repository; @@ -113,57 +113,60 @@ protected List getPredicates(CriteriaBuilder builder, Root root) { return filters.stream().map(filterPredicateFunction(root, builder)).collect(toList()); } - private Function, Predicate> filterPredicateFunction(Root root, CriteriaBuilder builder) { - return filter -> { - switch (filter.getCondition()) { - case CONTAINS: - return builder.isMember(filter.getValue(), getAttributePath(root, filter.getAttribute())); - case CONTAINS_ALL: - List predicatesAll = (List) ((List) filter.getValue()).stream() - .map(o -> builder.isMember(o, getAttributePath(root, filter.getAttribute()))).collect(toList()); - return builder.and(predicatesAll.toArray(new Predicate[] {})); - case CONTAINS_ANY: - List predicatesAny = (List) ((List) filter.getValue()).stream() - .map(o -> builder.isMember(o, getAttributePath(root, filter.getAttribute()))).collect(toList()); - return builder.or(predicatesAny.toArray(new Predicate[] {})); - case IN: - return getAttributePath(root, filter.getAttribute()).in((Collection) filter.getValue()); - case LIKE: - return builder.like(getAttributePath(root, filter.getAttribute()), - filter.getValue().toString().replaceAll("\\*", "%")); - case EQUAL: - return builder.equal(getAttributePath(root, filter.getAttribute()), filter.getValue()); - case IS_NULL: - Path pathNull = getAttributePath(root, filter.getAttribute()); - return isPluralAttribute(filter.getAttribute()) ? builder.isEmpty(pathNull) : builder.isNull(pathNull); - case NOT_NULL: - Path pathNotNull = getAttributePath(root, filter.getAttribute()); - return isPluralAttribute(filter.getAttribute()) ? builder.isNotEmpty(pathNotNull) : builder.isNotNull(pathNotNull); - case BETWEEN: - List value = (List) filter.getValue(); - return builder - .between(getAttributePath(root, filter.getAttribute()), (Comparable) value.get(0), - (Comparable) value.get(1)); - case GT: - return builder.greaterThan(getAttributePath(root, filter.getAttribute()), (Comparable) filter.getValue()); - case GTE: - return builder.greaterThanOrEqualTo(getAttributePath(root, filter.getAttribute()), - (Comparable) filter.getValue()); - case LT: - return builder.lessThan(getAttributePath(root, filter.getAttribute()), (Comparable) filter.getValue()); - case LTE: - return builder - .lessThanOrEqualTo(getAttributePath(root, filter.getAttribute()), (Comparable) filter.getValue()); - case OR: - return builder.or(getRecursivePredicate(filter, root, builder).toArray(new Predicate[] {})); - case AND: - return builder.and(getRecursivePredicate(filter, root, builder).toArray(new Predicate[] {})); - case NOT: - return builder.not(filterPredicateFunction(root, builder).apply((AttributeFilter) filter.getValue())); - default: - return null; - } - }; + protected Function, Predicate> filterPredicateFunction(Root root, CriteriaBuilder builder) { + return filter -> buildPredicateFunction(filter, root, builder); + } + + protected final Predicate buildPredicateFunction(AttributeFilter filter, Root root, CriteriaBuilder builder) { + switch (filter.getCondition()) { + case CONTAINS: + return builder.isMember(filter.getValue(), getAttributePath(root, filter.getAttribute())); + case CONTAINS_ALL: + List predicatesAll = (List) ((List) filter.getValue()).stream() + .map(o -> builder.isMember(o, getAttributePath(root, filter.getAttribute()))).collect(toList()); + return builder.and(predicatesAll.toArray(new Predicate[] {})); + case CONTAINS_ANY: + List predicatesAny = (List) ((List) filter.getValue()).stream() + .map(o -> builder.isMember(o, getAttributePath(root, filter.getAttribute()))).collect(toList()); + return builder.or(predicatesAny.toArray(new Predicate[] {})); + case IN: + return getAttributePath(root, filter.getAttribute()).in((Collection) filter.getValue()); + case LIKE: + return builder.like(getAttributePath(root, filter.getAttribute()), + filter.getValue().toString().replaceAll("\\*", "%")); + case EQUAL: + return builder.equal(getAttributePath(root, filter.getAttribute()), filter.getValue()); + case IS_NULL: + Path pathNull = getAttributePath(root, filter.getAttribute()); + return isPluralAttribute(filter.getAttribute()) ? builder.isEmpty(pathNull) : builder.isNull(pathNull); + case NOT_NULL: + Path pathNotNull = getAttributePath(root, filter.getAttribute()); + return isPluralAttribute(filter.getAttribute()) ? builder.isNotEmpty(pathNotNull) : builder.isNotNull(pathNotNull); + case BETWEEN: + List value = (List) filter.getValue(); + return builder + .between(getAttributePath(root, filter.getAttribute()), (Comparable) value.get(0), + (Comparable) value.get(1)); + case GT: + return builder.greaterThan(getAttributePath(root, filter.getAttribute()), (Comparable) filter.getValue()); + case GTE: + return builder.greaterThanOrEqualTo(getAttributePath(root, filter.getAttribute()), + (Comparable) filter.getValue()); + case LT: + return builder.lessThan(getAttributePath(root, filter.getAttribute()), (Comparable) filter.getValue()); + case LTE: + return builder + .lessThanOrEqualTo(getAttributePath(root, filter.getAttribute()), (Comparable) filter.getValue()); + case OR: + return builder.or(getRecursivePredicate(filter, root, builder).toArray(new Predicate[] {})); + case AND: + return builder.and(getRecursivePredicate(filter, root, builder).toArray(new Predicate[] {})); + case NOT: + return builder.not(filterPredicateFunction(root, builder).apply((AttributeFilter) filter.getValue())); + default: + return null; + } + } private Path getAttributePath(Root root, String attribute) { diff --git a/data-index/data-index-storage/data-index-storage-jpa-common/src/main/java/org/kie/kogito/index/jpa/storage/ProcessInstanceEntityStorage.java b/data-index/data-index-storage/data-index-storage-jpa-common/src/main/java/org/kie/kogito/index/jpa/storage/ProcessInstanceEntityStorage.java index 68e5d24844..17a3c82904 100644 --- a/data-index/data-index-storage/data-index-storage-jpa-common/src/main/java/org/kie/kogito/index/jpa/storage/ProcessInstanceEntityStorage.java +++ b/data-index/data-index-storage/data-index-storage-jpa-common/src/main/java/org/kie/kogito/index/jpa/storage/ProcessInstanceEntityStorage.java @@ -48,6 +48,8 @@ import org.kie.kogito.index.model.ProcessInstance; import org.kie.kogito.index.storage.ProcessInstanceStorage; +import io.quarkus.arc.DefaultBean; + import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.transaction.Transactional; @@ -57,6 +59,7 @@ import static org.kie.kogito.index.DateTimeUtils.toZonedDateTime; @ApplicationScoped +@DefaultBean public class ProcessInstanceEntityStorage extends AbstractJPAStorageFetcher implements ProcessInstanceStorage { protected ProcessInstanceEntityStorage() { diff --git a/data-index/data-index-storage/data-index-storage-postgresql/src/main/java/org/kie/kogito/index/postgresql/PostgresqlJsonHelper.java b/data-index/data-index-storage/data-index-storage-postgresql/src/main/java/org/kie/kogito/index/postgresql/PostgresqlJsonHelper.java new file mode 100644 index 0000000000..6ca18f3567 --- /dev/null +++ b/data-index/data-index-storage/data-index-storage-postgresql/src/main/java/org/kie/kogito/index/postgresql/PostgresqlJsonHelper.java @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.kogito.index.postgresql; + +import java.util.List; +import java.util.stream.Collectors; + +import org.kie.kogito.persistence.api.query.AttributeFilter; + +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.Expression; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; + +import static java.util.stream.Collectors.toList; + +public class PostgresqlJsonHelper { + + private PostgresqlJsonHelper() { + } + + public static Predicate buildPredicate(AttributeFilter filter, Root root, + CriteriaBuilder builder) { + boolean isString; + List values; + switch (filter.getCondition()) { + case EQUAL: + isString = filter.getValue() instanceof String; + return builder.equal(buildPathExpression(builder, root, filter.getAttribute(), isString), buildObjectExpression(builder, filter.getValue(), isString)); + case GT: + isString = filter.getValue() instanceof String; + return builder.greaterThan(buildPathExpression(builder, root, filter.getAttribute(), isString), buildObjectExpression(builder, filter.getValue(), isString)); + case GTE: + isString = filter.getValue() instanceof String; + return builder.greaterThanOrEqualTo(buildPathExpression(builder, root, filter.getAttribute(), isString), buildObjectExpression(builder, filter.getValue(), isString)); + case LT: + isString = filter.getValue() instanceof String; + return builder.lessThan(buildPathExpression(builder, root, filter.getAttribute(), isString), buildObjectExpression(builder, filter.getValue(), isString)); + case LTE: + isString = filter.getValue() instanceof String; + return builder + .lessThanOrEqualTo(buildPathExpression(builder, root, filter.getAttribute(), isString), buildObjectExpression(builder, filter.getValue(), isString)); + case LIKE: + return builder.like(buildPathExpression(builder, root, filter.getAttribute(), true), + filter.getValue().toString().replaceAll("\\*", "%")); + case IS_NULL: + return builder.isNull(buildPathExpression(builder, root, filter.getAttribute(), false)); + case NOT_NULL: + return builder.isNotNull(buildPathExpression(builder, root, filter.getAttribute(), false)); + case BETWEEN: + values = (List) filter.getValue(); + isString = values.get(0) instanceof String; + return builder.between(buildPathExpression(builder, root, filter.getAttribute(), isString), buildObjectExpression(builder, values.get(0), isString), + buildObjectExpression(builder, values.get(1), isString)); + case IN: + values = (List) filter.getValue(); + isString = values.get(0) instanceof String; + return buildPathExpression(builder, root, filter.getAttribute(), isString).in(values.stream().map(o -> buildObjectExpression(builder, o, isString)).collect(Collectors.toList())); + } + throw new UnsupportedOperationException("Filter " + filter + " is not supported"); + } + + private static Expression buildObjectExpression(CriteriaBuilder builder, Object value, boolean isString) { + return isString ? builder.literal(value) : builder.function("to_jsonb", Object.class, builder.literal(value)); + } + + private static Expression buildObjectExpression(CriteriaBuilder builder, Object value) { + return buildObjectExpression(builder, value, value instanceof String); + } + + private static Expression buildPathExpression(CriteriaBuilder builder, Root root, String attributeName, boolean isStr) { + String[] attributes = attributeName.split("\\."); + Expression[] arguments = new Expression[attributes.length]; + arguments[0] = root.get(attributes[0]); + for (int i = 1; i < attributes.length; i++) { + arguments[i] = builder.literal(attributes[i]); + } + return isStr ? builder.function("jsonb_extract_path_text", String.class, arguments) : builder.function("jsonb_extract_path", Object.class, arguments); + } +} diff --git a/data-index/data-index-storage/data-index-storage-postgresql/src/main/java/org/kie/kogito/index/postgresql/PostgresqlJsonJPAQuery.java b/data-index/data-index-storage/data-index-storage-postgresql/src/main/java/org/kie/kogito/index/postgresql/PostgresqlJsonJPAQuery.java new file mode 100644 index 0000000000..0a3d27cddb --- /dev/null +++ b/data-index/data-index-storage/data-index-storage-postgresql/src/main/java/org/kie/kogito/index/postgresql/PostgresqlJsonJPAQuery.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.kogito.index.postgresql; + +import java.util.function.Function; + +import org.kie.kogito.index.jpa.model.AbstractEntity; +import org.kie.kogito.index.jpa.storage.JPAQuery; +import org.kie.kogito.persistence.api.query.AttributeFilter; + +import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase; + +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; + +public class PostgresqlJsonJPAQuery extends JPAQuery { + + public PostgresqlJsonJPAQuery(PanacheRepositoryBase repository, Function mapper, Class entityClass) { + super(repository, mapper, entityClass); + } + + protected Function, Predicate> filterPredicateFunction(Root root, CriteriaBuilder builder) { + return filter -> filter.isJson() ? PostgresqlJsonHelper.buildPredicate(filter, root, builder) : buildPredicateFunction(filter, root, builder); + } +} diff --git a/data-index/data-index-storage/data-index-storage-postgresql/src/main/java/org/kie/kogito/index/postgresql/PostgresqlProcessInstanceEntityStorage.java b/data-index/data-index-storage/data-index-storage-postgresql/src/main/java/org/kie/kogito/index/postgresql/PostgresqlProcessInstanceEntityStorage.java new file mode 100644 index 0000000000..db6a001568 --- /dev/null +++ b/data-index/data-index-storage/data-index-storage-postgresql/src/main/java/org/kie/kogito/index/postgresql/PostgresqlProcessInstanceEntityStorage.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.kogito.index.postgresql; + +import org.kie.kogito.index.jpa.mapper.ProcessInstanceEntityMapper; +import org.kie.kogito.index.jpa.model.ProcessInstanceEntityRepository; +import org.kie.kogito.index.jpa.storage.ProcessInstanceEntityStorage; +import org.kie.kogito.index.model.ProcessInstance; +import org.kie.kogito.persistence.api.query.Query; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +@ApplicationScoped +public class PostgresqlProcessInstanceEntityStorage extends ProcessInstanceEntityStorage { + + @Inject + public PostgresqlProcessInstanceEntityStorage(ProcessInstanceEntityRepository repository, ProcessInstanceEntityMapper mapper) { + super(repository, mapper); + } + + @Override + public Query query() { + return new PostgresqlJsonJPAQuery<>(repository, mapToModel, entityClass); + } +} diff --git a/data-index/data-index-storage/data-index-storage-postgresql/src/test/java/org/kie/kogito/index/postgresql/query/ProcessInstanceEntityQueryIT.java b/data-index/data-index-storage/data-index-storage-postgresql/src/test/java/org/kie/kogito/index/postgresql/query/ProcessInstanceEntityQueryIT.java index 254bca48f2..872ba48906 100644 --- a/data-index/data-index-storage/data-index-storage-postgresql/src/test/java/org/kie/kogito/index/postgresql/query/ProcessInstanceEntityQueryIT.java +++ b/data-index/data-index-storage/data-index-storage-postgresql/src/test/java/org/kie/kogito/index/postgresql/query/ProcessInstanceEntityQueryIT.java @@ -18,13 +18,103 @@ */ package org.kie.kogito.index.postgresql.query; +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.Test; +import org.kie.kogito.event.process.ProcessInstanceVariableDataEvent; import org.kie.kogito.index.jpa.query.AbstractProcessInstanceEntityQueryIT; +import org.kie.kogito.index.storage.ProcessInstanceStorage; +import org.kie.kogito.index.test.TestUtils; import org.kie.kogito.testcontainers.quarkus.PostgreSqlQuarkusTestResource; import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.junit.QuarkusTest; +import static java.util.Collections.singletonList; +import static org.kie.kogito.index.json.JsonUtils.jsonFilter; +import static org.kie.kogito.index.test.QueryTestUtils.assertNotId; +import static org.kie.kogito.index.test.QueryTestUtils.assertWithId; +import static org.kie.kogito.persistence.api.query.QueryFilterFactory.*; + @QuarkusTest @QuarkusTestResource(PostgreSqlQuarkusTestResource.class) class ProcessInstanceEntityQueryIT extends AbstractProcessInstanceEntityQueryIT { + + @Test + void testProcessInstanceVariables() { + String processId = "travels"; + String processInstanceId = UUID.randomUUID().toString(); + ProcessInstanceStorage storage = getStorage(); + ProcessInstanceVariableDataEvent variableEvent = TestUtils.createProcessInstanceVariableEvent(processInstanceId, processId, "John", 28, false, + List.of("Super", "Astonishing", "TheRealThing")); + storage.indexVariable(variableEvent); + queryAndAssert(assertWithId(), storage, singletonList(jsonFilter(equalTo("variables.traveller.name", "John"))), null, null, null, + processInstanceId); + queryAndAssert(assertNotId(), storage, singletonList(jsonFilter(equalTo("variables.traveller.name", "Smith"))), null, null, null, + processInstanceId); + queryAndAssert(assertWithId(), storage, singletonList(jsonFilter(equalTo("variables.traveller.isMartian", false))), null, null, null, + processInstanceId); + queryAndAssert(assertNotId(), storage, singletonList(jsonFilter(equalTo("variables.traveller.isMartian", true))), null, null, null, + processInstanceId); + queryAndAssert(assertWithId(), storage, singletonList(jsonFilter(equalTo("variables.traveller.age", 28))), null, null, null, + processInstanceId); + queryAndAssert(assertNotId(), storage, singletonList(jsonFilter(equalTo("variables.traveller.age", 29))), null, null, null, + processInstanceId); + queryAndAssert(assertWithId(), storage, singletonList(jsonFilter(between("variables.traveller.age", 26, 30))), null, null, null, + processInstanceId); + queryAndAssert(assertNotId(), storage, singletonList(jsonFilter(between("variables.traveller.age", 1, 3))), null, null, null, + processInstanceId); + queryAndAssert(assertWithId(), storage, singletonList(jsonFilter(between("variables.traveller.age", 26, 30))), null, null, null, + processInstanceId); + queryAndAssert(assertNotId(), storage, singletonList(jsonFilter(between("variables.traveller.age", 1, 3))), null, null, null, + processInstanceId); + queryAndAssert(assertWithId(), storage, singletonList(jsonFilter(greaterThan("variables.traveller.age", 26))), null, null, null, + processInstanceId); + queryAndAssert(assertNotId(), storage, singletonList(jsonFilter(greaterThan("variables.traveller.age", 28))), null, null, null, + processInstanceId); + queryAndAssert(assertWithId(), storage, singletonList(jsonFilter(greaterThanEqual("variables.traveller.age", 28))), null, null, null, + processInstanceId); + queryAndAssert(assertNotId(), storage, singletonList(jsonFilter(greaterThanEqual("variables.traveller.age", 29))), null, null, null, + processInstanceId); + queryAndAssert(assertWithId(), storage, singletonList(jsonFilter(lessThan("variables.traveller.age", 29))), null, null, null, + processInstanceId); + queryAndAssert(assertNotId(), storage, singletonList(jsonFilter(lessThan("variables.traveller.age", 28))), null, null, null, + processInstanceId); + queryAndAssert(assertWithId(), storage, singletonList(jsonFilter(lessThanEqual("variables.traveller.age", 28))), null, null, null, + processInstanceId); + queryAndAssert(assertNotId(), storage, singletonList(jsonFilter(lessThanEqual("variables.traveller.age", 27))), null, null, null, + processInstanceId); + queryAndAssert(assertWithId(), storage, singletonList(jsonFilter(lessThanEqual("variables.traveller.age", 28))), null, null, null, + processInstanceId); + queryAndAssert(assertNotId(), storage, singletonList(jsonFilter(lessThanEqual("variables.traveller.age", 27))), null, null, null, + processInstanceId); + queryAndAssert(assertWithId(), storage, singletonList(jsonFilter(in("variables.traveller.name", List.of("John", "Smith")))), null, null, null, + processInstanceId); + queryAndAssert(assertNotId(), storage, singletonList(jsonFilter(in("variables.traveller.age", List.of("Jack", "Smith")))), null, null, null, + processInstanceId); + queryAndAssert(assertWithId(), storage, singletonList(jsonFilter(in("variables.traveller.age", List.of(28, 29)))), null, null, null, + processInstanceId); + queryAndAssert(assertNotId(), storage, singletonList(jsonFilter(in("variables.traveller.age", List.of(27, 29)))), null, null, null, + processInstanceId); + queryAndAssert(assertWithId(), storage, singletonList(jsonFilter(like("variables.traveller.name", "Joh*"))), null, null, null, + processInstanceId); + queryAndAssert(assertNotId(), storage, singletonList(jsonFilter(like("variables.traveller.name", "Joha*"))), null, null, null, + processInstanceId); + queryAndAssert(assertWithId(), storage, singletonList(jsonFilter(notNull("variables.traveller.aliases"))), null, null, null, + processInstanceId); + queryAndAssert(assertNotId(), storage, singletonList(jsonFilter(isNull("variables.traveller.aliases"))), null, null, null, + processInstanceId); + queryAndAssert(assertWithId(), storage, singletonList(not(jsonFilter(isNull("variables.traveller.aliases")))), null, null, null, + processInstanceId); + queryAndAssert(assertWithId(), storage, singletonList(and(List.of(jsonFilter(notNull("variables.traveller.aliases")), jsonFilter(lessThan("variables.traveller.age", 45))))), null, null, null, + processInstanceId); + queryAndAssert(assertWithId(), storage, singletonList(or(List.of(jsonFilter(notNull("variables.traveller.aliases")), jsonFilter(lessThan("variables.traveller.age", 22))))), null, null, null, + processInstanceId); + // TODO add support for json contains (requires writing dialect extension on hibernate) + //queryAndAssert(assertWithId(), storage, singletonList(jsonFilter(contains("variables.traveller.aliases", "TheRealThing"))), null, null, null, + // processInstanceId); + //queryAndAssert(assertEmpty(), storage, singletonList(jsonFilter(contains("variables.traveller.aliases", "TheDummyThing"))), null, null, null, + // processInstanceId); + } } diff --git a/persistence-commons/persistence-commons-api/src/main/java/org/kie/kogito/persistence/api/query/AttributeFilter.java b/persistence-commons/persistence-commons-api/src/main/java/org/kie/kogito/persistence/api/query/AttributeFilter.java index a7427ba8eb..2577d78f3c 100644 --- a/persistence-commons/persistence-commons-api/src/main/java/org/kie/kogito/persistence/api/query/AttributeFilter.java +++ b/persistence-commons/persistence-commons-api/src/main/java/org/kie/kogito/persistence/api/query/AttributeFilter.java @@ -26,6 +26,8 @@ public class AttributeFilter { private T value; + private transient boolean jsonFilter; + protected AttributeFilter(String attribute, FilterCondition condition, T value) { this.attribute = attribute; this.condition = condition; @@ -56,6 +58,14 @@ public void setValue(T value) { this.value = value; } + public void setJson(boolean jsonFilter) { + this.jsonFilter = jsonFilter; + } + + public boolean isJson() { + return jsonFilter; + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/persistence-commons/persistence-commons-api/src/main/java/org/kie/kogito/persistence/api/query/QueryFilterFactory.java b/persistence-commons/persistence-commons-api/src/main/java/org/kie/kogito/persistence/api/query/QueryFilterFactory.java index a54f58919a..ab02f57810 100644 --- a/persistence-commons/persistence-commons-api/src/main/java/org/kie/kogito/persistence/api/query/QueryFilterFactory.java +++ b/persistence-commons/persistence-commons-api/src/main/java/org/kie/kogito/persistence/api/query/QueryFilterFactory.java @@ -34,7 +34,7 @@ public static AttributeFilter like(String attribute, String value) { return new AttributeFilter<>(attribute, FilterCondition.LIKE, value); } - public static AttributeFilter contains(String attribute, String value) { + public static AttributeFilter contains(String attribute, T value) { return new AttributeFilter<>(attribute, FilterCondition.CONTAINS, value); } From ca6a92969488403076b3c0ce5ae7b767b2eb7a7b Mon Sep 17 00:00:00 2001 From: Francisco Javier Tirado Sarti <65240126+fjtirado@users.noreply.github.com> Date: Tue, 17 Dec 2024 11:53:38 +0100 Subject: [PATCH 3/3] [Fix #2163] Add contains, containsAny and contaisAll support for variables field (#2164) * [Fix #2163] Add contains support * [Fix #2163] ContainsAny&ConstainsAll --- .../graphql/query/GraphQLQueryMapper.java | 4 +- .../graphql/query/GraphQLQueryMapperTest.java | 12 +++ .../index/postgresql/ContainsSQLFunction.java | 76 +++++++++++++++++++ .../CustomFunctionsContributor.java | 36 +++++++++ .../postgresql/PostgresqlJsonHelper.java | 15 ++++ ...g.hibernate.boot.model.FunctionContributor | 19 +++++ .../query/ProcessInstanceEntityQueryIT.java | 17 +++-- .../api/query/QueryFilterFactory.java | 4 +- 8 files changed, 174 insertions(+), 9 deletions(-) create mode 100644 data-index/data-index-storage/data-index-storage-postgresql/src/main/java/org/kie/kogito/index/postgresql/ContainsSQLFunction.java create mode 100644 data-index/data-index-storage/data-index-storage-postgresql/src/main/java/org/kie/kogito/index/postgresql/CustomFunctionsContributor.java create mode 100644 data-index/data-index-storage/data-index-storage-postgresql/src/main/resources/META-INF/services/org.hibernate.boot.model.FunctionContributor diff --git a/data-index/data-index-graphql/src/main/java/org/kie/kogito/index/graphql/query/GraphQLQueryMapper.java b/data-index/data-index-graphql/src/main/java/org/kie/kogito/index/graphql/query/GraphQLQueryMapper.java index 73c3793ec3..1fb357ffa0 100644 --- a/data-index/data-index-graphql/src/main/java/org/kie/kogito/index/graphql/query/GraphQLQueryMapper.java +++ b/data-index/data-index-graphql/src/main/java/org/kie/kogito/index/graphql/query/GraphQLQueryMapper.java @@ -159,9 +159,9 @@ private AttributeFilter mapJsonArgument(String attribute, String key, Object case LIKE: return jsonFilter(like(sb.toString(), value.toString())); case CONTAINS_ALL: - return filterValueList(value, val -> containsAll(sb.toString(), val)); + return jsonFilter(filterValueList(value, val -> containsAll(sb.toString(), val))); case CONTAINS_ANY: - return filterValueList(value, val -> containsAny(sb.toString(), val)); + return jsonFilter(filterValueList(value, val -> containsAny(sb.toString(), val))); case EQUAL: default: return jsonFilter(equalTo(sb.toString(), value)); diff --git a/data-index/data-index-graphql/src/test/java/org/kie/kogito/index/graphql/query/GraphQLQueryMapperTest.java b/data-index/data-index-graphql/src/test/java/org/kie/kogito/index/graphql/query/GraphQLQueryMapperTest.java index 1d9e3fdbee..27f053f9a3 100644 --- a/data-index/data-index-graphql/src/test/java/org/kie/kogito/index/graphql/query/GraphQLQueryMapperTest.java +++ b/data-index/data-index-graphql/src/test/java/org/kie/kogito/index/graphql/query/GraphQLQueryMapperTest.java @@ -86,6 +86,18 @@ void testJsonMapperContains() { jsonFilter(contains("variables.workflowdata.number", 1))); } + @Test + void testJsonMapperContainsAny() { + assertThat(mapper.mapJsonArgument("variables").apply(Map.of("workflowdata", Map.of("number", Map.of("containsAny", List.of(1, 2, 3)))))).containsExactly( + jsonFilter(containsAny("variables.workflowdata.number", List.of(1, 2, 3)))); + } + + @Test + void testJsonMapperContainsAll() { + assertThat(mapper.mapJsonArgument("variables").apply(Map.of("workflowdata", Map.of("number", Map.of("containsAll", List.of(1, 2, 3)))))).containsExactly( + jsonFilter(containsAll("variables.workflowdata.number", List.of(1, 2, 3)))); + } + @Test void testJsonMapperLike() { assertThat(mapper.mapJsonArgument("variables").apply(Map.of("workflowdata", Map.of("number", Map.of("like", "kk"))))).containsExactly( diff --git a/data-index/data-index-storage/data-index-storage-postgresql/src/main/java/org/kie/kogito/index/postgresql/ContainsSQLFunction.java b/data-index/data-index-storage/data-index-storage-postgresql/src/main/java/org/kie/kogito/index/postgresql/ContainsSQLFunction.java new file mode 100644 index 0000000000..ef0563b936 --- /dev/null +++ b/data-index/data-index-storage/data-index-storage-postgresql/src/main/java/org/kie/kogito/index/postgresql/ContainsSQLFunction.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.kogito.index.postgresql; + +import java.util.Iterator; +import java.util.List; + +import org.hibernate.dialect.function.StandardSQLFunction; +import org.hibernate.query.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.type.BasicTypeReference; +import org.hibernate.type.SqlTypes; + +public class ContainsSQLFunction extends StandardSQLFunction { + + static final String CONTAINS_NAME = "contains"; + static final String CONTAINS_ALL_NAME = "containsAll"; + static final String CONTAINS_ANY_NAME = "containsAny"; + + static final String CONTAINS_SEQ = "??"; + static final String CONTAINS_ALL_SEQ = "??&"; + static final String CONTAINS_ANY_SEQ = "??|"; + + private final String operator; + + private static final BasicTypeReference RETURN_TYPE = new BasicTypeReference<>("boolean", Boolean.class, SqlTypes.BOOLEAN); + + ContainsSQLFunction(String name, String operator) { + super(name, RETURN_TYPE); + this.operator = operator; + } + + @Override + public void render( + SqlAppender sqlAppender, + List args, + ReturnableType returnType, + SqlAstTranslator translator) { + int size = args.size(); + if (size < 2) { + throw new IllegalArgumentException("Function " + getName() + " requires at least two arguments"); + } + Iterator iter = args.iterator(); + iter.next().accept(translator); + sqlAppender.append(' '); + sqlAppender.append(operator); + sqlAppender.append(' '); + if (size == 2) { + iter.next().accept(translator); + } else { + sqlAppender.append("array["); + do { + iter.next().accept(translator); + sqlAppender.append(iter.hasNext() ? ',' : ']'); + } while (iter.hasNext()); + } + } +} diff --git a/data-index/data-index-storage/data-index-storage-postgresql/src/main/java/org/kie/kogito/index/postgresql/CustomFunctionsContributor.java b/data-index/data-index-storage/data-index-storage-postgresql/src/main/java/org/kie/kogito/index/postgresql/CustomFunctionsContributor.java new file mode 100644 index 0000000000..cb8a68c9e1 --- /dev/null +++ b/data-index/data-index-storage/data-index-storage-postgresql/src/main/java/org/kie/kogito/index/postgresql/CustomFunctionsContributor.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.kogito.index.postgresql; + +import org.hibernate.boot.model.FunctionContributions; +import org.hibernate.boot.model.FunctionContributor; +import org.hibernate.query.sqm.function.SqmFunctionRegistry; + +import static org.kie.kogito.index.postgresql.ContainsSQLFunction.*; + +public class CustomFunctionsContributor implements FunctionContributor { + + @Override + public void contributeFunctions(FunctionContributions functionContributions) { + SqmFunctionRegistry registry = functionContributions.getFunctionRegistry(); + registry.register(CONTAINS_NAME, new ContainsSQLFunction(CONTAINS_NAME, CONTAINS_SEQ)); + registry.register(CONTAINS_ANY_NAME, new ContainsSQLFunction(CONTAINS_ANY_NAME, CONTAINS_ANY_SEQ)); + registry.register(CONTAINS_ALL_NAME, new ContainsSQLFunction(CONTAINS_ALL_NAME, CONTAINS_ALL_SEQ)); + } +} diff --git a/data-index/data-index-storage/data-index-storage-postgresql/src/main/java/org/kie/kogito/index/postgresql/PostgresqlJsonHelper.java b/data-index/data-index-storage/data-index-storage-postgresql/src/main/java/org/kie/kogito/index/postgresql/PostgresqlJsonHelper.java index 6ca18f3567..3cb1f347d8 100644 --- a/data-index/data-index-storage/data-index-storage-postgresql/src/main/java/org/kie/kogito/index/postgresql/PostgresqlJsonHelper.java +++ b/data-index/data-index-storage/data-index-storage-postgresql/src/main/java/org/kie/kogito/index/postgresql/PostgresqlJsonHelper.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.kie.kogito.persistence.api.query.AttributeFilter; @@ -72,10 +73,24 @@ public static Predicate buildPredicate(AttributeFilter filter, Root root, values = (List) filter.getValue(); isString = values.get(0) instanceof String; return buildPathExpression(builder, root, filter.getAttribute(), isString).in(values.stream().map(o -> buildObjectExpression(builder, o, isString)).collect(Collectors.toList())); + case CONTAINS: + return builder.isTrue( + builder.function(ContainsSQLFunction.CONTAINS_NAME, Boolean.class, buildPathExpression(builder, root, filter.getAttribute(), false), builder.literal(filter.getValue()))); + case CONTAINS_ANY: + return containsPredicate(filter, root, builder, ContainsSQLFunction.CONTAINS_ANY_NAME); + case CONTAINS_ALL: + return containsPredicate(filter, root, builder, ContainsSQLFunction.CONTAINS_ALL_NAME); } throw new UnsupportedOperationException("Filter " + filter + " is not supported"); } + private static Predicate containsPredicate(AttributeFilter filter, Root root, CriteriaBuilder builder, String name) { + return builder.isTrue( + builder.function(name, Boolean.class, + Stream.concat(Stream.of(buildPathExpression(builder, root, filter.getAttribute(), false)), ((List) filter.getValue()).stream().map(o -> builder.literal(o))) + .toArray(Expression[]::new))); + } + private static Expression buildObjectExpression(CriteriaBuilder builder, Object value, boolean isString) { return isString ? builder.literal(value) : builder.function("to_jsonb", Object.class, builder.literal(value)); } diff --git a/data-index/data-index-storage/data-index-storage-postgresql/src/main/resources/META-INF/services/org.hibernate.boot.model.FunctionContributor b/data-index/data-index-storage/data-index-storage-postgresql/src/main/resources/META-INF/services/org.hibernate.boot.model.FunctionContributor new file mode 100644 index 0000000000..c3c15eeb39 --- /dev/null +++ b/data-index/data-index-storage/data-index-storage-postgresql/src/main/resources/META-INF/services/org.hibernate.boot.model.FunctionContributor @@ -0,0 +1,19 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# +org.kie.kogito.index.postgresql.CustomFunctionsContributor \ No newline at end of file diff --git a/data-index/data-index-storage/data-index-storage-postgresql/src/test/java/org/kie/kogito/index/postgresql/query/ProcessInstanceEntityQueryIT.java b/data-index/data-index-storage/data-index-storage-postgresql/src/test/java/org/kie/kogito/index/postgresql/query/ProcessInstanceEntityQueryIT.java index 872ba48906..9eb7611e74 100644 --- a/data-index/data-index-storage/data-index-storage-postgresql/src/test/java/org/kie/kogito/index/postgresql/query/ProcessInstanceEntityQueryIT.java +++ b/data-index/data-index-storage/data-index-storage-postgresql/src/test/java/org/kie/kogito/index/postgresql/query/ProcessInstanceEntityQueryIT.java @@ -111,10 +111,17 @@ void testProcessInstanceVariables() { processInstanceId); queryAndAssert(assertWithId(), storage, singletonList(or(List.of(jsonFilter(notNull("variables.traveller.aliases")), jsonFilter(lessThan("variables.traveller.age", 22))))), null, null, null, processInstanceId); - // TODO add support for json contains (requires writing dialect extension on hibernate) - //queryAndAssert(assertWithId(), storage, singletonList(jsonFilter(contains("variables.traveller.aliases", "TheRealThing"))), null, null, null, - // processInstanceId); - //queryAndAssert(assertEmpty(), storage, singletonList(jsonFilter(contains("variables.traveller.aliases", "TheDummyThing"))), null, null, null, - // processInstanceId); + queryAndAssert(assertWithId(), storage, singletonList(jsonFilter(contains("variables.traveller.aliases", "TheRealThing"))), null, null, null, + processInstanceId); + queryAndAssert(assertNotId(), storage, singletonList(jsonFilter(contains("variables.traveller.aliases", "TheDummyThing"))), null, null, null, + processInstanceId); + queryAndAssert(assertWithId(), storage, singletonList(jsonFilter(containsAny("variables.traveller.aliases", List.of("TheRealThing", "TheDummyThing")))), null, null, null, + processInstanceId); + queryAndAssert(assertNotId(), storage, singletonList(jsonFilter(containsAny("variables.traveller.aliases", List.of("TheRedPandaThing", "TheDummyThing")))), null, null, null, + processInstanceId); + queryAndAssert(assertWithId(), storage, singletonList(jsonFilter(containsAll("variables.traveller.aliases", List.of("Super", "Astonishing", "TheRealThing")))), null, null, null, + processInstanceId); + queryAndAssert(assertNotId(), storage, singletonList(jsonFilter(containsAll("variables.traveller.aliases", List.of("Super", "TheDummyThing")))), null, null, null, + processInstanceId); } } diff --git a/persistence-commons/persistence-commons-api/src/main/java/org/kie/kogito/persistence/api/query/QueryFilterFactory.java b/persistence-commons/persistence-commons-api/src/main/java/org/kie/kogito/persistence/api/query/QueryFilterFactory.java index ab02f57810..1017556895 100644 --- a/persistence-commons/persistence-commons-api/src/main/java/org/kie/kogito/persistence/api/query/QueryFilterFactory.java +++ b/persistence-commons/persistence-commons-api/src/main/java/org/kie/kogito/persistence/api/query/QueryFilterFactory.java @@ -42,11 +42,11 @@ public static AttributeFilter> in(String attribute, List values) return new AttributeFilter<>(attribute, FilterCondition.IN, values); } - public static AttributeFilter> containsAny(String attribute, List values) { + public static AttributeFilter> containsAny(String attribute, List values) { return new AttributeFilter<>(attribute, FilterCondition.CONTAINS_ANY, values); } - public static AttributeFilter> containsAll(String attribute, List values) { + public static AttributeFilter> containsAll(String attribute, List values) { return new AttributeFilter<>(attribute, FilterCondition.CONTAINS_ALL, values); }