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);