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 c45f2ee50e..7c2be1764d 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 @@ 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.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; @@ -99,7 +101,7 @@ protected Criteria create(Part part, Iterator iterator) { 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 +122,7 @@ protected Criteria and(Part part, Criteria base, Iterator iterator) { 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 +167,11 @@ protected Query complete(Criteria criteria, Sort sort) { * @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 +197,7 @@ private Criteria from(Type type, MongoPersistentProperty property, Criteria crit 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 +223,103 @@ private Criteria from(Type type, MongoPersistentProperty property, Criteria crit 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 +352,9 @@ private Object[] nextAsArray(PotentiallyConvertingIterator iterator, MongoPersis 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 +366,9 @@ private String toLikeRegex(String source, Type type) { 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 b5b445f91a..6e6df90c34 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 @@ -680,4 +680,61 @@ public void executesGeoPageQueryForWithPageRequestForJustOneElementEmptyPage() { 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 df79b1b8a9..2a450be756 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 @@ -218,4 +218,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 e46caee89d..ee13d298e2 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.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; @@ -61,11 +63,12 @@ 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 +313,99 @@ private void assertBindsDistanceToQuery(Point point, Distance distance, Query re 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 +413,8 @@ interface PersonRepository extends Repository { class User { - @Field("foo") - String username; + @Field("foo") String username; - @DBRef - User creator; + @DBRef User creator; } }