Skip to content

allow to customize json search function #188

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
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
12 changes: 10 additions & 2 deletions rsql-jpa/src/main/java/io/github/perplexhub/rsql/QuerySupport.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,20 @@ public class QuerySupport {
private Map<Class<?>, List<String>> propertyBlacklist;
private Collection<String> procedureWhiteList;
private Collection<String> procedureBlackList;
/**
* Postgresql {@code jsonb_path_exists} function to use
*/
private String jsonbPathExists;
/**
* Postgresql {@code jsonb_path_exists_tz} function to use
*/
private String jsonbPathExistsTz;

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,jsonbPathExists:%s,jsonbPathExistsTz:%s",
rsqlQuery, distinct, propertyPathMapper, customPredicates == null ? 0 : customPredicates.size(), joinHints, propertyWhitelist, propertyBlacklist, jsonbPathExists, jsonbPathExistsTz);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ public class RSQLJPAPredicateConverter extends RSQLVisitorBase<Predicate, From>
private final Collection<String> procedureBlackList;
private final boolean strictEquality;
private final Character likeEscapeCharacter;
private final String jsonbPathExists;
private final String jsonbPathExistsTz;

public RSQLJPAPredicateConverter(CriteriaBuilder builder, Map<String, String> propertyPathMapper) {
this(builder, propertyPathMapper, null, null);
Expand All @@ -54,14 +56,27 @@ 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, null, 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) {
Character likeEscapeCharacter,
String jsonbPathExists,
String jsonbPathExistsTz) {
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 +85,8 @@ public RSQLJPAPredicateConverter(CriteriaBuilder builder,
this.procedureBlackList = proceduresBlackList != null ? proceduresBlackList : Collections.emptyList();
this.strictEquality = strictEquality;
this.likeEscapeCharacter = likeEscapeCharacter;
this.jsonbPathExists = jsonbPathExists;
this.jsonbPathExistsTz = jsonbPathExistsTz;
}

RSQLJPAContext findPropertyPath(String propertyPath, Path startRoot) {
Expand Down Expand Up @@ -244,7 +261,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, jsonbPathExists, jsonbPathExistsTz);
} 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.getJsonbPathExists(), querySupport.getJsonbPathExistsTz());

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,9 @@
*/
public class JsonbExpressionBuilder {

private final String jsonbPathExistsTz;
private final String jsonbPathExists;

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

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

JsonbExpressionBuilder(ComparisonOperator operator, String keyPath, List<String> args, String jsonbPathExists, String jsonbPathExistsTz) {
this.operator = Objects.requireNonNull(operator);
this.keyPath = Objects.requireNonNull(keyPath);
if(FORBIDDEN_NEGATION.contains(operator)) {
Expand All @@ -189,6 +196,8 @@ public ArgValue convert(String s) {
throw new IllegalArgumentException("Operator " + operator + " requires at least one value");
}
this.values = findMoreTypes(operator, candidateValues);
this.jsonbPathExistsTz = jsonbPathExistsTz == null ? JSONB_PATH_EXISTS_TZ : jsonbPathExistsTz;
this.jsonbPathExists = jsonbPathExists == null ? JSONB_PATH_EXISTS : jsonbPathExists;
}

/**
Expand All @@ -210,7 +219,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 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 Down Expand Up @@ -63,9 +61,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, String jsonbPathExists, String jsonbPathExistsTz) {
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(), jsonbPathExists, jsonbPathExistsTz);
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
Expand Up @@ -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 @@ -542,9 +543,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 +722,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(new QuerySupport.QuerySupportBuilder().rsqlQuery(rsql).jsonbPathExists("my_jsonb_path_exists").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 @@ -37,6 +37,16 @@ void testJsonbPathExpressionWithTemporal(ComparisonOperator operator, String key
assertEquals(expectedJsonbPath, expression.jsonbPath());
}

@ParameterizedTest
@MethodSource("customized")
void testJsonbPathExpressionCustomized(ComparisonOperator operator, String keyPath, List<String> arguments, String expectedJsonbFunction, String expectedJsonbPath) {
JsonbSupport.DATE_TIME_SUPPORT = true;
JsonbExpressionBuilder builder = new JsonbExpressionBuilder(operator, keyPath, arguments, "my_jsonb_path_exists", "my_jsonb_path_exists_tz");
var expression = builder.getJsonPathExpression();
assertEquals(expectedJsonbFunction, expression.jsonbFunction());
assertEquals(expectedJsonbPath, expression.jsonbPath());
}

static Stream<Arguments> data() {
return Stream.of(
allOperators(),
Expand Down Expand Up @@ -78,6 +88,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