Skip to content

Commit b64a594

Browse files
fekynkoMartin Fekete
andauthored
allow to customize json search function (#188)
* allow to customize json search function * undo imports changes * move json query functions configuration to QuerySupport * updated QuerySupport.toString * JsonbExtractor * JsonbExtractor -> JsonbConfiguration * JsonbConfigurationSupport record * JsonbConfigurationSupport -> JsonbConfiguration --------- Co-authored-by: Martin Fekete <[email protected]>
1 parent 7f1ebb2 commit b64a594

File tree

8 files changed

+117
-36
lines changed

8 files changed

+117
-36
lines changed

rsql-jpa/src/main/java/io/github/perplexhub/rsql/QuerySupport.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.github.perplexhub.rsql;
22

3+
import io.github.perplexhub.rsql.jsonb.JsonbConfiguration;
34
import lombok.Builder;
45
import lombok.Data;
56

@@ -27,12 +28,14 @@ public class QuerySupport {
2728
private Map<Class<?>, List<String>> propertyBlacklist;
2829
private Collection<String> procedureWhiteList;
2930
private Collection<String> procedureBlackList;
31+
@Builder.Default
32+
private JsonbConfiguration jsonbConfiguration = JsonbConfiguration.DEFAULT;
3033

3134
public static class QuerySupportBuilder {}
3235

3336
@Override
3437
public String toString() {
35-
return String.format("%s,distinct:%b,propertyPathMapper:%s,customPredicates:%d,joinHints:%s,propertyWhitelist:%s,propertyBlacklist:%s",
36-
rsqlQuery, distinct, propertyPathMapper, customPredicates == null ? 0 : customPredicates.size(), joinHints, propertyWhitelist, propertyBlacklist);
38+
return String.format("%s,distinct:%b,propertyPathMapper:%s,customPredicates:%d,joinHints:%s,propertyWhitelist:%s,propertyBlacklist:%s,jsonbConfiguration:%s",
39+
rsqlQuery, distinct, propertyPathMapper, customPredicates == null ? 0 : customPredicates.size(), joinHints, propertyWhitelist, propertyBlacklist, jsonbConfiguration);
3740
}
3841
}

rsql-jpa/src/main/java/io/github/perplexhub/rsql/RSQLJPAPredicateConverter.java

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import java.util.function.Function;
99
import java.util.stream.Collectors;
1010

11+
import io.github.perplexhub.rsql.jsonb.JsonbConfiguration;
1112
import io.github.perplexhub.rsql.jsonb.JsonbSupport;
1213
import jakarta.persistence.criteria.*;
1314
import jakarta.persistence.metamodel.Attribute;
@@ -37,6 +38,7 @@ public class RSQLJPAPredicateConverter extends RSQLVisitorBase<Predicate, From>
3738
private final Collection<String> procedureBlackList;
3839
private final boolean strictEquality;
3940
private final Character likeEscapeCharacter;
41+
private final JsonbConfiguration jsonbConfiguration;
4042

4143
public RSQLJPAPredicateConverter(CriteriaBuilder builder, Map<String, String> propertyPathMapper) {
4244
this(builder, propertyPathMapper, null, null);
@@ -54,14 +56,26 @@ public RSQLJPAPredicateConverter(CriteriaBuilder builder, Map<String, String> pr
5456
this(builder, propertyPathMapper, customPredicates, joinHints, procedureWhiteList, procedureBlackList, false, null);
5557
}
5658

59+
public RSQLJPAPredicateConverter(CriteriaBuilder builder,
60+
Map<String, String> propertyPathMapper,
61+
List<RSQLCustomPredicate<?>> customPredicates,
62+
Map<String, JoinType> joinHints,
63+
Collection<String> proceduresWhiteList,
64+
Collection<String> proceduresBlackList,
65+
boolean strictEquality,
66+
Character likeEscapeCharacter) {
67+
this(builder, propertyPathMapper, customPredicates, joinHints, proceduresWhiteList, proceduresBlackList, strictEquality, likeEscapeCharacter, JsonbConfiguration.DEFAULT);
68+
}
69+
5770
public RSQLJPAPredicateConverter(CriteriaBuilder builder,
58-
Map<String, String> propertyPathMapper,
59-
List<RSQLCustomPredicate<?>> customPredicates,
60-
Map<String, JoinType> joinHints,
61-
Collection<String> proceduresWhiteList,
62-
Collection<String> proceduresBlackList,
63-
boolean strictEquality,
64-
Character likeEscapeCharacter) {
71+
Map<String, String> propertyPathMapper,
72+
List<RSQLCustomPredicate<?>> customPredicates,
73+
Map<String, JoinType> joinHints,
74+
Collection<String> proceduresWhiteList,
75+
Collection<String> proceduresBlackList,
76+
boolean strictEquality,
77+
Character likeEscapeCharacter,
78+
JsonbConfiguration jsonbConfiguration) {
6579
this.builder = builder;
6680
this.propertyPathMapper = propertyPathMapper != null ? propertyPathMapper : Collections.emptyMap();
6781
this.customPredicates = customPredicates != null ? customPredicates.stream().collect(Collectors.toMap(RSQLCustomPredicate::getOperator, Function.identity(), (a, b) -> a)) : Collections.emptyMap();
@@ -70,6 +84,7 @@ public RSQLJPAPredicateConverter(CriteriaBuilder builder,
7084
this.procedureBlackList = proceduresBlackList != null ? proceduresBlackList : Collections.emptyList();
7185
this.strictEquality = strictEquality;
7286
this.likeEscapeCharacter = likeEscapeCharacter;
87+
this.jsonbConfiguration = jsonbConfiguration;
7388
}
7489

7590
RSQLJPAContext findPropertyPath(String propertyPath, Path startRoot) {
@@ -244,7 +259,7 @@ private ResolvedExpression resolveExpression(ComparisonNode node, From root, Sel
244259
String jsonbPath = JsonbSupport.jsonPathOfSelector(attribute, jsonSelector);
245260
if(jsonbPath.contains(".")) {
246261
ComparisonNode jsonbNode = node.withSelector(jsonbPath);
247-
return JsonbSupport.jsonbPathExistsExpression(builder, jsonbNode, path);
262+
return JsonbSupport.jsonbPathExistsExpression(builder, jsonbNode, path, jsonbConfiguration);
248263
} else {
249264
final Expression expression;
250265
if (path instanceof JpaExpression jpaExpression) {

rsql-jpa/src/main/java/io/github/perplexhub/rsql/RSQLJPASupport.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,8 @@ public static <T> Specification<T> toSpecification(final QuerySupport querySuppo
127127
RSQLJPAPredicateConverter visitor = new RSQLJPAPredicateConverter(cb, querySupport.getPropertyPathMapper(),
128128
querySupport.getCustomPredicates(), querySupport.getJoinHints(),
129129
querySupport.getProcedureWhiteList(), querySupport.getProcedureBlackList(),
130-
querySupport.isStrictEquality(), querySupport.getLikeEscapeCharacter());
130+
querySupport.isStrictEquality(), querySupport.getLikeEscapeCharacter(),
131+
querySupport.getJsonbConfiguration());
131132

132133
visitor.setPropertyWhitelist(querySupport.getPropertyWhitelist());
133134
visitor.setPropertyBlacklist(querySupport.getPropertyBlacklist());
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package io.github.perplexhub.rsql.jsonb;
2+
3+
import lombok.Builder;
4+
5+
/**
6+
* convenient way to define configuration, based on default values
7+
*
8+
* @param pathExists Postgresql {@code jsonb_path_exists} function to use
9+
* @param pathExistsTz Postgresql {@code jsonb_path_exists_tz} function to use
10+
* @param useDateTime enable temporal values support
11+
*/
12+
@Builder
13+
public record JsonbConfiguration(String pathExists, String pathExistsTz, boolean useDateTime) {
14+
15+
public static final JsonbConfiguration DEFAULT = JsonbConfiguration.builder().build();
16+
17+
public static class JsonbConfigurationBuilder {
18+
JsonbConfigurationBuilder() {
19+
pathExists = "jsonb_path_exists";
20+
pathExistsTz = "jsonb_path_exists_tz";
21+
useDateTime = false;
22+
}
23+
}
24+
25+
@Override
26+
public String toString() {
27+
return String.format("pathExists:%s,pathExistsTz:%s,useDateTime:%b", pathExists, pathExistsTz, useDateTime);
28+
}
29+
}

rsql-jpa/src/main/java/io/github/perplexhub/rsql/jsonb/JsonbExpressionBuilder.java

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
public class JsonbExpressionBuilder {
1717

18+
private final JsonbConfiguration configuration;
19+
1820
/**
1921
* The base json type.
2022
*/
@@ -161,15 +163,15 @@ public ArgValue convert(String s) {
161163
Map.entry(BETWEEN, "(%1$s >= %2$s && %1$s <= %3$s)")
162164
);
163165

164-
private static final String JSONB_PATH_EXISTS = "jsonb_path_exists";
165-
166-
private static final String JSONB_PATH_EXISTS_TZ = "jsonb_path_exists_tz";
167-
168166
private final ComparisonOperator operator;
169167
private final String keyPath;
170168
private final List<ArgValue> values;
171169

172170
JsonbExpressionBuilder(ComparisonOperator operator, String keyPath, List<String> args) {
171+
this(operator, keyPath, args, JsonbConfiguration.DEFAULT);
172+
}
173+
174+
JsonbExpressionBuilder(ComparisonOperator operator, String keyPath, List<String> args, JsonbConfiguration configuration) {
173175
this.operator = Objects.requireNonNull(operator);
174176
this.keyPath = Objects.requireNonNull(keyPath);
175177
if(FORBIDDEN_NEGATION.contains(operator)) {
@@ -188,6 +190,7 @@ public ArgValue convert(String s) {
188190
if(REQUIRE_AT_LEAST_ONE_ARGUMENT.contains(operator) && candidateValues.isEmpty()) {
189191
throw new IllegalArgumentException("Operator " + operator + " requires at least one value");
190192
}
193+
this.configuration = configuration;
191194
this.values = findMoreTypes(operator, candidateValues);
192195
}
193196

@@ -210,7 +213,7 @@ public JsonbPathExpression getJsonPathExpression() {
210213
List<String> templateArguments = new ArrayList<>();
211214
templateArguments.add(valueReference);
212215
templateArguments.addAll(valuesToCompare);
213-
var function = isDateTimeTz ? JSONB_PATH_EXISTS_TZ : JSONB_PATH_EXISTS;
216+
var function = isDateTimeTz ? configuration.pathExistsTz() : configuration.pathExists();
214217
var expression = String.format("%s ? %s", targetPath, String.format(comparisonTemplate, templateArguments.toArray()));
215218
return new JsonbPathExpression(function, expression);
216219
}
@@ -234,7 +237,7 @@ private List<ArgValue> findMoreTypes(ComparisonOperator operator, List<String> v
234237
return values.stream().map(s -> new ArgValue(s, BaseJsonType.STRING)).toList();
235238
}
236239

237-
List<ArgConverter> argConverters = DATE_TIME_SUPPORT ?
240+
List<ArgConverter> argConverters = configuration.useDateTime() ?
238241
List.of(DATE_TIME_CONVERTER, DATE_TIME_CONVERTER_TZ, NUMBER_CONVERTER, BOOLEAN_ARG_CONVERTER)
239242
: List.of(NUMBER_CONVERTER, BOOLEAN_ARG_CONVERTER);
240243
Optional<ArgConverter> candidateConverter = argConverters.stream()

rsql-jpa/src/main/java/io/github/perplexhub/rsql/jsonb/JsonbSupport.java

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,7 @@
1616
import io.github.perplexhub.rsql.ResolvedExpression;
1717
import jakarta.persistence.Column;
1818
import jakarta.persistence.criteria.CriteriaBuilder;
19-
import jakarta.persistence.criteria.Expression;
2019
import jakarta.persistence.criteria.Path;
21-
import jakarta.persistence.criteria.Predicate;
2220
import jakarta.persistence.metamodel.Attribute;
2321
import jakarta.persistence.metamodel.ManagedType;
2422
import org.springframework.orm.jpa.vendor.Database;
@@ -28,8 +26,6 @@
2826
*/
2927
public class JsonbSupport {
3028

31-
public static boolean DATE_TIME_SUPPORT = false;
32-
3329
private static final Set<Database> JSON_SUPPORT = EnumSet.of(Database.POSTGRESQL);
3430

3531
private static final Map<ComparisonOperator, ComparisonOperator> NEGATE_OPERATORS =
@@ -63,9 +59,9 @@ record JsonbPathExpression(String jsonbFunction, String jsonbPath) {
6359
}
6460

6561

66-
public static ResolvedExpression jsonbPathExistsExpression(CriteriaBuilder builder, ComparisonNode node, Path<?> attrPath) {
62+
public static ResolvedExpression jsonbPathExistsExpression(CriteriaBuilder builder, ComparisonNode node, Path<?> attrPath, JsonbConfiguration configuration) {
6763
var mayBeInvertedOperator = Optional.ofNullable(NEGATE_OPERATORS.get(node.getOperator()));
68-
var jsb = new JsonbExpressionBuilder(mayBeInvertedOperator.orElse(node.getOperator()), node.getSelector(), node.getArguments());
64+
var jsb = new JsonbExpressionBuilder(mayBeInvertedOperator.orElse(node.getOperator()), node.getSelector(), node.getArguments(), configuration);
6965
var expression = jsb.getJsonPathExpression();
7066
return ResolvedExpression.ofJson(builder.function(expression.jsonbFunction, Boolean.class, attrPath,
7167
builder.literal(expression.jsonbPath)), mayBeInvertedOperator.isPresent());

rsql-jpa/src/test/java/io/github/perplexhub/rsql/RSQLJPASupportPostgresJsonTest.java

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package io.github.perplexhub.rsql;
22

3-
import io.github.perplexhub.rsql.jsonb.JsonbSupport;
3+
import io.github.perplexhub.rsql.jsonb.JsonbConfiguration;
44
import io.github.perplexhub.rsql.model.EntityWithJsonb;
55
import io.github.perplexhub.rsql.model.JsonbEntity;
66
import io.github.perplexhub.rsql.model.PostgresJsonEntity;
@@ -17,6 +17,7 @@
1717
import org.springframework.boot.test.context.SpringBootTest;
1818
import org.springframework.orm.jpa.vendor.Database;
1919
import org.springframework.test.context.ActiveProfiles;
20+
import org.springframework.test.context.jdbc.Sql;
2021

2122
import java.util.Collection;
2223
import java.util.HashMap;
@@ -47,7 +48,6 @@ class RSQLJPASupportPostgresJsonTest {
4748
void setup(@Autowired EntityManager em) {
4849
RSQLVisitorBase.setEntityManagerDatabase(Map.of(em, Database.POSTGRESQL));
4950
clear();
50-
JsonbSupport.DATE_TIME_SUPPORT = false;
5151
}
5252

5353
@AfterEach
@@ -83,12 +83,11 @@ void testJsonSearch(List<PostgresJsonEntity> entities, String rsql, List<Postgre
8383
@ParameterizedTest
8484
@MethodSource("temporalData")
8585
void testJsonSearchOfTemporal(List<PostgresJsonEntity> entities, String rsql, List<PostgresJsonEntity> expected) {
86-
JsonbSupport.DATE_TIME_SUPPORT = true;
8786
//given
8887
repository.saveAllAndFlush(entities);
8988

9089
//when
91-
List<PostgresJsonEntity> result = repository.findAll(toSpecification(rsql));
90+
List<PostgresJsonEntity> result = repository.findAll(toSpecification(QuerySupport.builder().rsqlQuery(rsql).jsonbConfiguration(JsonbConfiguration.builder().useDateTime(true).build()).build()));
9291

9392
//then
9493
assertThat(result)
@@ -161,7 +160,6 @@ void testJsonSearchOnMappedRelation(List<JsonbEntity> jsonbEntities, String rsql
161160
@ParameterizedTest
162161
@MethodSource("sortByRelation")
163162
void testJsonSortOnRelation(List<JsonbEntity> jsonbEntities, String rsql, List<JsonbEntity> expected) {
164-
JsonbSupport.DATE_TIME_SUPPORT = true;
165163
//given
166164
Collection<EntityWithJsonb> entitiesWithJsonb = jsonbEntityRepository.saveAllAndFlush(jsonbEntities).stream()
167165
.map(jsonbEntity -> EntityWithJsonb.builder().jsonb(jsonbEntity).build())
@@ -183,7 +181,6 @@ void testJsonSortOnRelation(List<JsonbEntity> jsonbEntities, String rsql, List<J
183181
@ParameterizedTest
184182
@MethodSource("sortByMappedRelation")
185183
void testJsonSortOnMappedRelation(List<JsonbEntity> jsonbEntities, String rsql, List<JsonbEntity> expected) {
186-
JsonbSupport.DATE_TIME_SUPPORT = true;
187184
//given
188185
Collection<EntityWithJsonb> entitiesWithJsonb = jsonbEntityRepository.saveAllAndFlush(jsonbEntities).stream()
189186
.map(jsonbEntity -> EntityWithJsonb.builder().jsonb(jsonbEntity).build())
@@ -542,9 +539,9 @@ private static Stream<Arguments> meltedTimeZone() {
542539
var e3 = new PostgresJsonEntity(Map.of("a", "2020-01-01T00:00:00"));
543540
var allCases = List.of(e1, e2, e3);
544541
return Stream.of(
545-
arguments(allCases, "properties.a=ge=1970-01-02T00:00:00+00:00", List.of(e2, e3)),
546-
arguments(allCases, "properties.a=ge=1970-01-02T00:00:00+01:00", List.of(e2, e3)),
547-
arguments(allCases, "properties.a=lt=2022-01-01T00:00:00+01:00", List.of(e1, e2, e3)),
542+
arguments(allCases, "properties.a=ge=1970-01-02T00:00:00+00:00", List.of(e2, e3)),
543+
arguments(allCases, "properties.a=ge=1970-01-02T00:00:00+01:00", List.of(e2, e3)),
544+
arguments(allCases, "properties.a=lt=2022-01-01T00:00:00+01:00", List.of(e1, e2, e3)),
548545
null
549546
).filter(Objects::nonNull);
550547
}
@@ -721,4 +718,23 @@ private static Map<String, Object> nullMap(String key) {
721718
nullValue.put(key, null);
722719
return nullValue;
723720
}
721+
722+
@Sql(statements = "CREATE OR REPLACE FUNCTION my_jsonb_path_exists(arg1 jsonb,arg2 jsonpath) RETURNS boolean AS 'SELECT $1 @? $2' LANGUAGE 'sql' IMMUTABLE;")
723+
@Sql(statements = "DROP FUNCTION IF EXISTS my_jsonb_path_exists;", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
724+
@ParameterizedTest
725+
@MethodSource("data")
726+
void testJsonSearchCustomFunction(List<PostgresJsonEntity> entities, String rsql, List<PostgresJsonEntity> expected) {
727+
//given
728+
repository.saveAllAndFlush(entities);
729+
730+
//when
731+
List<PostgresJsonEntity> result = repository.findAll(toSpecification(QuerySupport.builder().rsqlQuery(rsql).jsonbConfiguration(JsonbConfiguration.builder().pathExists("my_jsonb_path_exists").build()).build()));
732+
733+
//then
734+
assertThat(result)
735+
.hasSameSizeAs(expected)
736+
.containsExactlyInAnyOrderElementsOf(expected);
737+
738+
entities.forEach(e -> e.setId(null));
739+
}
724740
}

rsql-jpa/src/test/java/io/github/perplexhub/rsql/jsonb/JsonbExpressionBuilderTest.java

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@ class JsonbExpressionBuilderTest {
2020
@ParameterizedTest
2121
@MethodSource("data")
2222
void testJsonbPathExpression(ComparisonOperator operator, String keyPath, List<String> arguments, String expectedJsonbFunction, String expectedJsonbPath) {
23-
JsonbSupport.DATE_TIME_SUPPORT = false;
24-
JsonbExpressionBuilder builder = new JsonbExpressionBuilder(operator, keyPath, arguments);
23+
JsonbExpressionBuilder builder = new JsonbExpressionBuilder(operator, keyPath, arguments, JsonbConfiguration.DEFAULT);
2524
var expression = builder.getJsonPathExpression();
2625
assertEquals(expectedJsonbFunction, expression.jsonbFunction());
2726
assertEquals(expectedJsonbPath, expression.jsonbPath());
@@ -30,8 +29,16 @@ void testJsonbPathExpression(ComparisonOperator operator, String keyPath, List<S
3029
@ParameterizedTest
3130
@MethodSource("temporal")
3231
void testJsonbPathExpressionWithTemporal(ComparisonOperator operator, String keyPath, List<String> arguments, String expectedJsonbFunction, String expectedJsonbPath) {
33-
JsonbSupport.DATE_TIME_SUPPORT = true;
34-
JsonbExpressionBuilder builder = new JsonbExpressionBuilder(operator, keyPath, arguments);
32+
JsonbExpressionBuilder builder = new JsonbExpressionBuilder(operator, keyPath, arguments, JsonbConfiguration.builder().useDateTime(true).build());
33+
var expression = builder.getJsonPathExpression();
34+
assertEquals(expectedJsonbFunction, expression.jsonbFunction());
35+
assertEquals(expectedJsonbPath, expression.jsonbPath());
36+
}
37+
38+
@ParameterizedTest
39+
@MethodSource("customized")
40+
void testJsonbPathExpressionCustomized(ComparisonOperator operator, String keyPath, List<String> arguments, String expectedJsonbFunction, String expectedJsonbPath) {
41+
JsonbExpressionBuilder builder = new JsonbExpressionBuilder(operator, keyPath, arguments, JsonbConfiguration.builder().pathExists("my_jsonb_path_exists").pathExistsTz("my_jsonb_path_exists_tz").useDateTime(true).build());
3542
var expression = builder.getJsonPathExpression();
3643
assertEquals(expectedJsonbFunction, expression.jsonbFunction());
3744
assertEquals(expectedJsonbPath, expression.jsonbPath());
@@ -78,6 +85,17 @@ static Stream<Arguments> conversion() {
7885
).filter(Objects::nonNull);
7986
}
8087

88+
static Stream<Arguments> customized() {
89+
90+
return Stream.of(
91+
arguments(RSQLOperators.EQUAL, "json.equal_key", Collections.singletonList("value"), "my_jsonb_path_exists", "$.equal_key ? (@ == \"value\")"),
92+
arguments(RSQLOperators.GREATER_THAN, "json.greater_than_key", Collections.singletonList("value"), "my_jsonb_path_exists", "$.greater_than_key ? (@ > \"value\")"),
93+
arguments(RSQLOperators.EQUAL, "json.equal_key", Collections.singletonList("1970-01-01T00:00:00.000"), "my_jsonb_path_exists", "$.equal_key ? (@.datetime() == \"1970-01-01T00:00:00.000\".datetime())"),
94+
arguments(RSQLOperators.EQUAL, "json.equal_key", Collections.singletonList("1970-01-01T00:00:00.000Z"), "my_jsonb_path_exists_tz", "$.equal_key ? (@.datetime() == \"1970-01-01T00:00:00.000Z\".datetime())"),
95+
null
96+
).filter(Objects::nonNull);
97+
}
98+
8199
static Stream<Arguments> temporal() {
82100

83101
return Stream.of(

0 commit comments

Comments
 (0)