Introduce Predicate-based QueryEngine.

Closes: #565
This commit is contained in:
Christoph Strobl
2024-03-08 09:37:38 +01:00
parent 793db8a405
commit 805849254d
7 changed files with 1128 additions and 466 deletions

View File

@@ -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());
}
}

View File

@@ -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) {

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}
}