From fe41202f96ca054cbfdc5ee611eb3bc7f78a41ea Mon Sep 17 00:00:00 2001 From: Thomas Darimont Date: Mon, 7 Oct 2013 17:01:07 +0200 Subject: [PATCH] DATAMONGO-770 - Add support for IgnoreCase in query derivation. We now support IgnoreCase and AllIgnoreCase in predicate expression for derived queries. Original pull request: #78. --- .../repository/query/MongoQueryCreator.java | 111 ++++++++++++++++-- ...tractPersonRepositoryIntegrationTests.java | 58 +++++++++ .../mongodb/repository/PersonRepository.java | 27 +++++ .../query/MongoQueryCreatorUnitTests.java | 107 ++++++++++++++++- 4 files changed, 288 insertions(+), 15 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java index c45f2ee50..168e0d7d4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java @@ -17,6 +17,7 @@ package org.springframework.data.mongodb.repository.query; import static org.springframework.data.mongodb.core.query.Criteria.*; +import java.util.Arrays; import java.util.Collection; import java.util.Iterator; @@ -35,6 +36,7 @@ import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.repository.query.ConvertingParameterAccessor.PotentiallyConvertingIterator; 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.Part.Type; import org.springframework.data.repository.query.parser.PartTree; import org.springframework.util.Assert; @@ -43,6 +45,7 @@ import org.springframework.util.Assert; * Custom query creator to create Mongo criterias. * * @author Oliver Gierke + * @author Thomas Darimont */ class MongoQueryCreator extends AbstractQueryCreator { @@ -99,7 +102,7 @@ class MongoQueryCreator extends AbstractQueryCreator { PersistentPropertyPath path = context.getPersistentPropertyPath(part.getProperty()); MongoPersistentProperty property = path.getLeafProperty(); - Criteria criteria = from(part.getType(), property, + Criteria criteria = from(part, property, where(path.toDotPath(MongoPersistentProperty.PropertyToFieldNameConverter.INSTANCE)), (PotentiallyConvertingIterator) iterator); @@ -120,7 +123,7 @@ class MongoQueryCreator extends AbstractQueryCreator { PersistentPropertyPath path = context.getPersistentPropertyPath(part.getProperty()); MongoPersistentProperty property = path.getLeafProperty(); - return from(part.getType(), property, + return from(part, property, base.and(path.toDotPath(MongoPersistentProperty.PropertyToFieldNameConverter.INSTANCE)), (PotentiallyConvertingIterator) iterator); } @@ -165,9 +168,11 @@ class MongoQueryCreator extends AbstractQueryCreator { * @param parameters * @return */ - private Criteria from(Type type, MongoPersistentProperty property, Criteria criteria, + private Criteria from(Part part, MongoPersistentProperty property, Criteria criteria, PotentiallyConvertingIterator parameters) { + Type type = part.getType(); + switch (type) { case AFTER: case GREATER_THAN: @@ -193,8 +198,7 @@ class MongoQueryCreator extends AbstractQueryCreator { case STARTING_WITH: case ENDING_WITH: case CONTAINING: - String value = parameters.next().toString(); - return criteria.regex(toLikeRegex(value, type)); + return addAppropriateLikeRegexTo(criteria, part, parameters.next().toString()); case REGEX: return criteria.regex(parameters.next().toString()); case EXISTS: @@ -220,19 +224,103 @@ class MongoQueryCreator extends AbstractQueryCreator { criteria.maxDistance(distance.getNormalizedValue()); } return criteria; - case WITHIN: + Object parameter = parameters.next(); return criteria.within((Shape) parameter); case SIMPLE_PROPERTY: - return criteria.is(parameters.nextConverted(property)); + + return isSimpleComparisionPossible(part) ? criteria.is(parameters.nextConverted(property)) + : createLikeRegexCriteriaOrThrow(part, property, criteria, parameters, false); + case NEGATING_SIMPLE_PROPERTY: - return criteria.ne(parameters.nextConverted(property)); + + return isSimpleComparisionPossible(part) ? criteria.ne(parameters.nextConverted(property)) + : createLikeRegexCriteriaOrThrow(part, property, criteria, parameters, true); default: throw new IllegalArgumentException("Unsupported keyword!"); } } + private boolean isSimpleComparisionPossible(Part part) { + + switch (part.shouldIgnoreCase()) { + case NEVER: + return true; + case WHEN_POSSIBLE: + return part.getProperty().getType() != String.class; + case ALWAYS: + return false; + default: + return true; + } + } + + /** + * Creates and extends the given criteria with a like-regex if necessary. + * + * @param part + * @param property + * @param criteria + * @param parameters + * @param shouldNegateExpression + * @return the criteria extended with the like-regex. + */ + private Criteria createLikeRegexCriteriaOrThrow(Part part, MongoPersistentProperty property, Criteria criteria, + PotentiallyConvertingIterator parameters, boolean shouldNegateExpression) { + + switch (part.shouldIgnoreCase()) { + + case ALWAYS: + if (part.getProperty().getType() != String.class) { + throw new IllegalArgumentException(String.format("part %s must be of type String but was %s", + part.getProperty(), part.getType())); + } + // fall-through + + case WHEN_POSSIBLE: + if (shouldNegateExpression) { + criteria = criteria.not(); + } + return addAppropriateLikeRegexTo(criteria, part, parameters.nextConverted(property).toString()); + + case NEVER: + // intentional no-op + } + + throw new IllegalArgumentException(String.format("part.shouldCaseIgnore must be one of %s, but was %s", + Arrays.asList(IgnoreCaseType.ALWAYS, IgnoreCaseType.WHEN_POSSIBLE), part.shouldIgnoreCase())); + } + + /** + * Creates an appropriate like-regex and appends it to the given criteria. + * + * @param criteria + * @param part + * @param value + * @return the criteria extended with the regex. + */ + private Criteria addAppropriateLikeRegexTo(Criteria criteria, Part part, String value) { + + return criteria.regex(toLikeRegex(value, part), toRegexOptions(part)); + } + + /** + * @param part + * @return the regex options or {@literal null}. + */ + private String toRegexOptions(Part part) { + + String regexOptions = null; + switch (part.shouldIgnoreCase()) { + case WHEN_POSSIBLE: + case ALWAYS: + regexOptions = "i"; + case NEVER: + } + return regexOptions; + } + /** * Returns the next element from the given {@link Iterator} expecting it to be of a certain type. * @@ -265,7 +353,9 @@ class MongoQueryCreator extends AbstractQueryCreator { return new Object[] { next }; } - private String toLikeRegex(String source, Type type) { + private String toLikeRegex(String source, Part part) { + + Type type = part.getType(); switch (type) { case STARTING_WITH: @@ -277,6 +367,9 @@ class MongoQueryCreator extends AbstractQueryCreator { case CONTAINING: source = "*" + source + "*"; break; + case SIMPLE_PROPERTY: + case NEGATING_SIMPLE_PROPERTY: + source = "^" + source + "$"; default: } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java index b5b445f91..a3ffd2dc7 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java @@ -49,6 +49,7 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; * Base class for tests for {@link PersonRepository}. * * @author Oliver Gierke + * @author Thomas Darimont */ @RunWith(SpringJUnit4ClassRunner.class) public abstract class AbstractPersonRepositoryIntegrationTests { @@ -680,4 +681,61 @@ public abstract class AbstractPersonRepositoryIntegrationTests { assertThat(results.isLastPage(), is(true)); assertThat(results.getAverageDistance().getMetric(), is((Metric) Metrics.KILOMETERS)); } + + /** + * @see DATAMONGO-770 + */ + @Test + public void findByFirstNameIgnoreCase() { + + List result = repository.findByFirstnameIgnoreCase("dave"); + + assertThat(result.size(), is(1)); + assertThat(result.get(0), is(dave)); + } + + /** + * @see DATAMONGO-770 + */ + @Test + public void findByFirstnameNotIgnoreCase() { + + List result = repository.findByFirstnameNotIgnoreCase("dave"); + + assertThat(result.size(), is(6)); + assertThat(result, not(hasItem(dave))); + } + + /** + * @see DATAMONGO-770 + */ + @Test + public void findByFirstnameStartingWithIgnoreCase() { + + List result = repository.findByFirstnameStartingWithIgnoreCase("da"); + assertThat(result.size(), is(1)); + assertThat(result.get(0), is(dave)); + } + + /** + * @see DATAMONGO-770 + */ + @Test + public void findByFirstnameEndingWithIgnoreCase() { + + List result = repository.findByFirstnameEndingWithIgnoreCase("VE"); + assertThat(result.size(), is(1)); + assertThat(result.get(0), is(dave)); + } + + /** + * @see DATAMONGO-770 + */ + @Test + public void findByFirstnameContainingIgnoreCase() { + + List result = repository.findByFirstnameContainingIgnoreCase("AV"); + assertThat(result.size(), is(1)); + assertThat(result.get(0), is(dave)); + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java index df79b1b8a..52285f763 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java @@ -36,6 +36,7 @@ import org.springframework.data.querydsl.QueryDslPredicateExecutor; * Sample repository managing {@link Person} entities. * * @author Oliver Gierke + * @author Thomas Darimont */ public interface PersonRepository extends MongoRepository, QueryDslPredicateExecutor { @@ -218,4 +219,30 @@ public interface PersonRepository extends MongoRepository, Query */ @Query(value = "{ 'lastname' : ?0 }", count = true) long someCountQuery(String lastname); + + /** + * @see DATAMONGO-770 + */ + List findByFirstnameIgnoreCase(String firstName); + + /** + * @see DATAMONGO-770 + */ + List findByFirstnameNotIgnoreCase(String firstName); + + /** + * @see DATAMONGO-770 + */ + List findByFirstnameStartingWithIgnoreCase(String firstName); + + /** + * @see DATAMONGO-770 + */ + List findByFirstnameEndingWithIgnoreCase(String firstName); + + /** + * @see DATAMONGO-770 + */ + List findByFirstnameContainingIgnoreCase(String firstName); + } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryCreatorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryCreatorUnitTests.java index e46caee89..0cce34daf 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryCreatorUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryCreatorUnitTests.java @@ -27,7 +27,9 @@ import java.lang.reflect.Method; import java.util.List; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; @@ -55,17 +57,19 @@ import org.springframework.data.util.TypeInformation; * Unit test for {@link MongoQueryCreator}. * * @author Oliver Gierke + * @author Thomas Darimont */ @RunWith(MockitoJUnitRunner.class) public class MongoQueryCreatorUnitTests { Method findByFirstname, findByFirstnameAndFriend, findByFirstnameNotNull; - @Mock - MongoConverter converter; + @Mock MongoConverter converter; MappingContext context; + @Rule public ExpectedException expection = ExpectedException.none(); + @Before public void setUp() throws SecurityException, NoSuchMethodException { @@ -310,6 +314,99 @@ public class MongoQueryCreatorUnitTests { assertThat(query, is(query)); } + /** + * @see DATAMONGO-770 + */ + @Test + public void createsQueryWithFindByIgnoreCaseCorrectly() { + + PartTree tree = new PartTree("findByfirstNameIgnoreCase", Person.class); + MongoQueryCreator creator = new MongoQueryCreator(tree, getAccessor(converter, "dave"), context); + + Query query = creator.createQuery(); + assertThat(query, is(query(where("firstName").regex("^dave$", "i")))); + } + + /** + * @see DATAMONGO-770 + */ + @Test + public void createsQueryWithFindByNotIgnoreCaseCorrectly() { + + PartTree tree = new PartTree("findByFirstNameNotIgnoreCase", Person.class); + MongoQueryCreator creator = new MongoQueryCreator(tree, getAccessor(converter, "dave"), context); + + Query query = creator.createQuery(); + assertThat(query.toString(), is(query(where("firstName").not().regex("^dave$", "i")).toString())); + } + + /** + * @see DATAMONGO-770 + */ + @Test + public void createsQueryWithFindByStartingWithIgnoreCaseCorrectly() { + + PartTree tree = new PartTree("findByFirstNameStartingWithIgnoreCase", Person.class); + MongoQueryCreator creator = new MongoQueryCreator(tree, getAccessor(converter, "dave"), context); + + Query query = creator.createQuery(); + assertThat(query, is(query(where("firstName").regex("^dave", "i")))); + } + + /** + * @see DATAMONGO-770 + */ + @Test + public void createsQueryWithFindByEndingWithIgnoreCaseCorrectly() { + + PartTree tree = new PartTree("findByFirstNameEndingWithIgnoreCase", Person.class); + MongoQueryCreator creator = new MongoQueryCreator(tree, getAccessor(converter, "dave"), context); + + Query query = creator.createQuery(); + assertThat(query, is(query(where("firstName").regex("dave$", "i")))); + } + + /** + * @see DATAMONGO-770 + */ + @Test + public void createsQueryWithFindByContainingIgnoreCaseCorrectly() { + + PartTree tree = new PartTree("findByFirstNameContainingIgnoreCase", Person.class); + MongoQueryCreator creator = new MongoQueryCreator(tree, getAccessor(converter, "dave"), context); + + Query query = creator.createQuery(); + assertThat(query, is(query(where("firstName").regex(".*dave.*", "i")))); + } + + /** + * @see DATAMONGO-770 + */ + @Test + public void shouldThrowExceptionForQueryWithFindByIgnoreCaseOnNonStringProperty() { + + expection.expect(IllegalArgumentException.class); + expection.expectMessage("must be of type String"); + + PartTree tree = new PartTree("findByFirstNameAndAgeIgnoreCase", Person.class); + MongoQueryCreator creator = new MongoQueryCreator(tree, getAccessor(converter, "foo", 42), context); + + creator.createQuery(); + } + + /** + * @see DATAMONGO-770 + */ + @Test + public void shouldOnlyGenerateLikeExpressionsForStringPropertiesIfAllIgnoreCase() { + + PartTree tree = new PartTree("findByFirstNameAndAgeAllIgnoreCase", Person.class); + MongoQueryCreator creator = new MongoQueryCreator(tree, getAccessor(converter, "dave", 42), context); + + Query query = creator.createQuery(); + assertThat(query, is(query(where("firstName").regex("^dave$", "i").and("age").is(42)))); + } + interface PersonRepository extends Repository { List findByLocationNearAndFirstname(Point location, Distance maxDistance, String firstname); @@ -317,10 +414,8 @@ public class MongoQueryCreatorUnitTests { class User { - @Field("foo") - String username; + @Field("foo") String username; - @DBRef - User creator; + @DBRef User creator; } }