@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.keyvalue.core;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.data.keyvalue.core.query.KeyValueQuery;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* {@link QueryEngine} implementation specific for executing {@link Predicate} based {@link KeyValueQuery} against
|
||||
* {@link KeyValueAdapter}.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 3.3
|
||||
*/
|
||||
class PredicateQueryEngine extends QueryEngine<KeyValueAdapter, Predicate<?>, Comparator<?>> {
|
||||
|
||||
/**
|
||||
* Creates a new {@link PredicateQueryEngine}.
|
||||
*/
|
||||
public PredicateQueryEngine() {
|
||||
this(new PathSortAccessor());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new query engine using provided {@link SortAccessor accessor} for sorting results.
|
||||
*/
|
||||
public PredicateQueryEngine(SortAccessor<Comparator<?>> sortAccessor) {
|
||||
super(new CriteriaAccessor<>() {
|
||||
@Nullable
|
||||
@Override
|
||||
public Predicate<?> resolve(KeyValueQuery<?> query) {
|
||||
return (Predicate<?>) query.getCriteria();
|
||||
}
|
||||
}, sortAccessor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<?> execute(@Nullable Predicate<?> criteria, @Nullable Comparator<?> sort, long offset, int rows,
|
||||
String keyspace) {
|
||||
return sortAndFilterMatchingRange(getRequiredAdapter().getAllOf(keyspace), criteria, sort, offset, rows);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long count(@Nullable Predicate<?> criteria, String keyspace) {
|
||||
return filterMatchingRange(IterableConverter.toList(getRequiredAdapter().getAllOf(keyspace)), criteria, -1, -1)
|
||||
.size();
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
private List<?> sortAndFilterMatchingRange(Iterable<?> source, @Nullable Predicate<?> criteria,
|
||||
@Nullable Comparator sort, long offset, int rows) {
|
||||
|
||||
List<?> tmp = IterableConverter.toList(source);
|
||||
if (sort != null) {
|
||||
tmp.sort(sort);
|
||||
}
|
||||
|
||||
return filterMatchingRange(tmp, criteria, offset, rows);
|
||||
}
|
||||
|
||||
private static <S> List<S> filterMatchingRange(List<S> source, @Nullable Predicate criteria, long offset, int rows) {
|
||||
|
||||
Stream<S> stream = source.stream();
|
||||
|
||||
if (criteria != null) {
|
||||
stream = stream.filter(criteria);
|
||||
}
|
||||
if (offset > 0) {
|
||||
stream = stream.skip(offset);
|
||||
}
|
||||
if (rows > 0) {
|
||||
stream = stream.limit(rows);
|
||||
}
|
||||
|
||||
return stream.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper;
|
||||
* @author Christoph Strobl
|
||||
* @since 3.1.10
|
||||
*/
|
||||
class SimplePropertyPathAccessor<T> {
|
||||
public class SimplePropertyPathAccessor<T> {
|
||||
|
||||
private final Object root;
|
||||
|
||||
@@ -31,7 +31,7 @@ class SimplePropertyPathAccessor<T> {
|
||||
this.root = source;
|
||||
}
|
||||
|
||||
Object getValue(PropertyPath path) {
|
||||
public Object getValue(PropertyPath path) {
|
||||
|
||||
Object currentValue = root;
|
||||
for (PropertyPath current : path) {
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
/*
|
||||
* Copyright 2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.keyvalue.repository.query;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.dao.InvalidDataAccessApiUsageException;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.keyvalue.core.SimplePropertyPathAccessor;
|
||||
import org.springframework.data.keyvalue.core.query.KeyValueQuery;
|
||||
import org.springframework.data.mapping.PropertyPath;
|
||||
import org.springframework.data.repository.query.ParameterAccessor;
|
||||
import org.springframework.data.repository.query.parser.AbstractQueryCreator;
|
||||
import org.springframework.data.repository.query.parser.Part;
|
||||
import org.springframework.data.repository.query.parser.Part.IgnoreCaseType;
|
||||
import org.springframework.data.repository.query.parser.PartTree;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.comparator.NullSafeComparator;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @since 3.3
|
||||
*/
|
||||
public class PredicateQueryCreator extends AbstractQueryCreator<KeyValueQuery<Predicate<?>>, Predicate<?>> {
|
||||
|
||||
public PredicateQueryCreator(PartTree tree, ParameterAccessor parameters) {
|
||||
super(tree, parameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Predicate<?> create(Part part, Iterator<Object> iterator) {
|
||||
|
||||
switch (part.getType()) {
|
||||
case TRUE:
|
||||
return PredicateBuilder.propertyValueOf(part).isTrue();
|
||||
case FALSE:
|
||||
return PredicateBuilder.propertyValueOf(part).isFalse();
|
||||
case SIMPLE_PROPERTY:
|
||||
return PredicateBuilder.propertyValueOf(part).isEqualTo(iterator.next());
|
||||
case IS_NULL:
|
||||
return PredicateBuilder.propertyValueOf(part).isNull();
|
||||
case IS_NOT_NULL:
|
||||
return PredicateBuilder.propertyValueOf(part).isNotNull();
|
||||
case LIKE:
|
||||
return PredicateBuilder.propertyValueOf(part).contains(iterator.next());
|
||||
case STARTING_WITH:
|
||||
return PredicateBuilder.propertyValueOf(part).startsWith(iterator.next());
|
||||
case AFTER:
|
||||
case GREATER_THAN:
|
||||
return PredicateBuilder.propertyValueOf(part).isGreaterThan(iterator.next());
|
||||
case GREATER_THAN_EQUAL:
|
||||
return PredicateBuilder.propertyValueOf(part).isGreaterThanEqual(iterator.next());
|
||||
case BEFORE:
|
||||
case LESS_THAN:
|
||||
return PredicateBuilder.propertyValueOf(part).isLessThan(iterator.next());
|
||||
case LESS_THAN_EQUAL:
|
||||
return PredicateBuilder.propertyValueOf(part).isLessThanEqual(iterator.next());
|
||||
case ENDING_WITH:
|
||||
return PredicateBuilder.propertyValueOf(part).endsWith(iterator.next());
|
||||
case BETWEEN:
|
||||
return PredicateBuilder.propertyValueOf(part).isGreaterThan(iterator.next())
|
||||
.and(PredicateBuilder.propertyValueOf(part).isLessThan(iterator.next()));
|
||||
case REGEX:
|
||||
return PredicateBuilder.propertyValueOf(part).matches(iterator.next());
|
||||
case IN:
|
||||
return PredicateBuilder.propertyValueOf(part).in(iterator.next());
|
||||
default:
|
||||
throw new InvalidDataAccessApiUsageException(String.format("Found invalid part '%s' in query", part.getType()));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Predicate<?> and(Part part, Predicate<?> base, Iterator<Object> iterator) {
|
||||
return base.and((Predicate) create(part, iterator));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Predicate<?> or(Predicate<?> base, Predicate<?> criteria) {
|
||||
return base.or((Predicate) criteria);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected KeyValueQuery<Predicate<?>> complete(@Nullable Predicate<?> criteria, Sort sort) {
|
||||
if (criteria == null) {
|
||||
return new KeyValueQuery<>(it -> true, sort);
|
||||
}
|
||||
return new KeyValueQuery<>(criteria, sort);
|
||||
}
|
||||
|
||||
static class PredicateBuilder {
|
||||
|
||||
private final Part part;
|
||||
|
||||
public PredicateBuilder(Part part) {
|
||||
this.part = part;
|
||||
}
|
||||
|
||||
static PredicateBuilder propertyValueOf(Part part) {
|
||||
return new PredicateBuilder(part);
|
||||
}
|
||||
|
||||
public Predicate<Object> isTrue() {
|
||||
return new ValueComparingPredicate(part.getProperty(), true);
|
||||
}
|
||||
|
||||
public Predicate<Object> isFalse() {
|
||||
return new ValueComparingPredicate(part.getProperty(), false);
|
||||
}
|
||||
|
||||
public Predicate<Object> isEqualTo(Object value) {
|
||||
return new ValueComparingPredicate(part.getProperty(), o -> {
|
||||
|
||||
if (!ObjectUtils.nullSafeEquals(IgnoreCaseType.NEVER, part.shouldIgnoreCase())) {
|
||||
if (o instanceof String s1 && value instanceof String s2) {
|
||||
return s1.equalsIgnoreCase(s2);
|
||||
}
|
||||
}
|
||||
return ObjectUtils.nullSafeEquals(o, value);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
public Predicate<Object> isNull() {
|
||||
return new ValueComparingPredicate(part.getProperty(), Objects::isNull);
|
||||
}
|
||||
|
||||
public Predicate<Object> isNotNull() {
|
||||
return isNull().negate();
|
||||
}
|
||||
|
||||
public Predicate<Object> isLessThan(Object value) {
|
||||
return new ValueComparingPredicate(part.getProperty(),
|
||||
o -> NullSafeComparator.NULLS_HIGH.compare(o, value) == -1 ? true : false);
|
||||
}
|
||||
|
||||
public Predicate<Object> isLessThanEqual(Object value) {
|
||||
return new ValueComparingPredicate(part.getProperty(),
|
||||
o -> NullSafeComparator.NULLS_HIGH.compare(o, value) <= 0 ? true : false);
|
||||
}
|
||||
|
||||
public Predicate<Object> isGreaterThan(Object value) {
|
||||
return new ValueComparingPredicate(part.getProperty(),
|
||||
o -> NullSafeComparator.NULLS_HIGH.compare(o, value) == 1 ? true : false);
|
||||
}
|
||||
|
||||
public Predicate<Object> isGreaterThanEqual(Object value) {
|
||||
return new ValueComparingPredicate(part.getProperty(),
|
||||
o -> NullSafeComparator.NULLS_HIGH.compare(o, value) >= 0 ? true : false);
|
||||
}
|
||||
|
||||
public Predicate<Object> matches(Pattern pattern) {
|
||||
|
||||
return new ValueComparingPredicate(part.getProperty(), o -> {
|
||||
if (o == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return pattern.matcher(o.toString()).find();
|
||||
});
|
||||
}
|
||||
|
||||
public Predicate<Object> matches(Object value) {
|
||||
return new ValueComparingPredicate(part.getProperty(), o -> {
|
||||
|
||||
if (o == null || value == null) {
|
||||
return ObjectUtils.nullSafeEquals(o, value);
|
||||
}
|
||||
|
||||
if (value instanceof Pattern pattern) {
|
||||
return pattern.matcher(o.toString()).find();
|
||||
}
|
||||
|
||||
return o.toString().matches(value.toString());
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
public Predicate<Object> matches(String regex) {
|
||||
return matches(Pattern.compile(regex));
|
||||
}
|
||||
|
||||
public Predicate<Object> in(Object value) {
|
||||
return new ValueComparingPredicate(part.getProperty(), o -> {
|
||||
|
||||
if (value instanceof Collection<?> collection) {
|
||||
|
||||
if (o instanceof Collection<?> subSet) {
|
||||
collection.containsAll(subSet);
|
||||
}
|
||||
return collection.contains(o);
|
||||
}
|
||||
if (ObjectUtils.isArray(value)) {
|
||||
return ObjectUtils.containsElement(ObjectUtils.toObjectArray(value), value);
|
||||
}
|
||||
return false;
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
public Predicate<Object> contains(Object value) {
|
||||
|
||||
return new ValueComparingPredicate(part.getProperty(), o -> {
|
||||
|
||||
if (o == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (o instanceof Collection<?> collection) {
|
||||
return collection.contains(value);
|
||||
}
|
||||
|
||||
if (ObjectUtils.isArray(o)) {
|
||||
return ObjectUtils.containsElement(ObjectUtils.toObjectArray(o), value);
|
||||
}
|
||||
|
||||
if (o instanceof Map<?, ?> map) {
|
||||
return map.values().contains(value);
|
||||
}
|
||||
|
||||
if (value == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String s = o.toString();
|
||||
|
||||
if (ObjectUtils.nullSafeEquals(IgnoreCaseType.NEVER, part.shouldIgnoreCase())) {
|
||||
return s.contains(value.toString());
|
||||
}
|
||||
return s.toLowerCase().contains(value.toString().toLowerCase());
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
public Predicate<Object> startsWith(Object value) {
|
||||
return new ValueComparingPredicate(part.getProperty(), o -> {
|
||||
|
||||
if (!(o instanceof String s)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ObjectUtils.nullSafeEquals(IgnoreCaseType.NEVER, part.shouldIgnoreCase())) {
|
||||
return s.startsWith(value.toString());
|
||||
}
|
||||
|
||||
return s.toLowerCase().startsWith(value.toString().toLowerCase());
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public Predicate<Object> endsWith(Object value) {
|
||||
return new ValueComparingPredicate(part.getProperty(), o -> {
|
||||
|
||||
if (!(o instanceof String s)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ObjectUtils.nullSafeEquals(IgnoreCaseType.NEVER, part.shouldIgnoreCase())) {
|
||||
return s.endsWith(value.toString());
|
||||
}
|
||||
|
||||
return s.toLowerCase().endsWith(value.toString().toLowerCase());
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
static class ValueComparingPredicate implements Predicate<Object> {
|
||||
|
||||
private final PropertyPath path;
|
||||
private final Function<Object, Boolean> check;
|
||||
|
||||
public ValueComparingPredicate(PropertyPath path, Object expected) {
|
||||
this(path, (value) -> ObjectUtils.nullSafeEquals(value, expected));
|
||||
}
|
||||
|
||||
public ValueComparingPredicate(PropertyPath path, Function<Object, Boolean> check) {
|
||||
this.path = path;
|
||||
this.check = check;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean test(Object o) {
|
||||
Object value = new SimplePropertyPathAccessor<>(o).getValue(path);
|
||||
return check.apply(value);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright 2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.keyvalue.core;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.keyvalue.repository.query.PredicateQueryCreator;
|
||||
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
|
||||
import org.springframework.data.repository.core.RepositoryMetadata;
|
||||
import org.springframework.data.repository.query.ParametersParameterAccessor;
|
||||
import org.springframework.data.repository.query.QueryMethod;
|
||||
import org.springframework.data.repository.query.parser.PartTree;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link SpelQueryEngine}.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class PredicateQueryEngineUnitTests {
|
||||
|
||||
private static final Person BOB_WITH_FIRSTNAME = new Person("bob", 30);
|
||||
private static final Person MIKE_WITHOUT_FIRSTNAME = new Person(null, 25);
|
||||
|
||||
@Mock KeyValueAdapter adapter;
|
||||
|
||||
private PredicateQueryEngine engine;
|
||||
|
||||
private Iterable<Person> people = Arrays.asList(BOB_WITH_FIRSTNAME, MIKE_WITHOUT_FIRSTNAME);
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
|
||||
engine = new PredicateQueryEngine();
|
||||
engine.registerAdapter(adapter);
|
||||
}
|
||||
|
||||
@Test // DATAKV-114
|
||||
@SuppressWarnings("unchecked")
|
||||
void queriesEntitiesWithNullProperty() throws Exception {
|
||||
|
||||
doReturn(people).when(adapter).getAllOf(anyString());
|
||||
|
||||
Collection result = engine.execute(createQueryForMethodWithArgs("findByFirstname", "bob"), null, -1, -1,
|
||||
anyString());
|
||||
assertThat(result).containsExactly(BOB_WITH_FIRSTNAME);
|
||||
}
|
||||
|
||||
@Test // DATAKV-114
|
||||
void countsEntitiesWithNullProperty() throws Exception {
|
||||
|
||||
doReturn(people).when(adapter).getAllOf(anyString());
|
||||
|
||||
assertThat(engine.count(createQueryForMethodWithArgs("findByFirstname", "bob"), anyString())).isEqualTo(1L);
|
||||
}
|
||||
|
||||
private static Predicate<?> createQueryForMethodWithArgs(String methodName, Object... args) throws Exception {
|
||||
|
||||
List<Class<?>> types = new ArrayList<>(args.length);
|
||||
|
||||
for (Object arg : args) {
|
||||
types.add(arg.getClass());
|
||||
}
|
||||
|
||||
Method method = PersonRepository.class.getMethod(methodName, types.toArray(new Class<?>[types.size()]));
|
||||
RepositoryMetadata metadata = mock(RepositoryMetadata.class);
|
||||
doReturn(method.getReturnType()).when(metadata).getReturnedDomainClass(method);
|
||||
doReturn(TypeInformation.fromReturnTypeOf(method)).when(metadata).getReturnType(method);
|
||||
doReturn(TypeInformation.of(Person.class)).when(metadata).getDomainTypeInformation();
|
||||
|
||||
PartTree partTree = new PartTree(method.getName(), method.getReturnType());
|
||||
PredicateQueryCreator creator = new PredicateQueryCreator(partTree, new ParametersParameterAccessor(
|
||||
new QueryMethod(method, metadata, new SpelAwareProxyProjectionFactory()).getParameters(), args));
|
||||
|
||||
return creator.createQuery().getCriteria();
|
||||
}
|
||||
|
||||
interface PersonRepository {
|
||||
Person findByFirstname(String firstname);
|
||||
}
|
||||
|
||||
public static class Person {
|
||||
|
||||
@Id String id;
|
||||
String firstname;
|
||||
int age;
|
||||
|
||||
Person(String firstname, int age) {
|
||||
|
||||
this.firstname = firstname;
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
public String getFirstname() {
|
||||
return firstname;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,507 @@
|
||||
/*
|
||||
* Copyright 2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.keyvalue.repository.query;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.dao.InvalidDataAccessApiUsageException;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.keyvalue.core.query.KeyValueQuery;
|
||||
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.data.repository.core.RepositoryMetadata;
|
||||
import org.springframework.data.repository.query.ParametersParameterAccessor;
|
||||
import org.springframework.data.repository.query.QueryMethod;
|
||||
import org.springframework.data.repository.query.parser.AbstractQueryCreator;
|
||||
import org.springframework.data.repository.query.parser.PartTree;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public abstract class AbstractQueryCreatorTestBase<QUERY_CREATOR extends AbstractQueryCreator<KeyValueQuery<CRITERIA>, ?>, CRITERIA> {
|
||||
|
||||
static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_ZONED_DATE_TIME;
|
||||
|
||||
static final Person RICKON = new Person("rickon", 4);
|
||||
static final Person BRAN = new Person("bran", 9)//
|
||||
.skinChanger(true).bornAt(Date.from(ZonedDateTime.parse("2013-01-31T06:00:00Z", FORMATTER).toInstant()));
|
||||
static final Person ARYA = new Person("arya", 13);
|
||||
static final Person ROBB = new Person("robb", 16)//
|
||||
.named("stark").bornAt(Date.from(ZonedDateTime.parse("2010-09-20T06:00:00Z", FORMATTER).toInstant()));
|
||||
static final Person JON = new Person("jon", 17).named("snow");
|
||||
|
||||
@Mock RepositoryMetadata metadataMock;
|
||||
|
||||
@Test
|
||||
// DATACMNS-525
|
||||
void equalsReturnsTrueWhenMatching() {
|
||||
assertThat(evaluate("findByFirstname", BRAN.firstname).against(BRAN)).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void equalsReturnsFalseWhenNotMatching() {
|
||||
assertThat(evaluate("findByFirstname", BRAN.firstname).against(RICKON)).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void isTrueAssertedProperlyWhenTrue() {
|
||||
assertThat(evaluate("findBySkinChangerIsTrue").against(BRAN)).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void isTrueAssertedProperlyWhenFalse() {
|
||||
assertThat(evaluate("findBySkinChangerIsTrue").against(RICKON)).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void isFalseAssertedProperlyWhenTrue() {
|
||||
assertThat(evaluate("findBySkinChangerIsFalse").against(BRAN)).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void isFalseAssertedProperlyWhenFalse() {
|
||||
assertThat(evaluate("findBySkinChangerIsFalse").against(RICKON)).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void isNullAssertedProperlyWhenAttributeIsNull() {
|
||||
assertThat(evaluate("findByLastnameIsNull").against(BRAN)).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void isNullAssertedProperlyWhenAttributeIsNotNull() {
|
||||
assertThat(evaluate("findByLastnameIsNull").against(ROBB)).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void isNotNullFalseTrueWhenAttributeIsNull() {
|
||||
assertThat(evaluate("findByLastnameIsNotNull").against(BRAN)).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void isNotNullReturnsTrueAttributeIsNotNull() {
|
||||
assertThat(evaluate("findByLastnameIsNotNull").against(ROBB)).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void startsWithReturnsTrueWhenMatching() {
|
||||
assertThat(evaluate("findByFirstnameStartingWith", "r").against(ROBB)).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void startsWithReturnsFalseWhenNotMatching() {
|
||||
assertThat(evaluate("findByFirstnameStartingWith", "r").against(BRAN)).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void likeReturnsTrueWhenMatching() {
|
||||
assertThat(evaluate("findByFirstnameLike", "ob").against(ROBB)).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void likeReturnsFalseWhenNotMatching() {
|
||||
assertThat(evaluate("findByFirstnameLike", "ra").against(ROBB)).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void endsWithReturnsTrueWhenMatching() {
|
||||
assertThat(evaluate("findByFirstnameEndingWith", "bb").against(ROBB)).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void endsWithReturnsFalseWhenNotMatching() {
|
||||
assertThat(evaluate("findByFirstnameEndingWith", "an").against(ROBB)).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void startsWithIgnoreCaseReturnsTrueWhenMatching() {
|
||||
assertThatExceptionOfType(InvalidDataAccessApiUsageException.class)
|
||||
.isThrownBy(() -> evaluate("findByFirstnameIgnoreCase", "R").against(ROBB));
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void greaterThanReturnsTrueForHigherValues() {
|
||||
assertThat(evaluate("findByAgeGreaterThan", BRAN.age).against(ROBB)).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void greaterThanReturnsFalseForLowerValues() {
|
||||
assertThat(evaluate("findByAgeGreaterThan", BRAN.age).against(RICKON)).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void afterReturnsTrueForHigherValues() {
|
||||
assertThat(evaluate("findByBirthdayAfter", ROBB.birthday).against(BRAN)).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void afterReturnsFalseForLowerValues() {
|
||||
assertThat(evaluate("findByBirthdayAfter", BRAN.birthday).against(ROBB)).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void greaterThanEaualsReturnsTrueForHigherValues() {
|
||||
assertThat(evaluate("findByAgeGreaterThanEqual", BRAN.age).against(ROBB)).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void greaterThanEqualsReturnsTrueForEqualValues() {
|
||||
assertThat(evaluate("findByAgeGreaterThanEqual", BRAN.age).against(BRAN)).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void greaterThanEqualsReturnsFalseForLowerValues() {
|
||||
assertThat(evaluate("findByAgeGreaterThanEqual", BRAN.age).against(RICKON)).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void lessThanReturnsTrueForHigherValues() {
|
||||
assertThat(evaluate("findByAgeLessThan", BRAN.age).against(ROBB)).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void lessThanReturnsFalseForLowerValues() {
|
||||
assertThat(evaluate("findByAgeLessThan", BRAN.age).against(RICKON)).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void beforeReturnsTrueForLowerValues() {
|
||||
assertThat(evaluate("findByBirthdayBefore", BRAN.birthday).against(ROBB)).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void beforeReturnsFalseForHigherValues() {
|
||||
assertThat(evaluate("findByBirthdayBefore", ROBB.birthday).against(BRAN)).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void lessThanEaualsReturnsTrueForHigherValues() {
|
||||
assertThat(evaluate("findByAgeLessThanEqual", BRAN.age).against(ROBB)).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void lessThanEaualsReturnsTrueForEqualValues() {
|
||||
assertThat(evaluate("findByAgeLessThanEqual", BRAN.age).against(BRAN)).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void lessThanEqualsReturnsFalseForLowerValues() {
|
||||
assertThat(evaluate("findByAgeLessThanEqual", BRAN.age).against(RICKON)).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void betweenEqualsReturnsTrueForValuesInBetween() {
|
||||
assertThat(evaluate("findByAgeBetween", BRAN.age, ROBB.age).against(ARYA)).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void betweenEqualsReturnsFalseForHigherValues() {
|
||||
assertThat(evaluate("findByAgeBetween", BRAN.age, ROBB.age).against(JON)).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void betweenEqualsReturnsFalseForLowerValues() {
|
||||
assertThat(evaluate("findByAgeBetween", BRAN.age, ROBB.age).against(RICKON)).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void connectByAndReturnsTrueWhenAllPropertiesMatching() {
|
||||
assertThat(evaluate("findByAgeGreaterThanAndLastname", BRAN.age, JON.lastname).against(JON)).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void connectByAndReturnsFalseWhenOnlyFewPropertiesMatch() {
|
||||
assertThat(evaluate("findByAgeGreaterThanAndLastname", BRAN.age, JON.lastname).against(ROBB)).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void connectByOrReturnsTrueWhenOnlyFewPropertiesMatch() {
|
||||
assertThat(evaluate("findByAgeGreaterThanOrLastname", BRAN.age, JON.lastname).against(ROBB)).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void connectByOrReturnsTrueWhenAllPropertiesMatch() {
|
||||
assertThat(evaluate("findByAgeGreaterThanOrLastname", BRAN.age, JON.lastname).against(JON)).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void regexReturnsTrueWhenMatching() {
|
||||
assertThat(evaluate("findByLastnameMatches", "^s.*w$").against(JON)).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void regexReturnsFalseWhenNotMatching() {
|
||||
assertThat(evaluate("findByLastnameMatches", "^s.*w$").against(ROBB)).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATAKV-169
|
||||
void inReturnsMatchCorrectly() {
|
||||
|
||||
ArrayList<String> list = new ArrayList<>();
|
||||
list.add(ROBB.firstname);
|
||||
|
||||
assertThat(evaluate("findByFirstnameIn", list).against(ROBB)).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATAKV-169
|
||||
void inNotMatchingReturnsCorrectly() {
|
||||
|
||||
ArrayList<String> list = new ArrayList<>();
|
||||
list.add(ROBB.firstname);
|
||||
|
||||
assertThat(evaluate("findByFirstnameIn", list).against(JON)).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATAKV-169
|
||||
void inWithNullCompareValuesCorrectly() {
|
||||
|
||||
ArrayList<String> list = new ArrayList<>();
|
||||
list.add(null);
|
||||
|
||||
assertThat(evaluate("findByFirstnameIn", list).against(JON)).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATAKV-169
|
||||
void inWithNullSourceValuesMatchesCorrectly() {
|
||||
|
||||
ArrayList<String> list = new ArrayList<>();
|
||||
list.add(ROBB.firstname);
|
||||
|
||||
assertThat(evaluate("findByFirstnameIn", list).against(new PredicateQueryCreatorUnitTests.Person(null, 10)))
|
||||
.isFalse();
|
||||
}
|
||||
|
||||
@Test // DATAKV-169
|
||||
void inMatchesNullValuesCorrectly() {
|
||||
|
||||
ArrayList<String> list = new ArrayList<>();
|
||||
list.add(null);
|
||||
|
||||
boolean contains = list.contains(null);
|
||||
|
||||
assertThat(evaluate("findByFirstnameIn", list).against(new PredicateQueryCreatorUnitTests.Person(null, 10)))
|
||||
.isTrue();
|
||||
}
|
||||
|
||||
@Test // DATAKV-185
|
||||
void noDerivedQueryArgumentsMatchesAlways() {
|
||||
|
||||
assertThat(evaluate("findBy").against(JON)).isTrue();
|
||||
assertThat(evaluate("findBy").against(null)).isTrue();
|
||||
}
|
||||
|
||||
protected Evaluation evaluate(String methodName, Object... args) {
|
||||
try {
|
||||
return createEvaluation(createQueryForMethodWithArgs(methodName, args).getCriteria());
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract Evaluation createEvaluation(CRITERIA criteria);
|
||||
|
||||
protected KeyValueQuery<CRITERIA> createQueryForMethodWithArgs(String methodName, Object... args)
|
||||
throws NoSuchMethodException, SecurityException {
|
||||
|
||||
Class<?>[] argTypes = new Class<?>[args.length];
|
||||
if (!ObjectUtils.isEmpty(args)) {
|
||||
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
argTypes[i] = args[i].getClass();
|
||||
}
|
||||
}
|
||||
|
||||
Method method = PersonRepository.class.getMethod(methodName, argTypes);
|
||||
doReturn(Person.class).when(metadataMock).getReturnedDomainClass(method);
|
||||
doReturn(TypeInformation.of(Person.class)).when(metadataMock).getDomainTypeInformation();
|
||||
doReturn(TypeInformation.of(Person.class)).when(metadataMock).getReturnType(method);
|
||||
|
||||
PartTree partTree = new PartTree(method.getName(), method.getReturnType());
|
||||
QUERY_CREATOR creator = queryCreator(partTree, new ParametersParameterAccessor(
|
||||
new QueryMethod(method, metadataMock, new SpelAwareProxyProjectionFactory()).getParameters(), args));
|
||||
|
||||
KeyValueQuery<CRITERIA> q = creator.createQuery();
|
||||
return finalizeQuery(q, args);
|
||||
}
|
||||
|
||||
protected abstract QUERY_CREATOR queryCreator(PartTree partTree, ParametersParameterAccessor accessor);
|
||||
|
||||
protected abstract KeyValueQuery<CRITERIA> finalizeQuery(KeyValueQuery<CRITERIA> query, Object... args);
|
||||
|
||||
interface PersonRepository extends CrudRepository<Person, String> {
|
||||
|
||||
// No arguments
|
||||
Person findBy();
|
||||
|
||||
// Type.SIMPLE_PROPERTY
|
||||
Person findByFirstname(String firstname);
|
||||
|
||||
// Type.TRUE
|
||||
Person findBySkinChangerIsTrue();
|
||||
|
||||
// Type.FALSE
|
||||
Person findBySkinChangerIsFalse();
|
||||
|
||||
// Type.IS_NULL
|
||||
Person findByLastnameIsNull();
|
||||
|
||||
// Type.IS_NOT_NULL
|
||||
Person findByLastnameIsNotNull();
|
||||
|
||||
// Type.STARTING_WITH
|
||||
Person findByFirstnameStartingWith(String firstanme);
|
||||
|
||||
Person findByFirstnameIgnoreCase(String firstanme);
|
||||
|
||||
// Type.AFTER
|
||||
Person findByBirthdayAfter(Date date);
|
||||
|
||||
// Type.GREATHER_THAN
|
||||
Person findByAgeGreaterThan(Integer age);
|
||||
|
||||
// Type.GREATER_THAN_EQUAL
|
||||
Person findByAgeGreaterThanEqual(Integer age);
|
||||
|
||||
// Type.BEFORE
|
||||
Person findByBirthdayBefore(Date date);
|
||||
|
||||
// Type.LESS_THAN
|
||||
Person findByAgeLessThan(Integer age);
|
||||
|
||||
// Type.LESS_THAN_EQUAL
|
||||
Person findByAgeLessThanEqual(Integer age);
|
||||
|
||||
// Type.BETWEEN
|
||||
Person findByAgeBetween(Integer low, Integer high);
|
||||
|
||||
// Type.LIKE
|
||||
Person findByFirstnameLike(String firstname);
|
||||
|
||||
// Type.ENDING_WITH
|
||||
Person findByFirstnameEndingWith(String firstname);
|
||||
|
||||
Person findByAgeGreaterThanAndLastname(Integer age, String lastname);
|
||||
|
||||
Person findByAgeGreaterThanOrLastname(Integer age, String lastname);
|
||||
|
||||
// Type.REGEX
|
||||
Person findByLastnameMatches(String lastname);
|
||||
|
||||
// Type.IN
|
||||
Person findByFirstnameIn(ArrayList<String> in);
|
||||
|
||||
}
|
||||
|
||||
public interface Evaluation {
|
||||
Boolean against(Object candidate);
|
||||
|
||||
boolean evaluate();
|
||||
}
|
||||
|
||||
public static class Person {
|
||||
|
||||
private @Id String id;
|
||||
private String firstname, lastname;
|
||||
private int age;
|
||||
private boolean isSkinChanger = false;
|
||||
private Date birthday;
|
||||
|
||||
public Person() {}
|
||||
|
||||
Person(String firstname, int age) {
|
||||
super();
|
||||
this.firstname = firstname;
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
Person skinChanger(boolean isSkinChanger) {
|
||||
this.isSkinChanger = isSkinChanger;
|
||||
return this;
|
||||
}
|
||||
|
||||
Person named(String lastname) {
|
||||
this.lastname = lastname;
|
||||
return this;
|
||||
}
|
||||
|
||||
Person bornAt(Date date) {
|
||||
this.birthday = date;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public String getFirstname() {
|
||||
return this.firstname;
|
||||
}
|
||||
|
||||
public String getLastname() {
|
||||
return this.lastname;
|
||||
}
|
||||
|
||||
public int getAge() {
|
||||
return this.age;
|
||||
}
|
||||
|
||||
public boolean isSkinChanger() {
|
||||
return this.isSkinChanger;
|
||||
}
|
||||
|
||||
public Date getBirthday() {
|
||||
return this.birthday;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public void setFirstname(String firstname) {
|
||||
this.firstname = firstname;
|
||||
}
|
||||
|
||||
public void setLastname(String lastname) {
|
||||
this.lastname = lastname;
|
||||
}
|
||||
|
||||
public void setAge(int age) {
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
public void setSkinChanger(boolean isSkinChanger) {
|
||||
this.isSkinChanger = isSkinChanger;
|
||||
}
|
||||
|
||||
public void setBirthday(Date birthday) {
|
||||
this.birthday = birthday;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.keyvalue.repository.query;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.dao.InvalidDataAccessApiUsageException;
|
||||
import org.springframework.data.keyvalue.core.query.KeyValueQuery;
|
||||
import org.springframework.data.repository.query.ParametersParameterAccessor;
|
||||
import org.springframework.data.repository.query.parser.PartTree;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
class PredicateQueryCreatorUnitTests extends AbstractQueryCreatorTestBase<PredicateQueryCreator, Predicate<?>> {
|
||||
|
||||
@Override
|
||||
@Test // DATACMNS-525
|
||||
void startsWithIgnoreCaseReturnsTrueWhenMatching() {
|
||||
assertThat(evaluate("findByFirstnameIgnoreCase", "RobB").against(ROBB)).isTrue();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PredicateQueryCreator queryCreator(PartTree partTree, ParametersParameterAccessor accessor) {
|
||||
return new PredicateQueryCreator(partTree, accessor);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected KeyValueQuery<Predicate<?>> finalizeQuery(KeyValueQuery<Predicate<?>> query, Object... args) {
|
||||
return query;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Evaluation createEvaluation(Predicate<?> predicate) {
|
||||
return new PredicateEvaluation(predicate);
|
||||
}
|
||||
|
||||
static class PredicateEvaluation implements Evaluation {
|
||||
|
||||
private final Predicate expression;
|
||||
private Object candidate;
|
||||
|
||||
PredicateEvaluation(Predicate<?> expression) {
|
||||
this.expression = expression;
|
||||
}
|
||||
|
||||
public Boolean against(Object candidate) {
|
||||
this.candidate = candidate;
|
||||
return evaluate();
|
||||
}
|
||||
|
||||
public boolean evaluate() {
|
||||
return expression.test(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,502 +15,53 @@
|
||||
*/
|
||||
package org.springframework.data.keyvalue.repository.query;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.dao.InvalidDataAccessApiUsageException;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.keyvalue.core.query.KeyValueQuery;
|
||||
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.data.repository.core.RepositoryMetadata;
|
||||
import org.springframework.data.repository.query.ParametersParameterAccessor;
|
||||
import org.springframework.data.repository.query.QueryMethod;
|
||||
import org.springframework.data.repository.query.parser.PartTree;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.expression.spel.standard.SpelExpression;
|
||||
import org.springframework.expression.spel.support.SimpleEvaluationContext;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class SpelQueryCreatorUnitTests {
|
||||
public class SpelQueryCreatorUnitTests extends AbstractQueryCreatorTestBase<SpelQueryCreator, SpelExpression> {
|
||||
|
||||
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_ZONED_DATE_TIME;
|
||||
|
||||
private static final Person RICKON = new Person("rickon", 4);
|
||||
private static final Person BRAN = new Person("bran", 9)//
|
||||
.skinChanger(true).bornAt(Date.from(ZonedDateTime.parse("2013-01-31T06:00:00Z", FORMATTER).toInstant()));
|
||||
private static final Person ARYA = new Person("arya", 13);
|
||||
private static final Person ROBB = new Person("robb", 16)//
|
||||
.named("stark").bornAt(Date.from(ZonedDateTime.parse("2010-09-20T06:00:00Z", FORMATTER).toInstant()));
|
||||
private static final Person JON = new Person("jon", 17).named("snow");
|
||||
|
||||
@Mock RepositoryMetadata metadataMock;
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void equalsReturnsTrueWhenMatching() {
|
||||
assertThat(evaluate("findByFirstname", BRAN.firstname).against(BRAN)).isTrue();
|
||||
@Override
|
||||
protected SpelQueryCreator queryCreator(PartTree partTree, ParametersParameterAccessor accessor) {
|
||||
return new SpelQueryCreator(partTree, accessor);
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void equalsReturnsFalseWhenNotMatching() {
|
||||
assertThat(evaluate("findByFirstname", BRAN.firstname).against(RICKON)).isFalse();
|
||||
}
|
||||
@Override
|
||||
protected KeyValueQuery<SpelExpression> finalizeQuery(KeyValueQuery<SpelExpression> query, Object... args) {
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void isTrueAssertedProperlyWhenTrue() {
|
||||
assertThat(evaluate("findBySkinChangerIsTrue").against(BRAN)).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void isTrueAssertedProperlyWhenFalse() {
|
||||
assertThat(evaluate("findBySkinChangerIsTrue").against(RICKON)).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void isFalseAssertedProperlyWhenTrue() {
|
||||
assertThat(evaluate("findBySkinChangerIsFalse").against(BRAN)).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void isFalseAssertedProperlyWhenFalse() {
|
||||
assertThat(evaluate("findBySkinChangerIsFalse").against(RICKON)).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void isNullAssertedProperlyWhenAttributeIsNull() {
|
||||
assertThat(evaluate("findByLastnameIsNull").against(BRAN)).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void isNullAssertedProperlyWhenAttributeIsNotNull() {
|
||||
assertThat(evaluate("findByLastnameIsNull").against(ROBB)).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void isNotNullFalseTrueWhenAttributeIsNull() {
|
||||
assertThat(evaluate("findByLastnameIsNotNull").against(BRAN)).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void isNotNullReturnsTrueAttributeIsNotNull() {
|
||||
assertThat(evaluate("findByLastnameIsNotNull").against(ROBB)).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void startsWithReturnsTrueWhenMatching() {
|
||||
assertThat(evaluate("findByFirstnameStartingWith", "r").against(ROBB)).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void startsWithReturnsFalseWhenNotMatching() {
|
||||
assertThat(evaluate("findByFirstnameStartingWith", "r").against(BRAN)).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void likeReturnsTrueWhenMatching() {
|
||||
assertThat(evaluate("findByFirstnameLike", "ob").against(ROBB)).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void likeReturnsFalseWhenNotMatching() {
|
||||
assertThat(evaluate("findByFirstnameLike", "ra").against(ROBB)).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void endsWithReturnsTrueWhenMatching() {
|
||||
assertThat(evaluate("findByFirstnameEndingWith", "bb").against(ROBB)).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void endsWithReturnsFalseWhenNotMatching() {
|
||||
assertThat(evaluate("findByFirstnameEndingWith", "an").against(ROBB)).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void startsWithIgnoreCaseReturnsTrueWhenMatching() {
|
||||
assertThatExceptionOfType(InvalidDataAccessApiUsageException.class)
|
||||
.isThrownBy(() -> evaluate("findByFirstnameIgnoreCase", "R").against(ROBB));
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void greaterThanReturnsTrueForHigherValues() {
|
||||
assertThat(evaluate("findByAgeGreaterThan", BRAN.age).against(ROBB)).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void greaterThanReturnsFalseForLowerValues() {
|
||||
assertThat(evaluate("findByAgeGreaterThan", BRAN.age).against(RICKON)).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void afterReturnsTrueForHigherValues() {
|
||||
assertThat(evaluate("findByBirthdayAfter", ROBB.birthday).against(BRAN)).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void afterReturnsFalseForLowerValues() {
|
||||
assertThat(evaluate("findByBirthdayAfter", BRAN.birthday).against(ROBB)).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void greaterThanEaualsReturnsTrueForHigherValues() {
|
||||
assertThat(evaluate("findByAgeGreaterThanEqual", BRAN.age).against(ROBB)).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void greaterThanEqualsReturnsTrueForEqualValues() {
|
||||
assertThat(evaluate("findByAgeGreaterThanEqual", BRAN.age).against(BRAN)).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void greaterThanEqualsReturnsFalseForLowerValues() {
|
||||
assertThat(evaluate("findByAgeGreaterThanEqual", BRAN.age).against(RICKON)).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void lessThanReturnsTrueForHigherValues() {
|
||||
assertThat(evaluate("findByAgeLessThan", BRAN.age).against(ROBB)).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void lessThanReturnsFalseForLowerValues() {
|
||||
assertThat(evaluate("findByAgeLessThan", BRAN.age).against(RICKON)).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void beforeReturnsTrueForLowerValues() {
|
||||
assertThat(evaluate("findByBirthdayBefore", BRAN.birthday).against(ROBB)).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void beforeReturnsFalseForHigherValues() {
|
||||
assertThat(evaluate("findByBirthdayBefore", ROBB.birthday).against(BRAN)).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void lessThanEaualsReturnsTrueForHigherValues() {
|
||||
assertThat(evaluate("findByAgeLessThanEqual", BRAN.age).against(ROBB)).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void lessThanEaualsReturnsTrueForEqualValues() {
|
||||
assertThat(evaluate("findByAgeLessThanEqual", BRAN.age).against(BRAN)).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void lessThanEqualsReturnsFalseForLowerValues() {
|
||||
assertThat(evaluate("findByAgeLessThanEqual", BRAN.age).against(RICKON)).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void betweenEqualsReturnsTrueForValuesInBetween() {
|
||||
assertThat(evaluate("findByAgeBetween", BRAN.age, ROBB.age).against(ARYA)).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void betweenEqualsReturnsFalseForHigherValues() {
|
||||
assertThat(evaluate("findByAgeBetween", BRAN.age, ROBB.age).against(JON)).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void betweenEqualsReturnsFalseForLowerValues() {
|
||||
assertThat(evaluate("findByAgeBetween", BRAN.age, ROBB.age).against(RICKON)).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void connectByAndReturnsTrueWhenAllPropertiesMatching() {
|
||||
assertThat(evaluate("findByAgeGreaterThanAndLastname", BRAN.age, JON.lastname).against(JON)).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void connectByAndReturnsFalseWhenOnlyFewPropertiesMatch() {
|
||||
assertThat(evaluate("findByAgeGreaterThanAndLastname", BRAN.age, JON.lastname).against(ROBB)).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void connectByOrReturnsTrueWhenOnlyFewPropertiesMatch() {
|
||||
assertThat(evaluate("findByAgeGreaterThanOrLastname", BRAN.age, JON.lastname).against(ROBB)).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void connectByOrReturnsTrueWhenAllPropertiesMatch() {
|
||||
assertThat(evaluate("findByAgeGreaterThanOrLastname", BRAN.age, JON.lastname).against(JON)).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void regexReturnsTrueWhenMatching() {
|
||||
assertThat(evaluate("findByLastnameMatches", "^s.*w$").against(JON)).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATACMNS-525
|
||||
void regexReturnsFalseWhenNotMatching() {
|
||||
assertThat(evaluate("findByLastnameMatches", "^s.*w$").against(ROBB)).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATAKV-169
|
||||
void inReturnsMatchCorrectly() {
|
||||
|
||||
ArrayList<String> list = new ArrayList<>();
|
||||
list.add(ROBB.firstname);
|
||||
|
||||
assertThat(evaluate("findByFirstnameIn", list).against(ROBB)).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATAKV-169
|
||||
void inNotMatchingReturnsCorrectly() {
|
||||
|
||||
ArrayList<String> list = new ArrayList<>();
|
||||
list.add(ROBB.firstname);
|
||||
|
||||
assertThat(evaluate("findByFirstnameIn", list).against(JON)).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATAKV-169
|
||||
void inWithNullCompareValuesCorrectly() {
|
||||
|
||||
ArrayList<String> list = new ArrayList<>();
|
||||
list.add(null);
|
||||
|
||||
assertThat(evaluate("findByFirstnameIn", list).against(JON)).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATAKV-169
|
||||
void inWithNullSourceValuesMatchesCorrectly() {
|
||||
|
||||
ArrayList<String> list = new ArrayList<>();
|
||||
list.add(ROBB.firstname);
|
||||
|
||||
assertThat(evaluate("findByFirstnameIn", list).against(new Person(null, 10))).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATAKV-169
|
||||
void inMatchesNullValuesCorrectly() {
|
||||
|
||||
ArrayList<String> list = new ArrayList<>();
|
||||
list.add(null);
|
||||
|
||||
assertThat(evaluate("findByFirstnameIn", list).against(new Person(null, 10))).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATAKV-185
|
||||
void noDerivedQueryArgumentsMatchesAlways() {
|
||||
|
||||
assertThat(evaluate("findBy").against(JON)).isTrue();
|
||||
assertThat(evaluate("findBy").against(null)).isTrue();
|
||||
}
|
||||
|
||||
private Evaluation evaluate(String methodName, Object... args) {
|
||||
try {
|
||||
return new Evaluation(createQueryForMethodWithArgs(methodName, args).getCriteria());
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private KeyValueQuery<SpelExpression> createQueryForMethodWithArgs(String methodName, Object... args)
|
||||
throws NoSuchMethodException, SecurityException {
|
||||
|
||||
Class<?>[] argTypes = new Class<?>[args.length];
|
||||
if (!ObjectUtils.isEmpty(args)) {
|
||||
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
argTypes[i] = args[i].getClass();
|
||||
}
|
||||
}
|
||||
|
||||
Method method = PersonRepository.class.getMethod(methodName, argTypes);
|
||||
doReturn(Person.class).when(metadataMock).getReturnedDomainClass(method);
|
||||
doReturn(TypeInformation.of(Person.class)).when(metadataMock).getDomainTypeInformation();
|
||||
doReturn(TypeInformation.of(Person.class)).when(metadataMock).getReturnType(method);
|
||||
|
||||
PartTree partTree = new PartTree(method.getName(), method.getReturnType());
|
||||
SpelQueryCreator creator = new SpelQueryCreator(partTree, new ParametersParameterAccessor(
|
||||
new QueryMethod(method, metadataMock, new SpelAwareProxyProjectionFactory()).getParameters(), args));
|
||||
|
||||
KeyValueQuery<SpelExpression> q = creator.createQuery();
|
||||
q.getCriteria().setEvaluationContext(
|
||||
query.getCriteria().setEvaluationContext(
|
||||
SimpleEvaluationContext.forReadOnlyDataBinding().withRootObject(args).withInstanceMethods().build());
|
||||
|
||||
return q;
|
||||
return query;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
interface PersonRepository extends CrudRepository<Person, String> {
|
||||
|
||||
// No arguments
|
||||
Person findBy();
|
||||
|
||||
// Type.SIMPLE_PROPERTY
|
||||
Person findByFirstname(String firstname);
|
||||
|
||||
// Type.TRUE
|
||||
Person findBySkinChangerIsTrue();
|
||||
|
||||
// Type.FALSE
|
||||
Person findBySkinChangerIsFalse();
|
||||
|
||||
// Type.IS_NULL
|
||||
Person findByLastnameIsNull();
|
||||
|
||||
// Type.IS_NOT_NULL
|
||||
Person findByLastnameIsNotNull();
|
||||
|
||||
// Type.STARTING_WITH
|
||||
Person findByFirstnameStartingWith(String firstanme);
|
||||
|
||||
Person findByFirstnameIgnoreCase(String firstanme);
|
||||
|
||||
// Type.AFTER
|
||||
Person findByBirthdayAfter(Date date);
|
||||
|
||||
// Type.GREATHER_THAN
|
||||
Person findByAgeGreaterThan(Integer age);
|
||||
|
||||
// Type.GREATER_THAN_EQUAL
|
||||
Person findByAgeGreaterThanEqual(Integer age);
|
||||
|
||||
// Type.BEFORE
|
||||
Person findByBirthdayBefore(Date date);
|
||||
|
||||
// Type.LESS_THAN
|
||||
Person findByAgeLessThan(Integer age);
|
||||
|
||||
// Type.LESS_THAN_EQUAL
|
||||
Person findByAgeLessThanEqual(Integer age);
|
||||
|
||||
// Type.BETWEEN
|
||||
Person findByAgeBetween(Integer low, Integer high);
|
||||
|
||||
// Type.LIKE
|
||||
Person findByFirstnameLike(String firstname);
|
||||
|
||||
// Type.ENDING_WITH
|
||||
Person findByFirstnameEndingWith(String firstname);
|
||||
|
||||
Person findByAgeGreaterThanAndLastname(Integer age, String lastname);
|
||||
|
||||
Person findByAgeGreaterThanOrLastname(Integer age, String lastname);
|
||||
|
||||
// Type.REGEX
|
||||
Person findByLastnameMatches(String lastname);
|
||||
|
||||
// Type.IN
|
||||
Person findByFirstnameIn(ArrayList<String> in);
|
||||
|
||||
@Override
|
||||
protected Evaluation createEvaluation(SpelExpression spelExpression) {
|
||||
return new SpelEvaluation(spelExpression);
|
||||
}
|
||||
|
||||
static class Evaluation {
|
||||
static class SpelEvaluation implements Evaluation {
|
||||
|
||||
SpelExpression expression;
|
||||
Object candidate;
|
||||
|
||||
Evaluation(SpelExpression expression) {
|
||||
SpelEvaluation(SpelExpression expression) {
|
||||
this.expression = expression;
|
||||
}
|
||||
|
||||
Boolean against(Object candidate) {
|
||||
public Boolean against(Object candidate) {
|
||||
this.candidate = candidate;
|
||||
return evaluate();
|
||||
}
|
||||
|
||||
private boolean evaluate() {
|
||||
public boolean evaluate() {
|
||||
expression.getEvaluationContext().setVariable("it", candidate);
|
||||
return expression.getValue(Boolean.class);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Person {
|
||||
|
||||
private @Id String id;
|
||||
private String firstname, lastname;
|
||||
private int age;
|
||||
private boolean isSkinChanger = false;
|
||||
private Date birthday;
|
||||
|
||||
public Person() {}
|
||||
|
||||
Person(String firstname, int age) {
|
||||
super();
|
||||
this.firstname = firstname;
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
Person skinChanger(boolean isSkinChanger) {
|
||||
this.isSkinChanger = isSkinChanger;
|
||||
return this;
|
||||
}
|
||||
|
||||
Person named(String lastname) {
|
||||
this.lastname = lastname;
|
||||
return this;
|
||||
}
|
||||
|
||||
Person bornAt(Date date) {
|
||||
this.birthday = date;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public String getFirstname() {
|
||||
return this.firstname;
|
||||
}
|
||||
|
||||
public String getLastname() {
|
||||
return this.lastname;
|
||||
}
|
||||
|
||||
public int getAge() {
|
||||
return this.age;
|
||||
}
|
||||
|
||||
public boolean isSkinChanger() {
|
||||
return this.isSkinChanger;
|
||||
}
|
||||
|
||||
public Date getBirthday() {
|
||||
return this.birthday;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public void setFirstname(String firstname) {
|
||||
this.firstname = firstname;
|
||||
}
|
||||
|
||||
public void setLastname(String lastname) {
|
||||
this.lastname = lastname;
|
||||
}
|
||||
|
||||
public void setAge(int age) {
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
public void setSkinChanger(boolean isSkinChanger) {
|
||||
this.isSkinChanger = isSkinChanger;
|
||||
}
|
||||
|
||||
public void setBirthday(Date birthday) {
|
||||
this.birthday = birthday;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user