From 5c0707d2212d9f51fa11e959ab4f205ac3d7e43f Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 17 Jun 2015 13:28:12 +0200 Subject: [PATCH] DATAMONGO-1232 - IngoreCase in criteria now escapes query. We now quote the original criteria before actually wrapping it inside of an regular expression for case insensitive search. This happens not only to case insensitive is, startsWith, endsWith criteria but also to those using like. In that case we quote the part between leading and trailing wildcard if required. Original pull request: #301. --- .../repository/query/MongoQueryCreator.java | 47 ++++++++- .../query/MongoQueryCreatorUnitTests.java | 96 +++++++++++++++++++ 2 files changed, 138 insertions(+), 5 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 6a34d1d13..bef1c2165 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 @@ -20,6 +20,7 @@ import static org.springframework.data.mongodb.core.query.Criteria.*; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; +import java.util.regex.Pattern; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -45,6 +46,7 @@ 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; +import org.springframework.util.ObjectUtils; /** * Custom query creator to create Mongo criterias. @@ -56,6 +58,7 @@ import org.springframework.util.Assert; class MongoQueryCreator extends AbstractQueryCreator { private static final Logger LOG = LoggerFactory.getLogger(MongoQueryCreator.class); + private static final Pattern PUNCTATION_PATTERN = Pattern.compile("\\p{Punct}"); private final MongoParameterAccessor accessor; private final boolean isGeoNearQuery; @@ -389,25 +392,59 @@ class MongoQueryCreator extends AbstractQueryCreator { private String toLikeRegex(String source, Part part) { Type type = part.getType(); + String regex = prepareAndEscapeStringBeforeApplyingLikeRegex(source, part); switch (type) { case STARTING_WITH: - source = "^" + source; + regex = "^" + regex; break; case ENDING_WITH: - source = source + "$"; + regex = regex + "$"; break; case CONTAINING: case NOT_CONTAINING: - source = "*" + source + "*"; + regex = ".*" + regex + ".*"; break; case SIMPLE_PROPERTY: case NEGATING_SIMPLE_PROPERTY: - source = "^" + source + "$"; + regex = "^" + regex + "$"; default: } - return source.replaceAll("\\*", ".*"); + return regex; + } + + private String prepareAndEscapeStringBeforeApplyingLikeRegex(String source, Part qpart) { + + if (!ObjectUtils.nullSafeEquals(Type.LIKE, qpart.getType())) { + return PUNCTATION_PATTERN.matcher(source).find() ? Pattern.quote(source) : source; + } + + if (source.equals("*")) { + return ".*"; + } + + StringBuilder sb = new StringBuilder(); + + boolean leadingWildcard = source.startsWith("*"); + boolean trailingWildcard = source.endsWith("*"); + + String valueToUse = source.substring(leadingWildcard ? 1 : 0, + trailingWildcard ? source.length() - 1 : source.length()); + + if (PUNCTATION_PATTERN.matcher(valueToUse).find()) { + valueToUse = Pattern.quote(valueToUse); + } + + if (leadingWildcard) { + sb.append(".*"); + } + sb.append(valueToUse); + if (trailingWildcard) { + sb.append(".*"); + } + + return sb.toString(); } private boolean isSpherical(MongoPersistentProperty property) { 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 dfade4bd3..40be393cb 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 @@ -565,6 +565,102 @@ public class MongoQueryCreatorUnitTests { assertThat(new MongoQueryCreator(tree, accessor, context).createQuery(), is(notNullValue())); } + /** + * @see DATAMONGO-1232 + */ + @Test + public void ignoreCaseShouldEscapeSource() { + + PartTree tree = new PartTree("findByUsernameIgnoreCase", User.class); + ConvertingParameterAccessor accessor = getAccessor(converter, "con.flux+"); + + Query query = new MongoQueryCreator(tree, accessor, context).createQuery(); + + assertThat(query, is(query(where("username").regex("^\\Qcon.flux+\\E$", "i")))); + } + + /** + * @see DATAMONGO-1232 + */ + @Test + public void ignoreCaseShouldEscapeSourceWhenUsedForStartingWith() { + + PartTree tree = new PartTree("findByUsernameStartingWithIgnoreCase", User.class); + ConvertingParameterAccessor accessor = getAccessor(converter, "dawns.light+"); + + Query query = new MongoQueryCreator(tree, accessor, context).createQuery(); + + assertThat(query, is(query(where("username").regex("^\\Qdawns.light+\\E", "i")))); + } + + /** + * @see DATAMONGO-1232 + */ + @Test + public void ignoreCaseShouldEscapeSourceWhenUsedForEndingWith() { + + PartTree tree = new PartTree("findByUsernameEndingWithIgnoreCase", User.class); + ConvertingParameterAccessor accessor = getAccessor(converter, "new.ton+"); + + Query query = new MongoQueryCreator(tree, accessor, context).createQuery(); + + assertThat(query, is(query(where("username").regex("\\Qnew.ton+\\E$", "i")))); + } + + /** + * @see DATAMONGO-1232 + */ + @Test + public void likeShouldEscapeSourceWhenUsedWithLeadingAndTrailingWildcard() { + + PartTree tree = new PartTree("findByUsernameLike", User.class); + ConvertingParameterAccessor accessor = getAccessor(converter, "*fire.fight+*"); + + Query query = new MongoQueryCreator(tree, accessor, context).createQuery(); + + assertThat(query, is(query(where("username").regex(".*\\Qfire.fight+\\E.*")))); + } + + /** + * @see DATAMONGO-1232 + */ + @Test + public void likeShouldEscapeSourceWhenUsedWithLeadingWildcard() { + + PartTree tree = new PartTree("findByUsernameLike", User.class); + ConvertingParameterAccessor accessor = getAccessor(converter, "*steel.heart+"); + + Query query = new MongoQueryCreator(tree, accessor, context).createQuery(); + + assertThat(query, is(query(where("username").regex(".*\\Qsteel.heart+\\E")))); + } + + /** + * @see DATAMONGO-1232 + */ + @Test + public void likeShouldEscapeSourceWhenUsedWithTrailingWildcard() { + + PartTree tree = new PartTree("findByUsernameLike", User.class); + ConvertingParameterAccessor accessor = getAccessor(converter, "cala.mity+*"); + + Query query = new MongoQueryCreator(tree, accessor, context).createQuery(); + assertThat(query, is(query(where("username").regex("\\Qcala.mity+\\E.*")))); + } + + /** + * @see DATAMONGO-1232 + */ + @Test + public void likeShouldBeTreatedCorrectlyWhenUsedWithWildcardOnly() { + + PartTree tree = new PartTree("findByUsernameLike", User.class); + ConvertingParameterAccessor accessor = getAccessor(converter, "*"); + + Query query = new MongoQueryCreator(tree, accessor, context).createQuery(); + assertThat(query, is(query(where("username").regex(".*")))); + } + interface PersonRepository extends Repository { List findByLocationNearAndFirstname(Point location, Distance maxDistance, String firstname);