From 39ba28cdaeba43a69c26f614157b5b1bcbbe46b1 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 8 Feb 2017 15:11:13 +0100 Subject: [PATCH] DATAMONGO-1603 - Fix Placeholder not replaced correctly in @Query. Fix issues when placeholders are appended with other chars eg. '?0xyz' or have been reused multiple times within the query. Additional tests and fixes for complex quoted replacements eg. in regex query. Rely on placeholder quotation indication instead of binding one. Might be misleading when placeholder is used more than once. This backport contains elements from DATAMONGO-1575. Original pull request: #441. Related ticket: DATAMONGO-1575. --- .../ExpressionEvaluatingParameterBinder.java | 130 +++++++++++--- .../query/StringBasedMongoQuery.java | 26 +-- .../query/StringBasedMongoQueryUnitTests.java | 167 ++++++++++++++++-- 3 files changed, 271 insertions(+), 52 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ExpressionEvaluatingParameterBinder.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ExpressionEvaluatingParameterBinder.java index a86378e1d..83489e61b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ExpressionEvaluatingParameterBinder.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ExpressionEvaluatingParameterBinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2016 the original author or authors. + * Copyright 2015-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,9 @@ */ package org.springframework.data.mongodb.repository.query; +import lombok.EqualsAndHashCode; import lombok.Value; +import lombok.experimental.UtilityClass; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -37,12 +39,13 @@ import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; +import com.mongodb.DBObject; import com.mongodb.util.JSON; /** - * {@link ExpressionEvaluatingParameterBinder} allows to evaluate, convert and bind parameters to placholders within a + * {@link ExpressionEvaluatingParameterBinder} allows to evaluate, convert and bind parameters to placeholders within a * {@link String}. - * + * * @author Christoph Strobl * @author Thomas Darimont * @author Oliver Gierke @@ -56,7 +59,7 @@ class ExpressionEvaluatingParameterBinder { /** * Creates new {@link ExpressionEvaluatingParameterBinder} - * + * * @param expressionParser must not be {@literal null}. * @param evaluationContextProvider must not be {@literal null}. */ @@ -73,7 +76,7 @@ class ExpressionEvaluatingParameterBinder { /** * Bind values provided by {@link MongoParameterAccessor} to placeholders in {@literal raw} while considering * potential conversions and parameter types. - * + * * @param raw can be {@literal null} or empty. * @param accessor must not be {@literal null}. * @param bindingContext must not be {@literal null}. @@ -90,7 +93,7 @@ class ExpressionEvaluatingParameterBinder { /** * Replaced the parameter placeholders with the actual parameter values from the given {@link ParameterBinding}s. - * + * * @param input must not be {@literal null} or empty. * @param accessor must not be {@literal null}. * @param bindingContext must not be {@literal null}. @@ -110,16 +113,23 @@ class ExpressionEvaluatingParameterBinder { Matcher matcher = createReplacementPattern(bindingContext.getBindings()).matcher(input); StringBuffer buffer = new StringBuffer(); + int parameterIndex = 0; while (matcher.find()) { - ParameterBinding binding = bindingContext.getBindingFor(extractPlaceholder(matcher.group())); + Placeholder placeholder = extractPlaceholder(parameterIndex++, matcher); + ParameterBinding binding = bindingContext.getBindingFor(placeholder); String valueForBinding = getParameterValueForBinding(accessor, bindingContext.getParameters(), binding); // appendReplacement does not like unescaped $ sign and others, so we need to quote that stuff first matcher.appendReplacement(buffer, Matcher.quoteReplacement(valueForBinding)); + if (StringUtils.hasText(placeholder.getSuffix())) { + buffer.append(placeholder.getSuffix()); + } - if (binding.isQuoted()) { - postProcessQuotedBinding(buffer, valueForBinding); + if (placeholder.isQuoted()) { + postProcessQuotedBinding(buffer, valueForBinding, + !binding.isExpression() ? accessor.getBindableValue(binding.getParameterIndex()) : null, + binding.isExpression()); } } @@ -134,8 +144,10 @@ class ExpressionEvaluatingParameterBinder { * * @param buffer the {@link StringBuffer} to operate upon. * @param valueForBinding the actual binding value. + * @param raw the raw binding value + * @param isExpression {@literal true} if the binding value results from a SpEL expression. */ - private void postProcessQuotedBinding(StringBuffer buffer, String valueForBinding) { + private void postProcessQuotedBinding(StringBuffer buffer, String valueForBinding, Object raw, boolean isExpression) { int quotationMarkIndex = buffer.length() - valueForBinding.length() - 1; char quotationMark = buffer.charAt(quotationMarkIndex); @@ -151,7 +163,8 @@ class ExpressionEvaluatingParameterBinder { quotationMark = buffer.charAt(quotationMarkIndex); } - if (valueForBinding.startsWith("{")) { // remove quotation char before the complex object string + // remove quotation char before the complex object string + if (valueForBinding.startsWith("{") && (raw instanceof DBObject || isExpression)) { buffer.deleteCharAt(quotationMarkIndex); @@ -181,7 +194,12 @@ class ExpressionEvaluatingParameterBinder { : accessor.getBindableValue(binding.getParameterIndex()); if (value instanceof String && binding.isQuoted()) { - return ((String) value).startsWith("{") ? (String) value : ((String) value).replace("\"", "\\\""); + + if (binding.isExpression() && ((String) value).startsWith("{")) { + return (String) value; + } + + return QuotedString.unquote(JSON.serialize(value)); } if (value instanceof byte[]) { @@ -228,8 +246,9 @@ class ExpressionEvaluatingParameterBinder { for (ParameterBinding binding : bindings) { regex.append("|"); - regex.append(Pattern.quote(binding.getParameter())); - regex.append("['\"]?"); // potential quotation char (as in { foo : '?0' }). + regex.append("(" + Pattern.quote(binding.getParameter()) + ")"); + regex.append("([\\w.]*"); + regex.append("(\\W?['\"]|\\w*')?)"); } return Pattern.compile(regex.substring(1)); @@ -239,14 +258,40 @@ class ExpressionEvaluatingParameterBinder { * Extract the placeholder stripping any trailing trailing quotation mark that might have resulted from the * {@link #createReplacementPattern(List) pattern} used. * - * @param groupName The actual {@link Matcher#group() group}. + * @param parameterIndex The actual parameter index. + * @param matcher The actual {@link Matcher}. * @return */ - private Placeholder extractPlaceholder(String groupName) { + private Placeholder extractPlaceholder(int parameterIndex, Matcher matcher) { - return !groupName.endsWith("'") && !groupName.endsWith("\"") ? // - Placeholder.of(groupName, false) : // - Placeholder.of(groupName.substring(0, groupName.length() - 1), true); + String rawPlaceholder = matcher.group(parameterIndex * 3 + 1); + String suffix = matcher.group(parameterIndex * 3 + 2); + + if (!StringUtils.hasText(rawPlaceholder)) { + + rawPlaceholder = matcher.group(); + if (rawPlaceholder.matches(".*\\d$")) { + suffix = ""; + } else { + int index = rawPlaceholder.replaceAll("[^\\?0-9]*$", "").length() - 1; + if (index > 0 && rawPlaceholder.length() > index) { + suffix = rawPlaceholder.substring(index + 1); + } + } + if (QuotedString.endsWithQuote(rawPlaceholder)) { + rawPlaceholder = rawPlaceholder.substring(0, + rawPlaceholder.length() - (StringUtils.hasText(suffix) ? suffix.length() : 1)); + } + } + + if (StringUtils.hasText(suffix)) { + + boolean quoted = QuotedString.endsWithQuote(suffix); + + return Placeholder.of(parameterIndex, rawPlaceholder, quoted, + quoted ? QuotedString.unquoteSuffix(suffix) : suffix); + } + return Placeholder.of(parameterIndex, rawPlaceholder, false, null); } /** @@ -317,8 +362,9 @@ class ExpressionEvaluatingParameterBinder { Map map = new LinkedHashMap(bindings.size(), 1); + int parameterIndex = 0; for (ParameterBinding binding : bindings) { - map.put(Placeholder.of(binding.getParameter(), binding.isQuoted()), binding); + map.put(Placeholder.of(parameterIndex++, binding.getParameter(), binding.isQuoted(), null), binding); } return map; @@ -332,10 +378,13 @@ class ExpressionEvaluatingParameterBinder { * @since 1.9 */ @Value(staticConstructor = "of") + @EqualsAndHashCode(exclude = { "quoted", "suffix" }) static class Placeholder { + private int parameterIndex; private final String parameter; private final boolean quoted; + private final String suffix; /* * (non-Javadoc) @@ -343,7 +392,46 @@ class ExpressionEvaluatingParameterBinder { */ @Override public String toString() { - return quoted ? String.format("'%s'", parameter) : parameter; + return quoted ? String.format("'%s'", parameter + (suffix != null ? suffix : "")) + : parameter + (suffix != null ? suffix : ""); + } + + } + + /** + * Utility to handle quoted strings using single/double quotes. + * + * @author Mark Paluch + */ + @UtilityClass + static class QuotedString { + + /** + * @param string + * @return {@literal true} if {@literal string} ends with a single/double quote. + */ + static boolean endsWithQuote(String string) { + return string.endsWith("'") || string.endsWith("\""); + } + + /** + * Remove trailing quoting from {@literal quoted}. + * + * @param quoted + * @return {@literal quoted} with removed quotes. + */ + public static String unquoteSuffix(String quoted) { + return quoted.substring(0, quoted.length() - 1); + } + + /** + * Remove leading and trailing quoting from {@literal quoted}. + * + * @param quoted + * @return {@literal quoted} with removed quotes. + */ + public static String unquote(String quoted) { + return quoted.substring(1, quoted.length() - 1); } } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java index 33d2e15e7..88c3c31d4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2015 the original author or authors. + * Copyright 2011-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,10 +38,11 @@ import com.mongodb.util.JSON; /** * Query to use a plain JSON String to create the {@link Query} to actually execute. - * + * * @author Oliver Gierke * @author Christoph Strobl * @author Thomas Darimont + * @author Mark Paluch */ public class StringBasedMongoQuery extends AbstractMongoQuery { @@ -59,7 +60,7 @@ public class StringBasedMongoQuery extends AbstractMongoQuery { /** * Creates a new {@link StringBasedMongoQuery} for the given {@link MongoQueryMethod} and {@link MongoOperations}. - * + * * @param method must not be {@literal null}. * @param mongoOperations must not be {@literal null}. * @param expressionParser must not be {@literal null}. @@ -126,7 +127,7 @@ public class StringBasedMongoQuery extends AbstractMongoQuery { return query; } - /* + /* * (non-Javadoc) * @see org.springframework.data.mongodb.repository.query.AbstractMongoQuery#isCountQuery() */ @@ -146,10 +147,10 @@ public class StringBasedMongoQuery extends AbstractMongoQuery { /** * A parser that extracts the parameter bindings from a given query string. - * + * * @author Thomas Darimont */ - private static enum ParameterBindingParser { + private enum ParameterBindingParser { INSTANCE; @@ -169,7 +170,7 @@ public class StringBasedMongoQuery extends AbstractMongoQuery { /** * Returns a list of {@link ParameterBinding}s found in the given {@code input} or an * {@link Collections#emptyList()}. - * + * * @param input can be {@literal null} or empty. * @param bindings must not be {@literal null}. * @return @@ -256,7 +257,7 @@ public class StringBasedMongoQuery extends AbstractMongoQuery { } else if (value instanceof Pattern) { - String string = ((Pattern) value).toString().trim(); + String string = value.toString().trim(); Matcher valueMatcher = PARSEABLE_BINDING_PATTERN.matcher(string); while (valueMatcher.find()) { @@ -264,7 +265,7 @@ public class StringBasedMongoQuery extends AbstractMongoQuery { int paramIndex = Integer.parseInt(valueMatcher.group(PARAMETER_INDEX_GROUP)); /* - * The pattern is used as a direct parameter replacement, e.g. 'field': ?1, + * The pattern is used as a direct parameter replacement, e.g. 'field': ?1, * therefore we treat it as not quoted to remain backwards compatible. */ boolean quoted = !string.equals(PARAMETER_PREFIX + paramIndex); @@ -297,8 +298,7 @@ public class StringBasedMongoQuery extends AbstractMongoQuery { while (valueMatcher.find()) { int paramIndex = Integer.parseInt(valueMatcher.group(PARAMETER_INDEX_GROUP)); - boolean quoted = (source.startsWith("'") && source.endsWith("'")) - || (source.startsWith("\"") && source.endsWith("\"")); + boolean quoted = source.startsWith("'") || source.startsWith("\""); bindings.add(new ParameterBinding(paramIndex, quoted)); } @@ -315,7 +315,7 @@ public class StringBasedMongoQuery extends AbstractMongoQuery { /** * A generic parameter binding with name or position information. - * + * * @author Thomas Darimont */ static class ParameterBinding { @@ -326,7 +326,7 @@ public class StringBasedMongoQuery extends AbstractMongoQuery { /** * Creates a new {@link ParameterBinding} with the given {@code parameterIndex} and {@code quoted} information. - * + * * @param parameterIndex * @param quoted whether or not the parameter is already quoted. */ diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQueryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQueryUnitTests.java index 0cc59df1b..9e9827d96 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQueryUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQueryUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2016 the original author or authors. + * Copyright 2011-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import java.lang.reflect.Method; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.regex.Pattern; import javax.xml.bind.DatatypeConverter; @@ -59,7 +60,7 @@ import com.mongodb.util.JSON; /** * Unit tests for {@link StringBasedMongoQuery}. - * + * * @author Oliver Gierke * @author Christoph Strobl * @author Thomas Darimont @@ -150,7 +151,7 @@ public class StringBasedMongoQueryUnitTests { public void bindsDbrefCorrectly() throws Exception { StringBasedMongoQuery mongoQuery = createQueryForMethod("findByHavingSizeFansNotZero"); - ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter, new Object[] {}); + ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter); org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accessor); assertThat(query.getQueryObject(), is(new BasicQuery("{ fans : { $not : { $size : 0 } } }").getQueryObject())); @@ -198,7 +199,7 @@ public class StringBasedMongoQueryUnitTests { @Test public void shouldSupportRespectExistingQuotingInFindByTitleBeginsWithExplicitQuoting() throws Exception { - ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter, new Object[] { "fun" }); + ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter, "fun"); StringBasedMongoQuery mongoQuery = createQueryForMethod("findByTitleBeginsWithExplicitQuoting", String.class); org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accessor); @@ -212,7 +213,7 @@ public class StringBasedMongoQueryUnitTests { @Test public void shouldParseQueryWithParametersInExpression() throws Exception { - ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter, new Object[] { 1, 2, 3, 4 }); + ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter, 1, 2, 3, 4); StringBasedMongoQuery mongoQuery = createQueryForMethod("findByQueryWithParametersInExpression", int.class, int.class, int.class, int.class); @@ -407,9 +408,9 @@ public class StringBasedMongoQueryUnitTests { public void shouldQuoteStringReplacementCorrectly() throws Exception { StringBasedMongoQuery mongoQuery = createQueryForMethod("findByLastnameQuoted", String.class); - ConvertingParameterAccessor accesor = StubParameterAccessor.getAccessor(converter, "Matthews', password: 'foo"); + ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter, "Matthews', password: 'foo"); - org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accesor); + org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accessor); assertThat(query.getQueryObject(), is(not(new BasicDBObjectBuilder().add("lastname", "Matthews").add("password", "foo").get()))); assertThat(query.getQueryObject(), is((DBObject) new BasicDBObject("lastname", "Matthews', password: 'foo"))); @@ -422,9 +423,9 @@ public class StringBasedMongoQueryUnitTests { public void shouldQuoteStringReplacementContainingQuotesCorrectly() throws Exception { StringBasedMongoQuery mongoQuery = createQueryForMethod("findByLastnameQuoted", String.class); - ConvertingParameterAccessor accesor = StubParameterAccessor.getAccessor(converter, "Matthews\", password: \"foo"); + ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter, "Matthews\", password: \"foo"); - org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accesor); + org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accessor); assertThat(query.getQueryObject(), is(not(new BasicDBObjectBuilder().add("lastname", "Matthews").add("password", "foo").get()))); assertThat(query.getQueryObject(), is((DBObject) new BasicDBObject("lastname", "Matthews\", password: \"foo"))); @@ -437,10 +438,10 @@ public class StringBasedMongoQueryUnitTests { public void shouldQuoteStringReplacementWithQuotationsCorrectly() throws Exception { StringBasedMongoQuery mongoQuery = createQueryForMethod("findByLastnameQuoted", String.class); - ConvertingParameterAccessor accesor = StubParameterAccessor.getAccessor(converter, + ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter, "\"Dave Matthews\", password: 'foo"); - org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accesor); + org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accessor); assertThat(query.getQueryObject(), is((DBObject) new BasicDBObject("lastname", "\"Dave Matthews\", password: 'foo"))); } @@ -452,11 +453,10 @@ public class StringBasedMongoQueryUnitTests { public void shouldQuoteComplexQueryStringCorreclty() throws Exception { StringBasedMongoQuery mongoQuery = createQueryForMethod("findByLastnameQuoted", String.class); - ConvertingParameterAccessor accesor = StubParameterAccessor.getAccessor(converter, "{ $ne : \"calamity\" }"); + ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter, "{ $ne : \"calamity\" }"); - org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accesor); - assertThat(query.getQueryObject(), - is((DBObject) new BasicDBObject("lastname", new BasicDBObject("$ne", "calamity")))); + org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accessor); + assertThat(query.getQueryObject(), is((DBObject) new BasicDBObject("lastname", "{ $ne : \"calamity\" }"))); } /** @@ -466,12 +466,119 @@ public class StringBasedMongoQueryUnitTests { public void shouldQuotationInQuotedComplexQueryString() throws Exception { StringBasedMongoQuery mongoQuery = createQueryForMethod("findByLastnameQuoted", String.class); - ConvertingParameterAccessor accesor = StubParameterAccessor.getAccessor(converter, + ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter, "{ $ne : \"\\\"calamity\\\"\" }"); - org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accesor); + org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accessor); + assertThat(query.getQueryObject(), is((DBObject) new BasicDBObject("lastname", "{ $ne : \"\\\"calamity\\\"\" }"))); + } + + /** + * @see DATAMONGO-1575 + */ + @Test + public void shouldTakeBsonParameterAsIs() throws Exception { + + StringBasedMongoQuery mongoQuery = createQueryForMethod("findByWithBsonArgument", DBObject.class); + ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter, + new BasicDBObject("$regex", "^calamity$")); + + org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accessor); + assertThat(query.getQueryObject(), is((DBObject) new BasicDBObject("arg0", Pattern.compile("^calamity$")))); + } + + /** + * @see DATAMONGO-1575 + */ + @Test + public void shouldReplaceParametersInInQuotedExpressionOfNestedQueryOperator() throws Exception { + + StringBasedMongoQuery mongoQuery = createQueryForMethod("findByLastnameRegex", String.class); + ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter, "calamity"); + + org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accessor); + assertThat(query.getQueryObject(), is((DBObject) new BasicDBObject("lastname", Pattern.compile("^(calamity)")))); + } + + /** + * @see DATAMONGO-1603 + */ + @Test + public void shouldAllowReuseOfPlaceholderWithinQuery() throws Exception { + + StringBasedMongoQuery mongoQuery = createQueryForMethod("findByReusingPlaceholdersMultipleTimes", String.class, + String.class); + ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter, "calamity", "regalia"); + + org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accessor); + assertThat(query.getQueryObject(), is((DBObject) new BasicDBObject().append("arg0", "calamity") + .append("arg1", "regalia").append("arg2", "calamity"))); + } + + /** + * @see DATAMONGO-1575 + */ + @Test + public void shouldAllowReuseOfQuotedPlaceholderWithinQuery() throws Exception { + + StringBasedMongoQuery mongoQuery = createQueryForMethod("findByReusingPlaceholdersMultipleTimesWhenQuoted", + String.class, String.class); + ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter, "calamity", "regalia"); + + org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accessor); + assertThat(query.getQueryObject(), is((DBObject) new BasicDBObject().append("arg0", "calamity") + .append("arg1", "regalia").append("arg2", "calamity"))); + } + + /** + * @see DATAMONGO-1575 + */ + @Test + public void shouldAllowReuseOfQuotedPlaceholderWithinQueryAndIncludeSuffixCorrectly() throws Exception { + + StringBasedMongoQuery mongoQuery = createQueryForMethod( + "findByReusingPlaceholdersMultipleTimesWhenQuotedAndSomeStuffAppended", String.class, String.class); + ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter, "calamity", "regalia"); + + org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accessor); + assertThat(query.getQueryObject(), is((DBObject) new BasicDBObject().append("arg0", "calamity") + .append("arg1", "regalia").append("arg2", "calamitys"))); + } + + @Test // DATAMONGO-1603 + public void shouldAllowQuotedParameterWithSuffixAppended() throws Exception { + + StringBasedMongoQuery mongoQuery = createQueryForMethod("findByWhenQuotedAndSomeStuffAppended", String.class, + String.class); + ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter, "calamity", "regalia"); + + org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accessor); assertThat(query.getQueryObject(), - is((DBObject) new BasicDBObject("lastname", new BasicDBObject("$ne", "\"calamity\"")))); + is((DBObject) new BasicDBObject().append("arg0", "calamity").append("arg1", "regalias"))); + } + + @Test // DATAMONGO-1603 + public void shouldCaptureReplacementWithComplexSuffixCorrectly() throws Exception { + + StringBasedMongoQuery mongoQuery = createQueryForMethod("findByMultiRegex", String.class); + ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter, "calamity"); + + org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accessor); + + assertThat(query.getQueryObject(), is((DBObject) JSON.parse( + "{ \"$or\" : [ { \"firstname\" : { \"$regex\" : \".*calamity.*\" , \"$options\" : \"i\"}} , { \"lastname\" : { \"$regex\" : \".*calamityxyz.*\" , \"$options\" : \"i\"}}]}"))); + } + + @Test // DATAMONGO-1603 + public void shouldAllowPlaceholderReuseInQuotedValue() throws Exception { + + StringBasedMongoQuery mongoQuery = createQueryForMethod("findByLastnameRegex", String.class, String.class); + ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter, "calamity", "regalia"); + + org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accessor); + + assertThat(query.getQueryObject(), + is((DBObject) JSON.parse("{ 'lastname' : { '$regex' : '^(calamity|John regalia|regalia)'} }"))); } private StringBasedMongoQuery createQueryForMethod(String name, Class... parameters) throws Exception { @@ -494,6 +601,12 @@ public class StringBasedMongoQueryUnitTests { @Query("{ 'lastname' : '?0' }") Person findByLastnameQuoted(String lastname); + @Query("{ 'lastname' : { '$regex' : '^(?0)'} }") + Person findByLastnameRegex(String lastname); + + @Query("{'$or' : [{'firstname': {'$regex': '.*?0.*', '$options': 'i'}}, {'lastname' : {'$regex': '.*?0xyz.*', '$options': 'i'}} ]}") + Person findByMultiRegex(String arg0); + @Query("{ 'address' : ?0 }") Person findByAddress(Address address); @@ -538,5 +651,23 @@ public class StringBasedMongoQueryUnitTests { @Query("{ 'arg0' : ?0, 'arg1' : ?1 }") List findByStringWithWildcardChar(String arg0, String arg1); + + @Query("{ 'arg0' : ?0 }") + List findByWithBsonArgument(DBObject arg0); + + @Query("{ 'arg0' : ?0, 'arg1' : ?1, 'arg2' : ?0 }") + List findByReusingPlaceholdersMultipleTimes(String arg0, String arg1); + + @Query("{ 'arg0' : ?0, 'arg1' : ?1, 'arg2' : '?0' }") + List findByReusingPlaceholdersMultipleTimesWhenQuoted(String arg0, String arg1); + + @Query("{ 'arg0' : '?0', 'arg1' : ?1, 'arg2' : '?0s' }") + List findByReusingPlaceholdersMultipleTimesWhenQuotedAndSomeStuffAppended(String arg0, String arg1); + + @Query("{ 'arg0' : '?0', 'arg1' : '?1s' }") + List findByWhenQuotedAndSomeStuffAppended(String arg0, String arg1); + + @Query("{ 'lastname' : { '$regex' : '^(?0|John ?1|?1)'} }") // use spel or some regex string this is fucking bad + Person findByLastnameRegex(String lastname, String alternative); } }