Browse Source

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.
pull/79/head
Thomas Darimont 12 years ago committed by Oliver Gierke
parent
commit
fe41202f96
  1. 111
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java
  2. 58
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java
  3. 27
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java
  4. 107
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryCreatorUnitTests.java

111
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 static org.springframework.data.mongodb.core.query.Criteria.*;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Iterator; 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.mongodb.repository.query.ConvertingParameterAccessor.PotentiallyConvertingIterator;
import org.springframework.data.repository.query.parser.AbstractQueryCreator; import org.springframework.data.repository.query.parser.AbstractQueryCreator;
import org.springframework.data.repository.query.parser.Part; 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.Part.Type;
import org.springframework.data.repository.query.parser.PartTree; import org.springframework.data.repository.query.parser.PartTree;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -43,6 +45,7 @@ import org.springframework.util.Assert;
* Custom query creator to create Mongo criterias. * Custom query creator to create Mongo criterias.
* *
* @author Oliver Gierke * @author Oliver Gierke
* @author Thomas Darimont
*/ */
class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> { class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> {
@ -99,7 +102,7 @@ class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> {
PersistentPropertyPath<MongoPersistentProperty> path = context.getPersistentPropertyPath(part.getProperty()); PersistentPropertyPath<MongoPersistentProperty> path = context.getPersistentPropertyPath(part.getProperty());
MongoPersistentProperty property = path.getLeafProperty(); MongoPersistentProperty property = path.getLeafProperty();
Criteria criteria = from(part.getType(), property, Criteria criteria = from(part, property,
where(path.toDotPath(MongoPersistentProperty.PropertyToFieldNameConverter.INSTANCE)), where(path.toDotPath(MongoPersistentProperty.PropertyToFieldNameConverter.INSTANCE)),
(PotentiallyConvertingIterator) iterator); (PotentiallyConvertingIterator) iterator);
@ -120,7 +123,7 @@ class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> {
PersistentPropertyPath<MongoPersistentProperty> path = context.getPersistentPropertyPath(part.getProperty()); PersistentPropertyPath<MongoPersistentProperty> path = context.getPersistentPropertyPath(part.getProperty());
MongoPersistentProperty property = path.getLeafProperty(); MongoPersistentProperty property = path.getLeafProperty();
return from(part.getType(), property, return from(part, property,
base.and(path.toDotPath(MongoPersistentProperty.PropertyToFieldNameConverter.INSTANCE)), base.and(path.toDotPath(MongoPersistentProperty.PropertyToFieldNameConverter.INSTANCE)),
(PotentiallyConvertingIterator) iterator); (PotentiallyConvertingIterator) iterator);
} }
@ -165,9 +168,11 @@ class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> {
* @param parameters * @param parameters
* @return * @return
*/ */
private Criteria from(Type type, MongoPersistentProperty property, Criteria criteria, private Criteria from(Part part, MongoPersistentProperty property, Criteria criteria,
PotentiallyConvertingIterator parameters) { PotentiallyConvertingIterator parameters) {
Type type = part.getType();
switch (type) { switch (type) {
case AFTER: case AFTER:
case GREATER_THAN: case GREATER_THAN:
@ -193,8 +198,7 @@ class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> {
case STARTING_WITH: case STARTING_WITH:
case ENDING_WITH: case ENDING_WITH:
case CONTAINING: case CONTAINING:
String value = parameters.next().toString(); return addAppropriateLikeRegexTo(criteria, part, parameters.next().toString());
return criteria.regex(toLikeRegex(value, type));
case REGEX: case REGEX:
return criteria.regex(parameters.next().toString()); return criteria.regex(parameters.next().toString());
case EXISTS: case EXISTS:
@ -220,19 +224,103 @@ class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> {
criteria.maxDistance(distance.getNormalizedValue()); criteria.maxDistance(distance.getNormalizedValue());
} }
return criteria; return criteria;
case WITHIN: case WITHIN:
Object parameter = parameters.next(); Object parameter = parameters.next();
return criteria.within((Shape) parameter); return criteria.within((Shape) parameter);
case SIMPLE_PROPERTY: 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: 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: default:
throw new IllegalArgumentException("Unsupported keyword!"); 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. * 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<Query, Criteria> {
return new Object[] { next }; return new Object[] { next };
} }
private String toLikeRegex(String source, Type type) { private String toLikeRegex(String source, Part part) {
Type type = part.getType();
switch (type) { switch (type) {
case STARTING_WITH: case STARTING_WITH:
@ -277,6 +367,9 @@ class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> {
case CONTAINING: case CONTAINING:
source = "*" + source + "*"; source = "*" + source + "*";
break; break;
case SIMPLE_PROPERTY:
case NEGATING_SIMPLE_PROPERTY:
source = "^" + source + "$";
default: default:
} }

58
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}. * Base class for tests for {@link PersonRepository}.
* *
* @author Oliver Gierke * @author Oliver Gierke
* @author Thomas Darimont
*/ */
@RunWith(SpringJUnit4ClassRunner.class) @RunWith(SpringJUnit4ClassRunner.class)
public abstract class AbstractPersonRepositoryIntegrationTests { public abstract class AbstractPersonRepositoryIntegrationTests {
@ -680,4 +681,61 @@ public abstract class AbstractPersonRepositoryIntegrationTests {
assertThat(results.isLastPage(), is(true)); assertThat(results.isLastPage(), is(true));
assertThat(results.getAverageDistance().getMetric(), is((Metric) Metrics.KILOMETERS)); assertThat(results.getAverageDistance().getMetric(), is((Metric) Metrics.KILOMETERS));
} }
/**
* @see DATAMONGO-770
*/
@Test
public void findByFirstNameIgnoreCase() {
List<Person> result = repository.findByFirstnameIgnoreCase("dave");
assertThat(result.size(), is(1));
assertThat(result.get(0), is(dave));
}
/**
* @see DATAMONGO-770
*/
@Test
public void findByFirstnameNotIgnoreCase() {
List<Person> result = repository.findByFirstnameNotIgnoreCase("dave");
assertThat(result.size(), is(6));
assertThat(result, not(hasItem(dave)));
}
/**
* @see DATAMONGO-770
*/
@Test
public void findByFirstnameStartingWithIgnoreCase() {
List<Person> result = repository.findByFirstnameStartingWithIgnoreCase("da");
assertThat(result.size(), is(1));
assertThat(result.get(0), is(dave));
}
/**
* @see DATAMONGO-770
*/
@Test
public void findByFirstnameEndingWithIgnoreCase() {
List<Person> result = repository.findByFirstnameEndingWithIgnoreCase("VE");
assertThat(result.size(), is(1));
assertThat(result.get(0), is(dave));
}
/**
* @see DATAMONGO-770
*/
@Test
public void findByFirstnameContainingIgnoreCase() {
List<Person> result = repository.findByFirstnameContainingIgnoreCase("AV");
assertThat(result.size(), is(1));
assertThat(result.get(0), is(dave));
}
} }

27
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. * Sample repository managing {@link Person} entities.
* *
* @author Oliver Gierke * @author Oliver Gierke
* @author Thomas Darimont
*/ */
public interface PersonRepository extends MongoRepository<Person, String>, QueryDslPredicateExecutor<Person> { public interface PersonRepository extends MongoRepository<Person, String>, QueryDslPredicateExecutor<Person> {
@ -218,4 +219,30 @@ public interface PersonRepository extends MongoRepository<Person, String>, Query
*/ */
@Query(value = "{ 'lastname' : ?0 }", count = true) @Query(value = "{ 'lastname' : ?0 }", count = true)
long someCountQuery(String lastname); long someCountQuery(String lastname);
/**
* @see DATAMONGO-770
*/
List<Person> findByFirstnameIgnoreCase(String firstName);
/**
* @see DATAMONGO-770
*/
List<Person> findByFirstnameNotIgnoreCase(String firstName);
/**
* @see DATAMONGO-770
*/
List<Person> findByFirstnameStartingWithIgnoreCase(String firstName);
/**
* @see DATAMONGO-770
*/
List<Person> findByFirstnameEndingWithIgnoreCase(String firstName);
/**
* @see DATAMONGO-770
*/
List<Person> findByFirstnameContainingIgnoreCase(String firstName);
} }

107
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 java.util.List;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.Mockito; import org.mockito.Mockito;
@ -55,17 +57,19 @@ import org.springframework.data.util.TypeInformation;
* Unit test for {@link MongoQueryCreator}. * Unit test for {@link MongoQueryCreator}.
* *
* @author Oliver Gierke * @author Oliver Gierke
* @author Thomas Darimont
*/ */
@RunWith(MockitoJUnitRunner.class) @RunWith(MockitoJUnitRunner.class)
public class MongoQueryCreatorUnitTests { public class MongoQueryCreatorUnitTests {
Method findByFirstname, findByFirstnameAndFriend, findByFirstnameNotNull; Method findByFirstname, findByFirstnameAndFriend, findByFirstnameNotNull;
@Mock @Mock MongoConverter converter;
MongoConverter converter;
MappingContext<?, MongoPersistentProperty> context; MappingContext<?, MongoPersistentProperty> context;
@Rule public ExpectedException expection = ExpectedException.none();
@Before @Before
public void setUp() throws SecurityException, NoSuchMethodException { public void setUp() throws SecurityException, NoSuchMethodException {
@ -310,6 +314,99 @@ public class MongoQueryCreatorUnitTests {
assertThat(query, is(query)); 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<Person, Long> { interface PersonRepository extends Repository<Person, Long> {
List<Person> findByLocationNearAndFirstname(Point location, Distance maxDistance, String firstname); List<Person> findByLocationNearAndFirstname(Point location, Distance maxDistance, String firstname);
@ -317,10 +414,8 @@ public class MongoQueryCreatorUnitTests {
class User { class User {
@Field("foo") @Field("foo") String username;
String username;
@DBRef @DBRef User creator;
User creator;
} }
} }

Loading…
Cancel
Save