Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.github.perplexhub.rsql;

import io.github.perplexhub.rsql.jsonb.JsonbConfiguration;
import lombok.Builder;
import lombok.Data;

Expand Down Expand Up @@ -27,12 +28,14 @@ public class QuerySupport {
private Map<Class<?>, List<String>> propertyBlacklist;
private Collection<String> procedureWhiteList;
private Collection<String> procedureBlackList;
@Builder.Default
private JsonbConfiguration jsonbConfiguration = JsonbConfiguration.DEFAULT;

public static class QuerySupportBuilder {}

@Override
public String toString() {
return String.format("%s,distinct:%b,propertyPathMapper:%s,customPredicates:%d,joinHints:%s,propertyWhitelist:%s,propertyBlacklist:%s",
rsqlQuery, distinct, propertyPathMapper, customPredicates == null ? 0 : customPredicates.size(), joinHints, propertyWhitelist, propertyBlacklist);
return String.format("%s,distinct:%b,propertyPathMapper:%s,customPredicates:%d,joinHints:%s,propertyWhitelist:%s,propertyBlacklist:%s,jsonbConfiguration:%s",
rsqlQuery, distinct, propertyPathMapper, customPredicates == null ? 0 : customPredicates.size(), joinHints, propertyWhitelist, propertyBlacklist, jsonbConfiguration);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import java.util.function.Function;
import java.util.stream.Collectors;

import io.github.perplexhub.rsql.jsonb.JsonbConfiguration;
import io.github.perplexhub.rsql.jsonb.JsonbSupport;
import jakarta.persistence.criteria.*;
import jakarta.persistence.metamodel.Attribute;
Expand Down Expand Up @@ -37,6 +38,7 @@ public class RSQLJPAPredicateConverter extends RSQLVisitorBase<Predicate, From>
private final Collection<String> procedureBlackList;
private final boolean strictEquality;
private final Character likeEscapeCharacter;
private final JsonbConfiguration jsonbConfiguration;

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

public RSQLJPAPredicateConverter(CriteriaBuilder builder,
Map<String, String> propertyPathMapper,
List<RSQLCustomPredicate<?>> customPredicates,
Map<String, JoinType> joinHints,
Collection<String> proceduresWhiteList,
Collection<String> proceduresBlackList,
boolean strictEquality,
Character likeEscapeCharacter) {
this(builder, propertyPathMapper, customPredicates, joinHints, proceduresWhiteList, proceduresBlackList, strictEquality, likeEscapeCharacter, JsonbConfiguration.DEFAULT);
}

public RSQLJPAPredicateConverter(CriteriaBuilder builder,
Map<String, String> propertyPathMapper,
List<RSQLCustomPredicate<?>> customPredicates,
Map<String, JoinType> joinHints,
Collection<String> proceduresWhiteList,
Collection<String> proceduresBlackList,
boolean strictEquality,
Character likeEscapeCharacter) {
Map<String, String> propertyPathMapper,
List<RSQLCustomPredicate<?>> customPredicates,
Map<String, JoinType> joinHints,
Collection<String> proceduresWhiteList,
Collection<String> proceduresBlackList,
boolean strictEquality,
Character likeEscapeCharacter,
JsonbConfiguration jsonbConfiguration) {
this.builder = builder;
this.propertyPathMapper = propertyPathMapper != null ? propertyPathMapper : Collections.emptyMap();
this.customPredicates = customPredicates != null ? customPredicates.stream().collect(Collectors.toMap(RSQLCustomPredicate::getOperator, Function.identity(), (a, b) -> a)) : Collections.emptyMap();
Expand All @@ -70,6 +84,7 @@ public RSQLJPAPredicateConverter(CriteriaBuilder builder,
this.procedureBlackList = proceduresBlackList != null ? proceduresBlackList : Collections.emptyList();
this.strictEquality = strictEquality;
this.likeEscapeCharacter = likeEscapeCharacter;
this.jsonbConfiguration = jsonbConfiguration;
}

RSQLJPAContext findPropertyPath(String propertyPath, Path startRoot) {
Expand Down Expand Up @@ -244,7 +259,7 @@ private ResolvedExpression resolveExpression(ComparisonNode node, From root, Sel
String jsonbPath = JsonbSupport.jsonPathOfSelector(attribute, jsonSelector);
if(jsonbPath.contains(".")) {
ComparisonNode jsonbNode = node.withSelector(jsonbPath);
return JsonbSupport.jsonbPathExistsExpression(builder, jsonbNode, path);
return JsonbSupport.jsonbPathExistsExpression(builder, jsonbNode, path, jsonbConfiguration);
} else {
final Expression expression;
if (path instanceof JpaExpression jpaExpression) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ public static <T> Specification<T> toSpecification(final QuerySupport querySuppo
RSQLJPAPredicateConverter visitor = new RSQLJPAPredicateConverter(cb, querySupport.getPropertyPathMapper(),
querySupport.getCustomPredicates(), querySupport.getJoinHints(),
querySupport.getProcedureWhiteList(), querySupport.getProcedureBlackList(),
querySupport.isStrictEquality(), querySupport.getLikeEscapeCharacter());
querySupport.isStrictEquality(), querySupport.getLikeEscapeCharacter(),
querySupport.getJsonbConfiguration());

visitor.setPropertyWhitelist(querySupport.getPropertyWhitelist());
visitor.setPropertyBlacklist(querySupport.getPropertyBlacklist());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.github.perplexhub.rsql.jsonb;

import lombok.Builder;

/**
* convenient way to define configuration, based on default values
*
* @param pathExists Postgresql {@code jsonb_path_exists} function to use
* @param pathExistsTz Postgresql {@code jsonb_path_exists_tz} function to use
* @param useDateTime enable temporal values support
*/
@Builder
public record JsonbConfiguration(String pathExists, String pathExistsTz, boolean useDateTime) {

public static final JsonbConfiguration DEFAULT = JsonbConfiguration.builder().build();

public static class JsonbConfigurationBuilder {
JsonbConfigurationBuilder() {
pathExists = "jsonb_path_exists";
pathExistsTz = "jsonb_path_exists_tz";
useDateTime = false;
}
}

@Override
public String toString() {
return String.format("pathExists:%s,pathExistsTz:%s,useDateTime:%b", pathExists, pathExistsTz, useDateTime);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
public class JsonbExpressionBuilder {

private final JsonbConfiguration configuration;

/**
* The base json type.
*/
Expand Down Expand Up @@ -161,15 +163,15 @@ public ArgValue convert(String s) {
Map.entry(BETWEEN, "(%1$s >= %2$s && %1$s <= %3$s)")
);

private static final String JSONB_PATH_EXISTS = "jsonb_path_exists";

private static final String JSONB_PATH_EXISTS_TZ = "jsonb_path_exists_tz";

private final ComparisonOperator operator;
private final String keyPath;
private final List<ArgValue> values;

JsonbExpressionBuilder(ComparisonOperator operator, String keyPath, List<String> args) {
this(operator, keyPath, args, JsonbConfiguration.DEFAULT);
}

JsonbExpressionBuilder(ComparisonOperator operator, String keyPath, List<String> args, JsonbConfiguration configuration) {
this.operator = Objects.requireNonNull(operator);
this.keyPath = Objects.requireNonNull(keyPath);
if(FORBIDDEN_NEGATION.contains(operator)) {
Expand All @@ -188,6 +190,7 @@ public ArgValue convert(String s) {
if(REQUIRE_AT_LEAST_ONE_ARGUMENT.contains(operator) && candidateValues.isEmpty()) {
throw new IllegalArgumentException("Operator " + operator + " requires at least one value");
}
this.configuration = configuration;
this.values = findMoreTypes(operator, candidateValues);
}

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

List<ArgConverter> argConverters = DATE_TIME_SUPPORT ?
List<ArgConverter> argConverters = configuration.useDateTime() ?
List.of(DATE_TIME_CONVERTER, DATE_TIME_CONVERTER_TZ, NUMBER_CONVERTER, BOOLEAN_ARG_CONVERTER)
: List.of(NUMBER_CONVERTER, BOOLEAN_ARG_CONVERTER);
Optional<ArgConverter> candidateConverter = argConverters.stream()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@
import io.github.perplexhub.rsql.ResolvedExpression;
import jakarta.persistence.Column;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.Expression;
import jakarta.persistence.criteria.Path;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.metamodel.Attribute;
import jakarta.persistence.metamodel.ManagedType;
import org.springframework.orm.jpa.vendor.Database;
Expand All @@ -28,8 +26,6 @@
*/
public class JsonbSupport {

public static boolean DATE_TIME_SUPPORT = false;

private static final Set<Database> JSON_SUPPORT = EnumSet.of(Database.POSTGRESQL);

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


public static ResolvedExpression jsonbPathExistsExpression(CriteriaBuilder builder, ComparisonNode node, Path<?> attrPath) {
public static ResolvedExpression jsonbPathExistsExpression(CriteriaBuilder builder, ComparisonNode node, Path<?> attrPath, JsonbConfiguration configuration) {
var mayBeInvertedOperator = Optional.ofNullable(NEGATE_OPERATORS.get(node.getOperator()));
var jsb = new JsonbExpressionBuilder(mayBeInvertedOperator.orElse(node.getOperator()), node.getSelector(), node.getArguments());
var jsb = new JsonbExpressionBuilder(mayBeInvertedOperator.orElse(node.getOperator()), node.getSelector(), node.getArguments(), configuration);
var expression = jsb.getJsonPathExpression();
return ResolvedExpression.ofJson(builder.function(expression.jsonbFunction, Boolean.class, attrPath,
builder.literal(expression.jsonbPath)), mayBeInvertedOperator.isPresent());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.github.perplexhub.rsql;

import io.github.perplexhub.rsql.jsonb.JsonbSupport;
import io.github.perplexhub.rsql.jsonb.JsonbConfiguration;
import io.github.perplexhub.rsql.model.EntityWithJsonb;
import io.github.perplexhub.rsql.model.JsonbEntity;
import io.github.perplexhub.rsql.model.PostgresJsonEntity;
Expand All @@ -17,6 +17,7 @@
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.jdbc.Sql;

import java.util.Collection;
import java.util.HashMap;
Expand Down Expand Up @@ -47,7 +48,6 @@ class RSQLJPASupportPostgresJsonTest {
void setup(@Autowired EntityManager em) {
RSQLVisitorBase.setEntityManagerDatabase(Map.of(em, Database.POSTGRESQL));
clear();
JsonbSupport.DATE_TIME_SUPPORT = false;
}

@AfterEach
Expand Down Expand Up @@ -83,12 +83,11 @@ void testJsonSearch(List<PostgresJsonEntity> entities, String rsql, List<Postgre
@ParameterizedTest
@MethodSource("temporalData")
void testJsonSearchOfTemporal(List<PostgresJsonEntity> entities, String rsql, List<PostgresJsonEntity> expected) {
JsonbSupport.DATE_TIME_SUPPORT = true;
//given
repository.saveAllAndFlush(entities);

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

//then
assertThat(result)
Expand Down Expand Up @@ -161,7 +160,6 @@ void testJsonSearchOnMappedRelation(List<JsonbEntity> jsonbEntities, String rsql
@ParameterizedTest
@MethodSource("sortByRelation")
void testJsonSortOnRelation(List<JsonbEntity> jsonbEntities, String rsql, List<JsonbEntity> expected) {
JsonbSupport.DATE_TIME_SUPPORT = true;
//given
Collection<EntityWithJsonb> entitiesWithJsonb = jsonbEntityRepository.saveAllAndFlush(jsonbEntities).stream()
.map(jsonbEntity -> EntityWithJsonb.builder().jsonb(jsonbEntity).build())
Expand All @@ -183,7 +181,6 @@ void testJsonSortOnRelation(List<JsonbEntity> jsonbEntities, String rsql, List<J
@ParameterizedTest
@MethodSource("sortByMappedRelation")
void testJsonSortOnMappedRelation(List<JsonbEntity> jsonbEntities, String rsql, List<JsonbEntity> expected) {
JsonbSupport.DATE_TIME_SUPPORT = true;
//given
Collection<EntityWithJsonb> entitiesWithJsonb = jsonbEntityRepository.saveAllAndFlush(jsonbEntities).stream()
.map(jsonbEntity -> EntityWithJsonb.builder().jsonb(jsonbEntity).build())
Expand Down Expand Up @@ -542,9 +539,9 @@ private static Stream<Arguments> meltedTimeZone() {
var e3 = new PostgresJsonEntity(Map.of("a", "2020-01-01T00:00:00"));
var allCases = List.of(e1, e2, e3);
return Stream.of(
arguments(allCases, "properties.a=ge=1970-01-02T00:00:00+00:00", List.of(e2, e3)),
arguments(allCases, "properties.a=ge=1970-01-02T00:00:00+01:00", List.of(e2, e3)),
arguments(allCases, "properties.a=lt=2022-01-01T00:00:00+01:00", List.of(e1, e2, e3)),
arguments(allCases, "properties.a=ge=1970-01-02T00:00:00+00:00", List.of(e2, e3)),
arguments(allCases, "properties.a=ge=1970-01-02T00:00:00+01:00", List.of(e2, e3)),
arguments(allCases, "properties.a=lt=2022-01-01T00:00:00+01:00", List.of(e1, e2, e3)),
null
).filter(Objects::nonNull);
}
Expand Down Expand Up @@ -721,4 +718,23 @@ private static Map<String, Object> nullMap(String key) {
nullValue.put(key, null);
return nullValue;
}

@Sql(statements = "CREATE OR REPLACE FUNCTION my_jsonb_path_exists(arg1 jsonb,arg2 jsonpath) RETURNS boolean AS 'SELECT $1 @? $2' LANGUAGE 'sql' IMMUTABLE;")
@Sql(statements = "DROP FUNCTION IF EXISTS my_jsonb_path_exists;", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
@ParameterizedTest
@MethodSource("data")
void testJsonSearchCustomFunction(List<PostgresJsonEntity> entities, String rsql, List<PostgresJsonEntity> expected) {
//given
repository.saveAllAndFlush(entities);

//when
List<PostgresJsonEntity> result = repository.findAll(toSpecification(QuerySupport.builder().rsqlQuery(rsql).jsonbConfiguration(JsonbConfiguration.builder().pathExists("my_jsonb_path_exists").build()).build()));

//then
assertThat(result)
.hasSameSizeAs(expected)
.containsExactlyInAnyOrderElementsOf(expected);

entities.forEach(e -> e.setId(null));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ class JsonbExpressionBuilderTest {
@ParameterizedTest
@MethodSource("data")
void testJsonbPathExpression(ComparisonOperator operator, String keyPath, List<String> arguments, String expectedJsonbFunction, String expectedJsonbPath) {
JsonbSupport.DATE_TIME_SUPPORT = false;
JsonbExpressionBuilder builder = new JsonbExpressionBuilder(operator, keyPath, arguments);
JsonbExpressionBuilder builder = new JsonbExpressionBuilder(operator, keyPath, arguments, JsonbConfiguration.DEFAULT);
var expression = builder.getJsonPathExpression();
assertEquals(expectedJsonbFunction, expression.jsonbFunction());
assertEquals(expectedJsonbPath, expression.jsonbPath());
Expand All @@ -30,8 +29,16 @@ void testJsonbPathExpression(ComparisonOperator operator, String keyPath, List<S
@ParameterizedTest
@MethodSource("temporal")
void testJsonbPathExpressionWithTemporal(ComparisonOperator operator, String keyPath, List<String> arguments, String expectedJsonbFunction, String expectedJsonbPath) {
JsonbSupport.DATE_TIME_SUPPORT = true;
JsonbExpressionBuilder builder = new JsonbExpressionBuilder(operator, keyPath, arguments);
JsonbExpressionBuilder builder = new JsonbExpressionBuilder(operator, keyPath, arguments, JsonbConfiguration.builder().useDateTime(true).build());
var expression = builder.getJsonPathExpression();
assertEquals(expectedJsonbFunction, expression.jsonbFunction());
assertEquals(expectedJsonbPath, expression.jsonbPath());
}

@ParameterizedTest
@MethodSource("customized")
void testJsonbPathExpressionCustomized(ComparisonOperator operator, String keyPath, List<String> arguments, String expectedJsonbFunction, String expectedJsonbPath) {
JsonbExpressionBuilder builder = new JsonbExpressionBuilder(operator, keyPath, arguments, JsonbConfiguration.builder().pathExists("my_jsonb_path_exists").pathExistsTz("my_jsonb_path_exists_tz").useDateTime(true).build());
var expression = builder.getJsonPathExpression();
assertEquals(expectedJsonbFunction, expression.jsonbFunction());
assertEquals(expectedJsonbPath, expression.jsonbPath());
Expand Down Expand Up @@ -78,6 +85,17 @@ static Stream<Arguments> conversion() {
).filter(Objects::nonNull);
}

static Stream<Arguments> customized() {

return Stream.of(
arguments(RSQLOperators.EQUAL, "json.equal_key", Collections.singletonList("value"), "my_jsonb_path_exists", "$.equal_key ? (@ == \"value\")"),
arguments(RSQLOperators.GREATER_THAN, "json.greater_than_key", Collections.singletonList("value"), "my_jsonb_path_exists", "$.greater_than_key ? (@ > \"value\")"),
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())"),
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())"),
null
).filter(Objects::nonNull);
}

static Stream<Arguments> temporal() {

return Stream.of(
Expand Down