Skip to content

Commit 8068e36

Browse files
christophstroblmp911de
authored andcommitted
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. Original pull request: #441.
1 parent c62d131 commit 8068e36

File tree

3 files changed

+116
-14
lines changed

3 files changed

+116
-14
lines changed

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ExpressionEvaluatingParameterBinder.java

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
/**
4646
* {@link ExpressionEvaluatingParameterBinder} allows to evaluate, convert and bind parameters to placeholders within a
4747
* {@link String}.
48-
*
48+
*
4949
* @author Christoph Strobl
5050
* @author Thomas Darimont
5151
* @author Oliver Gierke
@@ -59,7 +59,7 @@ class ExpressionEvaluatingParameterBinder {
5959

6060
/**
6161
* Creates new {@link ExpressionEvaluatingParameterBinder}
62-
*
62+
*
6363
* @param expressionParser must not be {@literal null}.
6464
* @param evaluationContextProvider must not be {@literal null}.
6565
*/
@@ -76,7 +76,7 @@ public ExpressionEvaluatingParameterBinder(SpelExpressionParser expressionParser
7676
/**
7777
* Bind values provided by {@link MongoParameterAccessor} to placeholders in {@literal raw} while considering
7878
* potential conversions and parameter types.
79-
*
79+
*
8080
* @param raw can be {@literal null} or empty.
8181
* @param accessor must not be {@literal null}.
8282
* @param bindingContext must not be {@literal null}.
@@ -93,7 +93,7 @@ public String bind(String raw, MongoParameterAccessor accessor, BindingContext b
9393

9494
/**
9595
* Replaced the parameter placeholders with the actual parameter values from the given {@link ParameterBinding}s.
96-
*
96+
*
9797
* @param input must not be {@literal null} or empty.
9898
* @param accessor must not be {@literal null}.
9999
* @param bindingContext must not be {@literal null}.
@@ -126,7 +126,7 @@ private String replacePlaceholders(String input, MongoParameterAccessor accessor
126126
buffer.append(placeholder.getSuffix());
127127
}
128128

129-
if (binding.isQuoted() || placeholder.isQuoted()) {
129+
if (placeholder.isQuoted()) {
130130
postProcessQuotedBinding(buffer, valueForBinding,
131131
!binding.isExpression() ? accessor.getBindableValue(binding.getParameterIndex()) : null,
132132
binding.isExpression());
@@ -247,7 +247,8 @@ private Pattern createReplacementPattern(List<ParameterBinding> bindings) {
247247

248248
regex.append("|");
249249
regex.append("(" + Pattern.quote(binding.getParameter()) + ")");
250-
regex.append("(\\W?['\"])?"); // potential quotation char (as in { foo : '?0' }).
250+
regex.append("([\\w.]*");
251+
regex.append("(\\W?['\"]|\\w*')?)");
251252
}
252253

253254
return Pattern.compile(regex.substring(1));
@@ -265,15 +266,22 @@ private Placeholder extractPlaceholder(int parameterIndex, Matcher matcher) {
265266

266267
if (matcher.groupCount() > 1) {
267268

268-
String rawPlaceholder = matcher.group(parameterIndex * 2 + 1);
269-
String suffix = matcher.group(parameterIndex * 2 + 2);
269+
String rawPlaceholder = matcher.group(parameterIndex * 3 + 1);
270+
String suffix = matcher.group(parameterIndex * 3 + 2);
270271

271272
if (!StringUtils.hasText(rawPlaceholder)) {
272273

273274
rawPlaceholder = matcher.group();
274-
suffix = "" + rawPlaceholder.charAt(rawPlaceholder.length() - 1);
275+
if(rawPlaceholder.matches(".*\\d$")) {
276+
suffix = "";
277+
} else {
278+
int index = rawPlaceholder.replaceAll("[^\\?0-9]*$", "").length() - 1;
279+
if (index > 0 && rawPlaceholder.length() > index) {
280+
suffix = rawPlaceholder.substring(index+1);
281+
}
282+
}
275283
if (QuotedString.endsWithQuote(rawPlaceholder)) {
276-
rawPlaceholder = QuotedString.unquoteSuffix(rawPlaceholder);
284+
rawPlaceholder = rawPlaceholder.substring(0, rawPlaceholder.length() - (StringUtils.hasText(suffix) ? suffix.length() : 1));
277285
}
278286
}
279287

@@ -284,6 +292,7 @@ private Placeholder extractPlaceholder(int parameterIndex, Matcher matcher) {
284292
return Placeholder.of(parameterIndex, rawPlaceholder, quoted,
285293
quoted ? QuotedString.unquoteSuffix(suffix) : suffix);
286294
}
295+
return Placeholder.of(parameterIndex, rawPlaceholder, false, null);
287296
}
288297

289298
return Placeholder.of(parameterIndex, matcher.group(), false, null);

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,8 @@ protected boolean isDeleteQuery() {
168168
return this.isDeleteQuery;
169169
}
170170

171-
private static boolean hasAmbiguousProjectionFlags(boolean isCountQuery, boolean isExistsQuery, boolean isDeleteQuery) {
171+
private static boolean hasAmbiguousProjectionFlags(boolean isCountQuery, boolean isExistsQuery,
172+
boolean isDeleteQuery) {
172173
return countBooleanValues(isCountQuery, isExistsQuery, isDeleteQuery) > 1;
173174
}
174175

@@ -347,8 +348,7 @@ private static void potentiallyAddBinding(String source, List<ParameterBinding>
347348
while (valueMatcher.find()) {
348349

349350
int paramIndex = Integer.parseInt(valueMatcher.group(PARAMETER_INDEX_GROUP));
350-
boolean quoted = (source.startsWith("'") && source.endsWith("'"))
351-
|| (source.startsWith("\"") && source.endsWith("\""));
351+
boolean quoted = source.startsWith("'") || source.startsWith("\"");
352352

353353
bindings.add(new ParameterBinding(paramIndex, quoted));
354354
}

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQueryUnitTests.java

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828

2929
import javax.xml.bind.DatatypeConverter;
3030

31+
import com.mongodb.BasicDBObject;
32+
import com.mongodb.DBObject;
33+
import com.mongodb.util.JSON;
3134
import org.bson.BSON;
3235
import org.bson.BsonRegularExpression;
3336
import org.bson.Document;
@@ -56,7 +59,7 @@
5659

5760
/**
5861
* Unit tests for {@link StringBasedMongoQuery}.
59-
*
62+
*
6063
* @author Oliver Gierke
6164
* @author Christoph Strobl
6265
* @author Thomas Darimont
@@ -435,6 +438,78 @@ public void shouldReplaceParametersInInQuotedExpressionOfNestedQueryOperator() t
435438
assertThat(query.getQueryObject(), is(new Document("lastname", new BsonRegularExpression("^(calamity)"))));
436439
}
437440

441+
@Test // DATAMONGO-1603
442+
public void shouldAllowReuseOfPlaceholderWithinQuery() throws Exception {
443+
444+
StringBasedMongoQuery mongoQuery = createQueryForMethod("findByReusingPlaceholdersMultipleTimes", String.class,
445+
String.class);
446+
ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter, "calamity", "regalia");
447+
448+
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accessor);
449+
assertThat(query.getQueryObject(), is(new Document().append("arg0", "calamity")
450+
.append("arg1", "regalia").append("arg2", "calamity")));
451+
}
452+
453+
@Test // DATAMONGO-1603
454+
public void shouldAllowReuseOfQuotedPlaceholderWithinQuery() throws Exception {
455+
456+
StringBasedMongoQuery mongoQuery = createQueryForMethod("findByReusingPlaceholdersMultipleTimesWhenQuoted",
457+
String.class, String.class);
458+
ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter, "calamity", "regalia");
459+
460+
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accessor);
461+
assertThat(query.getQueryObject(), is(new Document().append("arg0", "calamity")
462+
.append("arg1", "regalia").append("arg2", "calamity")));
463+
}
464+
465+
@Test // DATAMONGO-1603
466+
public void shouldAllowReuseOfQuotedPlaceholderWithinQueryAndIncludeSuffixCorrectly() throws Exception {
467+
468+
StringBasedMongoQuery mongoQuery = createQueryForMethod(
469+
"findByReusingPlaceholdersMultipleTimesWhenQuotedAndSomeStuffAppended", String.class, String.class);
470+
ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter, "calamity", "regalia");
471+
472+
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accessor);
473+
assertThat(query.getQueryObject(), is(new Document().append("arg0", "calamity")
474+
.append("arg1", "regalia").append("arg2", "calamitys")));
475+
}
476+
477+
@Test // DATAMONGO-1603
478+
public void shouldAllowQuotedParameterWithSuffixAppended() throws Exception {
479+
480+
StringBasedMongoQuery mongoQuery = createQueryForMethod("findByWhenQuotedAndSomeStuffAppended", String.class,
481+
String.class);
482+
ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter, "calamity", "regalia");
483+
484+
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accessor);
485+
assertThat(query.getQueryObject(),
486+
is(new Document().append("arg0", "calamity").append("arg1", "regalias")));
487+
}
488+
489+
@Test // DATAMONGO-1603
490+
public void shouldCaptureReplacementWithComplexSuffixCorrectly() throws Exception {
491+
492+
StringBasedMongoQuery mongoQuery = createQueryForMethod("findByMultiRegex", String.class);
493+
ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter, "calamity");
494+
495+
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accessor);
496+
497+
assertThat(query.getQueryObject(), is(Document.parse(
498+
"{ \"$or\" : [ { \"firstname\" : { \"$regex\" : \".*calamity.*\" , \"$options\" : \"i\"}} , { \"lastname\" : { \"$regex\" : \".*calamityxyz.*\" , \"$options\" : \"i\"}}]}")));
499+
}
500+
501+
@Test // DATAMONGO-1603
502+
public void shouldAllowPlaceholderReuseInQuotedValue() throws Exception {
503+
504+
StringBasedMongoQuery mongoQuery = createQueryForMethod("findByLastnameRegex", String.class, String.class);
505+
ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter, "calamity", "regalia");
506+
507+
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accessor);
508+
509+
assertThat(query.getQueryObject(),
510+
is(Document.parse("{ 'lastname' : { '$regex' : '^(calamity|John regalia|regalia)'} }")));
511+
}
512+
438513
private StringBasedMongoQuery createQueryForMethod(String name, Class<?>... parameters) throws Exception {
439514

440515
Method method = SampleRepository.class.getMethod(name, parameters);
@@ -458,6 +533,9 @@ private interface SampleRepository extends Repository<Person, Long> {
458533
@Query("{ 'lastname' : { '$regex' : '^(?0)'} }")
459534
Person findByLastnameRegex(String lastname);
460535

536+
@Query("{'$or' : [{'firstname': {'$regex': '.*?0.*', '$options': 'i'}}, {'lastname' : {'$regex': '.*?0xyz.*', '$options': 'i'}} ]}")
537+
Person findByMultiRegex(String arg0);
538+
461539
@Query("{ 'address' : ?0 }")
462540
Person findByAddress(Address address);
463541

@@ -508,5 +586,20 @@ private interface SampleRepository extends Repository<Person, Long> {
508586

509587
@Query("{ 'arg0' : ?0 }")
510588
List<Person> findByWithBsonArgument(Document arg0);
589+
590+
@Query("{ 'arg0' : ?0, 'arg1' : ?1, 'arg2' : ?0 }")
591+
List<Person> findByReusingPlaceholdersMultipleTimes(String arg0, String arg1);
592+
593+
@Query("{ 'arg0' : ?0, 'arg1' : ?1, 'arg2' : '?0' }")
594+
List<Person> findByReusingPlaceholdersMultipleTimesWhenQuoted(String arg0, String arg1);
595+
596+
@Query("{ 'arg0' : '?0', 'arg1' : ?1, 'arg2' : '?0s' }")
597+
List<Person> findByReusingPlaceholdersMultipleTimesWhenQuotedAndSomeStuffAppended(String arg0, String arg1);
598+
599+
@Query("{ 'arg0' : '?0', 'arg1' : '?1s' }")
600+
List<Person> findByWhenQuotedAndSomeStuffAppended(String arg0, String arg1);
601+
602+
@Query("{ 'lastname' : { '$regex' : '^(?0|John ?1|?1)'} }") // use spel or some regex string this is fucking bad
603+
Person findByLastnameRegex(String lastname, String alternative);
511604
}
512605
}

0 commit comments

Comments
 (0)