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]


Reply via email to