diff --git a/src/main/java/org/springframework/data/keyvalue/core/PredicateQueryEngine.java b/src/main/java/org/springframework/data/keyvalue/core/PredicateQueryEngine.java new file mode 100644 index 0000000..9aa12bf --- /dev/null +++ b/src/main/java/org/springframework/data/keyvalue/core/PredicateQueryEngine.java @@ -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, 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> 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 List filterMatchingRange(List source, @Nullable Predicate criteria, long offset, int rows) { + + Stream 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()); + } +} diff --git a/src/main/java/org/springframework/data/keyvalue/core/SimplePropertyPathAccessor.java b/src/main/java/org/springframework/data/keyvalue/core/SimplePropertyPathAccessor.java index ac45429..f4ae8e7 100644 --- a/src/main/java/org/springframework/data/keyvalue/core/SimplePropertyPathAccessor.java +++ b/src/main/java/org/springframework/data/keyvalue/core/SimplePropertyPathAccessor.java @@ -23,7 +23,7 @@ import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper; * @author Christoph Strobl * @since 3.1.10 */ -class SimplePropertyPathAccessor { +public class SimplePropertyPathAccessor { private final Object root; @@ -31,7 +31,7 @@ class SimplePropertyPathAccessor { this.root = source; } - Object getValue(PropertyPath path) { + public Object getValue(PropertyPath path) { Object currentValue = root; for (PropertyPath current : path) { diff --git a/src/main/java/org/springframework/data/keyvalue/repository/query/PredicateQueryCreator.java b/src/main/java/org/springframework/data/keyvalue/repository/query/PredicateQueryCreator.java new file mode 100644 index 0000000..52076fe --- /dev/null +++ b/src/main/java/org/springframework/data/keyvalue/repository/query/PredicateQueryCreator.java @@ -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>, Predicate> { + + public PredicateQueryCreator(PartTree tree, ParameterAccessor parameters) { + super(tree, parameters); + } + + @Override + protected Predicate create(Part part, Iterator 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 iterator) { + return base.and((Predicate) create(part, iterator)); + } + + @Override + protected Predicate or(Predicate base, Predicate criteria) { + return base.or((Predicate) criteria); + } + + @Override + protected KeyValueQuery> 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 isTrue() { + return new ValueComparingPredicate(part.getProperty(), true); + } + + public Predicate isFalse() { + return new ValueComparingPredicate(part.getProperty(), false); + } + + public Predicate 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 isNull() { + return new ValueComparingPredicate(part.getProperty(), Objects::isNull); + } + + public Predicate isNotNull() { + return isNull().negate(); + } + + public Predicate isLessThan(Object value) { + return new ValueComparingPredicate(part.getProperty(), + o -> NullSafeComparator.NULLS_HIGH.compare(o, value) == -1 ? true : false); + } + + public Predicate isLessThanEqual(Object value) { + return new ValueComparingPredicate(part.getProperty(), + o -> NullSafeComparator.NULLS_HIGH.compare(o, value) <= 0 ? true : false); + } + + public Predicate isGreaterThan(Object value) { + return new ValueComparingPredicate(part.getProperty(), + o -> NullSafeComparator.NULLS_HIGH.compare(o, value) == 1 ? true : false); + } + + public Predicate isGreaterThanEqual(Object value) { + return new ValueComparingPredicate(part.getProperty(), + o -> NullSafeComparator.NULLS_HIGH.compare(o, value) >= 0 ? true : false); + } + + public Predicate matches(Pattern pattern) { + + return new ValueComparingPredicate(part.getProperty(), o -> { + if (o == null) { + return false; + } + + return pattern.matcher(o.toString()).find(); + }); + } + + public Predicate 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 matches(String regex) { + return matches(Pattern.compile(regex)); + } + + public Predicate 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 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 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 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 { + + private final PropertyPath path; + private final Function check; + + public ValueComparingPredicate(PropertyPath path, Object expected) { + this(path, (value) -> ObjectUtils.nullSafeEquals(value, expected)); + } + + public ValueComparingPredicate(PropertyPath path, Function check) { + this.path = path; + this.check = check; + } + + @Override + public boolean test(Object o) { + Object value = new SimplePropertyPathAccessor<>(o).getValue(path); + return check.apply(value); + } + } + +} diff --git a/src/test/java/org/springframework/data/keyvalue/core/PredicateQueryEngineUnitTests.java b/src/test/java/org/springframework/data/keyvalue/core/PredicateQueryEngineUnitTests.java new file mode 100644 index 0000000..eb3c071 --- /dev/null +++ b/src/test/java/org/springframework/data/keyvalue/core/PredicateQueryEngineUnitTests.java @@ -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 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> 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; + } + } +} diff --git a/src/test/java/org/springframework/data/keyvalue/repository/query/AbstractQueryCreatorTestBase.java b/src/test/java/org/springframework/data/keyvalue/repository/query/AbstractQueryCreatorTestBase.java new file mode 100644 index 0000000..1d11e46 --- /dev/null +++ b/src/test/java/org/springframework/data/keyvalue/repository/query/AbstractQueryCreatorTestBase.java @@ -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, ?>, 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 list = new ArrayList<>(); + list.add(ROBB.firstname); + + assertThat(evaluate("findByFirstnameIn", list).against(ROBB)).isTrue(); + } + + @Test // DATAKV-169 + void inNotMatchingReturnsCorrectly() { + + ArrayList list = new ArrayList<>(); + list.add(ROBB.firstname); + + assertThat(evaluate("findByFirstnameIn", list).against(JON)).isFalse(); + } + + @Test // DATAKV-169 + void inWithNullCompareValuesCorrectly() { + + ArrayList list = new ArrayList<>(); + list.add(null); + + assertThat(evaluate("findByFirstnameIn", list).against(JON)).isFalse(); + } + + @Test // DATAKV-169 + void inWithNullSourceValuesMatchesCorrectly() { + + ArrayList list = new ArrayList<>(); + list.add(ROBB.firstname); + + assertThat(evaluate("findByFirstnameIn", list).against(new PredicateQueryCreatorUnitTests.Person(null, 10))) + .isFalse(); + } + + @Test // DATAKV-169 + void inMatchesNullValuesCorrectly() { + + ArrayList 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 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 q = creator.createQuery(); + return finalizeQuery(q, args); + } + + protected abstract QUERY_CREATOR queryCreator(PartTree partTree, ParametersParameterAccessor accessor); + + protected abstract KeyValueQuery finalizeQuery(KeyValueQuery query, Object... args); + + interface PersonRepository extends CrudRepository { + + // 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 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; + } + } +} diff --git a/src/test/java/org/springframework/data/keyvalue/repository/query/PredicateQueryCreatorUnitTests.java b/src/test/java/org/springframework/data/keyvalue/repository/query/PredicateQueryCreatorUnitTests.java new file mode 100644 index 0000000..6113c75 --- /dev/null +++ b/src/test/java/org/springframework/data/keyvalue/repository/query/PredicateQueryCreatorUnitTests.java @@ -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> { + + @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> finalizeQuery(KeyValueQuery> 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); + } + } +} diff --git a/src/test/java/org/springframework/data/keyvalue/repository/query/SpelQueryCreatorUnitTests.java b/src/test/java/org/springframework/data/keyvalue/repository/query/SpelQueryCreatorUnitTests.java index e45a99a..9ddc54c 100644 --- a/src/test/java/org/springframework/data/keyvalue/repository/query/SpelQueryCreatorUnitTests.java +++ b/src/test/java/org/springframework/data/keyvalue/repository/query/SpelQueryCreatorUnitTests.java @@ -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 { - 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 finalizeQuery(KeyValueQuery 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 list = new ArrayList<>(); - list.add(ROBB.firstname); - - assertThat(evaluate("findByFirstnameIn", list).against(ROBB)).isTrue(); - } - - @Test // DATAKV-169 - void inNotMatchingReturnsCorrectly() { - - ArrayList list = new ArrayList<>(); - list.add(ROBB.firstname); - - assertThat(evaluate("findByFirstnameIn", list).against(JON)).isFalse(); - } - - @Test // DATAKV-169 - void inWithNullCompareValuesCorrectly() { - - ArrayList list = new ArrayList<>(); - list.add(null); - - assertThat(evaluate("findByFirstnameIn", list).against(JON)).isFalse(); - } - - @Test // DATAKV-169 - void inWithNullSourceValuesMatchesCorrectly() { - - ArrayList list = new ArrayList<>(); - list.add(ROBB.firstname); - - assertThat(evaluate("findByFirstnameIn", list).against(new Person(null, 10))).isFalse(); - } - - @Test // DATAKV-169 - void inMatchesNullValuesCorrectly() { - - ArrayList 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 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 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 { - - // 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 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; - } - } }