Skip to content
Merged
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
Expand Up @@ -13,7 +13,9 @@ public enum Operator {
LESSER_THAN(1),
LESSER_THAN_EQUALS(1),
IS_NULL(0),
IS_NOT_NULL(0);
IS_NOT_NULL(0),
IN(1),
NOT_IN(1);

@Getter
private final int requiredValuesCount;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,24 @@ void givenFilterWithOneIsNotNullCondition_whenFindAll_thenReturnListOfEntity() {
assertThat(people).isNotNull();
}

@Test
void givenFilterWithOneInListCondition_whenFindAll_thenReturnListOfEntity() {
List<Person> people = querity.findAll(Person.class,
Querity.query()
.filter(filterBy("lastName", IN, List.of("Skywalker", "Solo")))
.build());
assertThat(people).isNotNull();
}

@Test
void givenFilterWithOneNotInListCondition_whenFindAll_thenReturnListOfEntity() {
List<Person> people = querity.findAll(Person.class,
Querity.query()
.filter(filterBy("lastName", NOT_IN, List.of("Skywalker", "Solo")))
.build());
assertThat(people).isNotNull();
}

@Test
void givenFilterWithNotConditionContainingSimpleCondition_whenFindAll_thenReturnListOfEntity() {
List<Person> people = querity.findAll(Person.class,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.stream.StreamSupport;

import static io.github.queritylib.querity.common.util.ReflectionUtils.getAccessibleField;

Expand Down Expand Up @@ -49,6 +50,16 @@ private static Class<?> getGenericType(Field field, int index) {
public static <T> Object getActualPropertyValue(Class<T> beanClass, String propertyPath, Object value) {
if (value == null) return null;
Class<?> propertyType = getPropertyType(beanClass, propertyPath);
return PropertyValueExtractorFactory.getPropertyValueExtractor(propertyType).extractValue(propertyType, value);
if (value instanceof Iterable<?> it) {
return StreamSupport.stream(it.spliterator(), false)
.map(v -> PropertyValueExtractorFactory.getPropertyValueExtractor(propertyType).extractValue(propertyType, v))
.toArray();
} else if (value.getClass().isArray()) {
return Arrays.stream((Object[]) value)
.map(v -> PropertyValueExtractorFactory.getPropertyValueExtractor(propertyType).extractValue(propertyType, v))
.toArray();
} else {
return PropertyValueExtractorFactory.getPropertyValueExtractor(propertyType).extractValue(propertyType, value);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ public static Stream<Arguments> provideValuesAndTypes() {
Arguments.of(MyClass.class, "doubleValue", 1.2, Double.class),
Arguments.of(MyClass.class, "nested.stringValue", "test", String.class),
Arguments.of(MyClass.class, "stringList", "test", String.class),
Arguments.of(MyClass.class, "nestedList.stringValue", "test", String.class)
Arguments.of(MyClass.class, "nestedList.stringValue", "test", String.class),
Arguments.of(MyClass.class, "stringValue", List.of("test"), Object[].class),
Arguments.of(MyClass.class, "stringValue", new Object[]{"test"}, Object[].class)
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ class JpaOperatorMapper {
OPERATOR_PREDICATE_MAP.put(Operator.LESSER_THAN_EQUALS, JpaOperatorMapper::getLesserThanEquals);
OPERATOR_PREDICATE_MAP.put(Operator.IS_NULL, (path, value, cb) -> getIsNull(path, cb));
OPERATOR_PREDICATE_MAP.put(Operator.IS_NOT_NULL, (path, value, cb) -> getIsNotNull(path, cb));
OPERATOR_PREDICATE_MAP.put(Operator.IN, JpaOperatorMapper::getIn);
OPERATOR_PREDICATE_MAP.put(Operator.NOT_IN, JpaOperatorMapper::getNotIn);
}

private static Predicate getIsNull(Path<?> path, CriteriaBuilder cb) {
Expand Down Expand Up @@ -81,6 +83,18 @@ private static Predicate getLesserThanEquals(Path<?> path, Object value, Criteri
return cb.lessThanOrEqualTo((Expression) path, (Comparable) value);
}

private static Predicate getIn(Path<?> path, Object value, CriteriaBuilder cb) {
if (value.getClass().isArray()) {
return cb.and(path.in((Object[]) value), getIsNotNull(path, cb));
} else {
throw new IllegalArgumentException("Value must be an array");
}
}

private static Predicate getNotIn(Path<?> path, Object value, CriteriaBuilder cb) {
return getIn(path, value, cb).not();
}

@FunctionalInterface
private interface JpaOperatorPredicateProvider {
Predicate getPredicate(Path<?> path, Object value, CriteriaBuilder cb);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,29 @@
lexer grammar QueryLexer;

DISTINCT : 'distinct';
AND : 'and';
OR : 'or';
NOT : 'not';
SORT : 'sort by';
ASC : 'asc';
DESC : 'desc';
PAGINATION : 'page';
NEQ : '!=';
LTE : '<=';
GTE : '>=';
EQ : '=';
LT : '<';
GT : '>';
STARTS_WITH: 'starts with';
ENDS_WITH : 'ends with';
CONTAINS : 'contains';
IS_NULL : 'is null';
IS_NOT_NULL: 'is not null';
LPAREN : '(';
RPAREN : ')';
COMMA : ',';
DISTINCT : 'distinct';
AND : 'and';
OR : 'or';
NOT : 'not';
SORT : 'sort by';
ASC : 'asc';
DESC : 'desc';
PAGINATION : 'page';
NEQ : '!=';
LTE : '<=';
GTE : '>=';
EQ : '=';
LT : '<';
GT : '>';
STARTS_WITH : 'starts with';
ENDS_WITH : 'ends with';
CONTAINS : 'contains';
IS_NULL : 'is null';
IS_NOT_NULL : 'is not null';
IN : 'in';
NOT_IN : 'not in';
LPAREN : '(';
RPAREN : ')';
COMMA : ',';

INT_VALUE : [0-9]+;
DECIMAL_VALUE : [0-9]+'.'[0-9]+;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ options { tokenVocab=QueryLexer; }

query : DISTINCT? (condition)? (SORT sortFields)? (PAGINATION paginationParams)? ;
condition : simpleCondition | conditionWrapper | notCondition;
operator : NEQ | LTE | GTE | EQ | LT | GT | STARTS_WITH | ENDS_WITH | CONTAINS | IS_NULL | IS_NOT_NULL ;
operator : NEQ | LTE | GTE | EQ | LT | GT | STARTS_WITH | ENDS_WITH | CONTAINS | IS_NULL | IS_NOT_NULL | IN | NOT_IN ;
conditionWrapper : (AND | OR) LPAREN condition (COMMA condition)* RPAREN ;
notCondition : NOT LPAREN condition RPAREN ;
simpleCondition : PROPERTY operator (INT_VALUE | DECIMAL_VALUE | BOOLEAN_VALUE | STRING_VALUE)? ;
simpleValue : INT_VALUE | DECIMAL_VALUE | BOOLEAN_VALUE | STRING_VALUE;
arrayValue : LPAREN simpleValue (COMMA simpleValue)* RPAREN ;
simpleCondition : PROPERTY operator (simpleValue | arrayValue)? ;
direction : ASC | DESC ;
sortField : PROPERTY (direction)? ;
sortFields : sortField (COMMA sortField)* ;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,10 @@ public Condition visitSimpleCondition(QueryParser.SimpleConditionContext ctx) {
Operator operator = (Operator) visit(ctx.operator());
Object value = null;
if (operator.getRequiredValuesCount() > 0) {
if (ctx.INT_VALUE() != null) {
value = Integer.parseInt(ctx.INT_VALUE().getText());
} else if (ctx.DECIMAL_VALUE() != null) {
value = new BigDecimal(ctx.DECIMAL_VALUE().getText());
} else if (ctx.BOOLEAN_VALUE() != null) {
value = Boolean.valueOf(ctx.BOOLEAN_VALUE().getText());
} else {
value = ctx.STRING_VALUE().getText().replace("\"", ""); // Remove quotes if present
if (ctx.arrayValue() != null) {
value = visitArrayValue(ctx.arrayValue());
} else if (ctx.simpleValue() != null) {
value = visitSimpleValue(ctx.simpleValue());
}
}

Expand All @@ -91,6 +87,28 @@ public Condition visitSimpleCondition(QueryParser.SimpleConditionContext ctx) {
.build();
}

@Override
public Object visitSimpleValue(QueryParser.SimpleValueContext ctx) {
Object value;
if (ctx.INT_VALUE() != null) {
value = Integer.parseInt(ctx.INT_VALUE().getText());
} else if (ctx.DECIMAL_VALUE() != null) {
value = new BigDecimal(ctx.DECIMAL_VALUE().getText());
} else if (ctx.BOOLEAN_VALUE() != null) {
value = Boolean.valueOf(ctx.BOOLEAN_VALUE().getText());
} else {
value = ctx.STRING_VALUE().getText().replace("\"", ""); // Remove quotes if present
}
return value;
}

@Override
public Object visitArrayValue(QueryParser.ArrayValueContext ctx) {
return ctx.simpleValue().stream()
.map(this::visitSimpleValue)
.toArray();
}

@Override
public Object visitOperator(QueryParser.OperatorContext ctx) {
if (ctx.GTE() != null) {
Expand All @@ -115,6 +133,10 @@ public Object visitOperator(QueryParser.OperatorContext ctx) {
return Operator.IS_NULL;
} else if (ctx.IS_NOT_NULL() != null) {
return Operator.IS_NOT_NULL;
} else if (ctx.IN() != null) {
return Operator.IN;
} else if (ctx.NOT_IN() != null) {
return Operator.NOT_IN;
} else {
throw new UnsupportedOperationException("Unsupported operator: " + ctx.getText());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import static io.github.queritylib.querity.api.Querity.*;
import static io.github.queritylib.querity.api.Sort.Direction.ASC;
import static io.github.queritylib.querity.api.Sort.Direction.DESC;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.assertj.core.api.Assertions.assertThat;

class QuerityParserTests {

Expand Down Expand Up @@ -52,6 +52,10 @@ public static Stream<Arguments> provideArguments() {
Querity.query().filter(filterBy("lastName", IS_NULL)).build()),
Arguments.of("lastName is not null",
Querity.query().filter(filterBy("lastName", IS_NOT_NULL)).build()),
Arguments.of("lastName in (\"Skywalker\", \"Solo\")",
Querity.query().filter(filterBy("lastName", IN, new Object[]{"Skywalker", "Solo"})).build()),
Arguments.of("lastName not in (\"Skywalker\", \"Solo\")",
Querity.query().filter(filterBy("lastName", NOT_IN, new Object[]{"Skywalker", "Solo"})).build()),
Arguments.of("deleted=false",
Querity.query().filter(filterBy("deleted", Boolean.FALSE)).build()),
Arguments.of("address.city=\"Rome\"",
Expand All @@ -67,6 +71,6 @@ public static Stream<Arguments> provideArguments() {
@MethodSource("provideArguments")
void testParseQuery(String query, Query expected) {
Query actual = QuerityParser.parseQuery(query);
assertEquals(expected, actual);
assertThat(actual).usingRecursiveComparison().isEqualTo(expected);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ class ElasticsearchOperatorMapper {
OPERATOR_CRITERIA_MAP.put(Operator.LESSER_THAN_EQUALS, ElasticsearchOperatorMapper::getLesserThanEquals);
OPERATOR_CRITERIA_MAP.put(Operator.IS_NULL, (where, value, negate) -> getIsNull(where, negate));
OPERATOR_CRITERIA_MAP.put(Operator.IS_NOT_NULL, (where, value, negate) -> getIsNull(where, !negate));
OPERATOR_CRITERIA_MAP.put(Operator.IN, ElasticsearchOperatorMapper::getIn);
OPERATOR_CRITERIA_MAP.put(Operator.NOT_IN, ElasticsearchOperatorMapper::getNotIn);
}

private static Criteria getIsNull(Criteria where, boolean negate) {
Expand Down Expand Up @@ -72,6 +74,22 @@ private static Criteria getLesserThanEquals(Criteria where, Object value, boolea
return negate ? where.greaterThan(value) : where.lessThanEqual(value);
}

private static Criteria getIn(Criteria where, Object value, boolean negate) {
if (value.getClass().isArray()) {
return negate ? where.not().in((Object[]) value) : where.in((Object[]) value);
} else {
throw new IllegalArgumentException("Value must be an array");
}
}

private static Criteria getNotIn(Criteria where, Object value, boolean negate) {
if (value.getClass().isArray()) {
return negate ? where.in((Object[]) value) : where.not().in((Object[]) value);
} else {
throw new IllegalArgumentException("Value must be an array");
}
}

@FunctionalInterface
private interface ElasticSearchOperatorCriteriaProvider {
Criteria getCriteria(Criteria where, Object value, boolean negate);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ class MongodbOperatorMapper {
OPERATOR_CRITERIA_MAP.put(Operator.LESSER_THAN_EQUALS, MongodbOperatorMapper::getLesserThanEquals);
OPERATOR_CRITERIA_MAP.put(Operator.IS_NULL, (where, value, negate) -> getIsNull(where, negate));
OPERATOR_CRITERIA_MAP.put(Operator.IS_NOT_NULL, (where, value, negate) -> getIsNull(where, !negate));
OPERATOR_CRITERIA_MAP.put(Operator.IN, MongodbOperatorMapper::getIn);
OPERATOR_CRITERIA_MAP.put(Operator.NOT_IN, MongodbOperatorMapper::getNotIn);
}

private static Criteria getIsNull(Criteria where, boolean negate) {
Expand Down Expand Up @@ -74,6 +76,22 @@ private static Criteria getLesserThanEquals(Criteria where, Object value, boolea
return negate ? where.gt(value) : where.lte(value);
}

private static Criteria getIn(Criteria where, Object value, boolean negate) {
if (value.getClass().isArray()) {
return negate ? where.nin((Object[]) value) : where.in((Object[]) value);
} else {
throw new IllegalArgumentException("Value must be an array");
}
}

private static Criteria getNotIn(Criteria where, Object value, boolean negate) {
if (value.getClass().isArray()) {
return negate ? where.in((Object[]) value) : where.nin((Object[]) value);
} else {
throw new IllegalArgumentException("Value must be an array");
}
}

@FunctionalInterface
private interface MongodbOperatorCriteriaProvider {
Criteria getCriteria(Criteria where, Object value, boolean negate);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import io.github.queritylib.querity.api.*;
import lombok.SneakyThrows;

import java.io.IOException;
import java.util.List;
Expand Down Expand Up @@ -34,13 +35,15 @@ public Condition deserialize(JsonParser jsonParser, DeserializationContext deser
return parseCondition(root, jsonParser);
}

@SneakyThrows
private static Condition parseCondition(JsonNode jsonNode, JsonParser jsonParser) {
JsonParser jp = jsonNode.traverse();
jp.setCodec(jsonParser.getCodec());
if (isAndConditionsWrapper(jsonNode)) return parseAndConditionsWrapper(jsonNode, jsonParser);
if (isOrConditionsWrapper(jsonNode)) return parseOrConditionsWrapper(jsonNode, jsonParser);
if (isNotCondition(jsonNode)) return parseNotCondition(jsonNode, jsonParser);
return parseSimpleCondition(jsonNode);
try (JsonParser jp = jsonNode.traverse()) {
jp.setCodec(jsonParser.getCodec());
if (isAndConditionsWrapper(jsonNode)) return parseAndConditionsWrapper(jsonNode, jsonParser);
if (isOrConditionsWrapper(jsonNode)) return parseOrConditionsWrapper(jsonNode, jsonParser);
if (isNotCondition(jsonNode)) return parseNotCondition(jsonNode, jsonParser);
return parseSimpleCondition(jsonNode);
}
}

private static boolean isAndConditionsWrapper(JsonNode jsonNode) {
Expand Down Expand Up @@ -85,17 +88,38 @@ private static SimpleCondition parseSimpleCondition(JsonNode jsonNode) {
SimpleCondition.SimpleConditionBuilder builder = SimpleCondition.builder();
builder = setIfNotNull(jsonNode, builder, FIELD_SIMPLE_CONDITION_PROPERTY_NAME, JsonNode::asText, builder::propertyName);
builder = setIfNotNull(jsonNode, builder, FIELD_SIMPLE_CONDITION_OPERATOR, node -> Operator.valueOf(node.asText()), builder::operator);
builder = setIfNotNull(jsonNode, builder, FIELD_SIMPLE_CONDITION_VALUE, JsonNode::asText, builder::value);
if (isArray(jsonNode, FIELD_SIMPLE_CONDITION_VALUE))
builder = setArrayIfNotNull(jsonNode, builder, FIELD_SIMPLE_CONDITION_VALUE, JsonNode::asText, builder::value);
else
builder = setIfNotNull(jsonNode, builder, FIELD_SIMPLE_CONDITION_VALUE, JsonNode::asText, builder::value);
try {
return builder.build();
} catch (Exception ex) {
throw new IllegalArgumentException(ex.getMessage(), ex);
}
}

private static boolean isArray(JsonNode jsonNode, String fieldName) {
return jsonNode.hasNonNull(fieldName) && jsonNode.get(fieldName).isArray();
}

private static <T> SimpleCondition.SimpleConditionBuilder setIfNotNull(JsonNode jsonNode, SimpleCondition.SimpleConditionBuilder builder, String fieldName, Function<JsonNode, T> valueProvider, Function<T, SimpleCondition.SimpleConditionBuilder> setValueFunction) {
if (jsonNode.hasNonNull(fieldName))
builder = setValueFunction.apply(valueProvider.apply(jsonNode.get(fieldName)));
return builder;
}

@SuppressWarnings("unchecked")
private static <T> SimpleCondition.SimpleConditionBuilder setArrayIfNotNull(JsonNode jsonNode, SimpleCondition.SimpleConditionBuilder builder, String fieldName, Function<JsonNode, T> valueProvider, Function<Object, SimpleCondition.SimpleConditionBuilder> setValueFunction) {
if (jsonNode.hasNonNull(fieldName)) {
JsonNode valueNode = jsonNode.get(fieldName);
T[] values = (T[]) StreamSupport.stream(
Spliterators.spliteratorUnknownSize(valueNode.elements(), Spliterator.ORDERED),
false)
.map(valueProvider)
.toArray();
builder = setValueFunction.apply(values);
}
return builder;
}
}
Loading
Loading