This is an automated email from the ASF dual-hosted git repository.
fjtiradosarti pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-kie-kogito-apps.git
The following commit(s) were added to refs/heads/main by this push:
new 3c78fe9ed [Fix #2158] Adding support for querying workflow variables
(#2161)
3c78fe9ed is described below
commit 3c78fe9ed4b82db2290755abe154760785d20367
Author: Francisco Javier Tirado Sarti
<[email protected]>
AuthorDate: Fri Dec 13 17:41:44 2024 +0100
[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
---
.../java/org/kie/kogito/index/json/JsonUtils.java | 8 ++
.../index/graphql/query/GraphQLQueryMapper.java | 55 +++++++++-
.../src/main/resources/basic.schema.graphqls | 1 +
.../graphql/query/GraphQLQueryMapperTest.java | 106 ++++++++++++++++++++
.../service/graphql/GraphQLSchemaManagerImpl.java | 1 +
.../org/kie/kogito/index/test/QueryTestUtils.java | 5 +
.../java/org/kie/kogito/index/test/TestUtils.java | 16 +++
.../org/kie/kogito/index/jpa/storage/JPAQuery.java | 111 +++++++++++----------
.../jpa/storage/ProcessInstanceEntityStorage.java | 3 +
.../index/postgresql/PostgresqlJsonHelper.java | 96 ++++++++++++++++++
.../index/postgresql/PostgresqlJsonJPAQuery.java | 42 ++++++++
.../PostgresqlProcessInstanceEntityStorage.java | 42 ++++++++
.../query/ProcessInstanceEntityQueryIT.java | 90 +++++++++++++++++
.../persistence/api/query/AttributeFilter.java | 10 ++
.../persistence/api/query/QueryFilterFactory.java | 2 +-
15 files changed, 531 insertions(+), 57 deletions(-)
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 c660fb31c..e8a7a6d30 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 @@ package org.kie.kogito.index.json;
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 @@ public final class JsonUtils {
}
return result;
}
+
+ public static <T> AttributeFilter<T> jsonFilter(AttributeFilter<T> 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 20a333d5b..73c3793ec 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.simplePrint;
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 class GraphQLQueryMapper implements
Function<GraphQLInputObjectType, Grap
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<Object, Stream<AttributeFilter<?>>> mapJsonArgument(String
attribute) {
+ return argument -> ((Map<String, Object>)
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<String, Object> entry = ((Map<String, Object>)
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 0317ec61c..f30e90c7e 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 000000000..1d9e3fdbe
--- /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 af18b2821..2acaf539a 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 class GraphQLSchemaManagerImpl extends
AbstractGraphQLSchemaManager {
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 6477143d2..5e8f305dc 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 class QueryTestUtils {
public static BiConsumer<List<ObjectNode>, String[]>
assertWithObjectNode() {
return (instances, ids) -> assertThat(instances).hasSize(ids == null ?
0 : ids.length).extracting(n ->
n.get("id").asText()).containsExactlyInAnyOrder(ids);
}
+
+ public static <V> BiConsumer<List<V>, 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 59ee7601a..954bbf4a2 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.index.model.UserTaskInstance;
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 class TestUtils {
return event;
}
+ public static ProcessInstanceVariableDataEvent
createProcessInstanceVariableEvent(String processInstance,
+ String processId, String name, int age, boolean isMartian,
List<String> 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 3e1fbcade..4ab1f7d6d 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 @@ import static java.util.stream.Collectors.toList;
public class JPAQuery<K, E extends AbstractEntity, T> implements Query<T> {
- private PanacheRepositoryBase<E, K> repository;
+ protected final PanacheRepositoryBase<E, K> repository;
private Integer limit;
private Integer offset;
private List<AttributeFilter<?>> filters;
private List<AttributeSort> sortBy;
- private Class<E> entityClass;
- private Function<E, T> mapper;
+ protected final Class<E> entityClass;
+ protected final Function<E, T> mapper;
public JPAQuery(PanacheRepositoryBase<E, K> repository, Function<E, T>
mapper, Class<E> entityClass) {
this.repository = repository;
@@ -113,57 +113,60 @@ public class JPAQuery<K, E extends AbstractEntity, T>
implements Query<T> {
return filters.stream().map(filterPredicateFunction(root,
builder)).collect(toList());
}
- private Function<AttributeFilter<?>, Predicate>
filterPredicateFunction(Root<E> root, CriteriaBuilder builder) {
- return filter -> {
- switch (filter.getCondition()) {
- case CONTAINS:
- return builder.isMember(filter.getValue(),
getAttributePath(root, filter.getAttribute()));
- case CONTAINS_ALL:
- List<Predicate> predicatesAll = (List<Predicate>) ((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<Predicate> predicatesAny = (List<Predicate>) ((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<Object> value = (List<Object>) 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<AttributeFilter<?>, Predicate>
filterPredicateFunction(Root<E> root, CriteriaBuilder builder) {
+ return filter -> buildPredicateFunction(filter, root, builder);
+ }
+
+ protected final Predicate buildPredicateFunction(AttributeFilter filter,
Root<E> root, CriteriaBuilder builder) {
+ switch (filter.getCondition()) {
+ case CONTAINS:
+ return builder.isMember(filter.getValue(),
getAttributePath(root, filter.getAttribute()));
+ case CONTAINS_ALL:
+ List<Predicate> predicatesAll = (List<Predicate>) ((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<Predicate> predicatesAny = (List<Predicate>) ((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<Object> value = (List<Object>) 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<E> 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 68e5d2484..17a3c8290 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.MilestoneStatus;
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.event.process.ProcessInstanceNodeEventBody.EVENT_TY
import static org.kie.kogito.index.DateTimeUtils.toZonedDateTime;
@ApplicationScoped
+@DefaultBean
public class ProcessInstanceEntityStorage extends
AbstractJPAStorageFetcher<String, ProcessInstanceEntity, ProcessInstance>
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 000000000..6ca18f356
--- /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<Object> 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<Object>) 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<Object>) 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 000000000..0a3d27cdd
--- /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<K, E extends AbstractEntity, T> extends
JPAQuery<K, E, T> {
+
+ public PostgresqlJsonJPAQuery(PanacheRepositoryBase<E, K> repository,
Function<E, T> mapper, Class<E> entityClass) {
+ super(repository, mapper, entityClass);
+ }
+
+ protected Function<AttributeFilter<?>, Predicate>
filterPredicateFunction(Root<E> 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 000000000..db6a00156
--- /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<ProcessInstance> 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 254bca48f..872ba4890 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 a7427ba8e..2577d78f3 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<T> {
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 class AttributeFilter<T> {
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 a54f58919..ab02f5781 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 final class QueryFilterFactory {
return new AttributeFilter<>(attribute, FilterCondition.LIKE, value);
}
- public static AttributeFilter<String> contains(String attribute, String
value) {
+ public static <T> AttributeFilter<T> contains(String attribute, T value) {
return new AttributeFilter<>(attribute, FilterCondition.CONTAINS,
value);
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]