Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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.JsonbExtractor;
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 JsonbExtractor jsonbExtractor = JsonbExtractor.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,jsonbExtractor:%s",
rsqlQuery, distinct, propertyPathMapper, customPredicates == null ? 0 : customPredicates.size(), joinHints, propertyWhitelist, propertyBlacklist, jsonbExtractor);
}
}
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.JsonbExtractor;
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 JsonbExtractor jsonbExtractor;

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, JsonbExtractor.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,
JsonbExtractor jsonbExtractor) {
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.jsonbExtractor = jsonbExtractor;
}

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, jsonbExtractor);
} 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.getJsonbExtractor());

visitor.setPropertyWhitelist(querySupport.getPropertyWhitelist());
visitor.setPropertyBlacklist(querySupport.getPropertyBlacklist());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
*/
public class JsonbExpressionBuilder {

private final String jsonbPathExistsTz;
private final String jsonbPathExists;
private final boolean useDateTime;

/**
* The base json type.
*/
Expand Down Expand Up @@ -170,6 +174,10 @@ public ArgValue convert(String s) {
private final List<ArgValue> values;

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

JsonbExpressionBuilder(ComparisonOperator operator, String keyPath, List<String> args, JsonbExtractor extractor) {
this.operator = Objects.requireNonNull(operator);
this.keyPath = Objects.requireNonNull(keyPath);
if(FORBIDDEN_NEGATION.contains(operator)) {
Expand All @@ -188,6 +196,9 @@ 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.useDateTime = extractor.useDateTime();
this.jsonbPathExistsTz = extractor.pathExistsTz();
this.jsonbPathExists = extractor.pathExists();
this.values = findMoreTypes(operator, candidateValues);
}

Expand All @@ -210,7 +221,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 ? jsonbPathExistsTz : jsonbPathExists;
var expression = String.format("%s ? %s", targetPath, String.format(comparisonTemplate, templateArguments.toArray()));
return new JsonbPathExpression(function, expression);
}
Expand All @@ -234,7 +245,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 = 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
@@ -0,0 +1,48 @@
package io.github.perplexhub.rsql.jsonb;

/**
* jsonb expression configuration
*/
public interface JsonbExtractor {

JsonbExtractor DEFAULT = new JsonbExtractor() {

@Override
public String pathExists() {
return "jsonb_path_exists";
}

@Override
public String pathExistsTz() {
return "jsonb_path_exists_tz";
}

@Override
public boolean useDateTime() {
return false;
}

@Override
public String toString() {
return String.format("pathExists:%s,pathExistsTz:%s,useDateTime:%b", pathExists(), pathExistsTz(), useDateTime());
}
};

/**
*
* @return Postgresql {@code jsonb_path_exists} function to use
*/
String pathExists();

/**
*
* @return Postgresql {@code jsonb_path_exists_tz} function to use
*/
String pathExistsTz();

/**
*
* @return enable temporal values support
*/
boolean useDateTime();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.github.perplexhub.rsql.jsonb;

import lombok.Builder;
import lombok.Getter;
import lombok.experimental.Accessors;

/**
* convenient way to define configuration, based on default values
*/
@Builder
@Accessors(fluent = true)
@Getter
public class JsonbExtractorSupport implements JsonbExtractor {

@Builder.Default
private final String pathExists = JsonbExtractor.DEFAULT.pathExists();
@Builder.Default
private final String pathExistsTz = JsonbExtractor.DEFAULT.pathExistsTz();
@Builder.Default
private final boolean useDateTime = JsonbExtractor.DEFAULT.useDateTime();

@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 @@ -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, JsonbExtractor extractor) {
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(), extractor);
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.JsonbExtractorSupport;
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).jsonbExtractor(JsonbExtractorSupport.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).jsonbExtractor(JsonbExtractorSupport.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, JsonbExtractor.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, JsonbExtractorSupport.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, JsonbExtractorSupport.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