Skip to content

Commit 39ba28c

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. This backport contains elements from DATAMONGO-1575. Original pull request: #441. Related ticket: DATAMONGO-1575.
1 parent 7a32d40 commit 39ba28c

File tree

3 files changed

+271
-52
lines changed

3 files changed

+271
-52
lines changed

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

Lines changed: 109 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2015-2016 the original author or authors.
2+
* Copyright 2015-2017 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -15,7 +15,9 @@
1515
*/
1616
package org.springframework.data.mongodb.repository.query;
1717

18+
import lombok.EqualsAndHashCode;
1819
import lombok.Value;
20+
import lombok.experimental.UtilityClass;
1921

2022
import java.util.ArrayList;
2123
import java.util.LinkedHashMap;
@@ -37,12 +39,13 @@
3739
import org.springframework.util.CollectionUtils;
3840
import org.springframework.util.StringUtils;
3941

42+
import com.mongodb.DBObject;
4043
import com.mongodb.util.JSON;
4144

4245
/**
43-
* {@link ExpressionEvaluatingParameterBinder} allows to evaluate, convert and bind parameters to placholders within a
46+
* {@link ExpressionEvaluatingParameterBinder} allows to evaluate, convert and bind parameters to placeholders within a
4447
* {@link String}.
45-
*
48+
*
4649
* @author Christoph Strobl
4750
* @author Thomas Darimont
4851
* @author Oliver Gierke
@@ -56,7 +59,7 @@ class ExpressionEvaluatingParameterBinder {
5659

5760
/**
5861
* Creates new {@link ExpressionEvaluatingParameterBinder}
59-
*
62+
*
6063
* @param expressionParser must not be {@literal null}.
6164
* @param evaluationContextProvider must not be {@literal null}.
6265
*/
@@ -73,7 +76,7 @@ public ExpressionEvaluatingParameterBinder(SpelExpressionParser expressionParser
7376
/**
7477
* Bind values provided by {@link MongoParameterAccessor} to placeholders in {@literal raw} while considering
7578
* potential conversions and parameter types.
76-
*
79+
*
7780
* @param raw can be {@literal null} or empty.
7881
* @param accessor must not be {@literal null}.
7982
* @param bindingContext must not be {@literal null}.
@@ -90,7 +93,7 @@ public String bind(String raw, MongoParameterAccessor accessor, BindingContext b
9093

9194
/**
9295
* Replaced the parameter placeholders with the actual parameter values from the given {@link ParameterBinding}s.
93-
*
96+
*
9497
* @param input must not be {@literal null} or empty.
9598
* @param accessor must not be {@literal null}.
9699
* @param bindingContext must not be {@literal null}.
@@ -110,16 +113,23 @@ private String replacePlaceholders(String input, MongoParameterAccessor accessor
110113
Matcher matcher = createReplacementPattern(bindingContext.getBindings()).matcher(input);
111114
StringBuffer buffer = new StringBuffer();
112115

116+
int parameterIndex = 0;
113117
while (matcher.find()) {
114118

115-
ParameterBinding binding = bindingContext.getBindingFor(extractPlaceholder(matcher.group()));
119+
Placeholder placeholder = extractPlaceholder(parameterIndex++, matcher);
120+
ParameterBinding binding = bindingContext.getBindingFor(placeholder);
116121
String valueForBinding = getParameterValueForBinding(accessor, bindingContext.getParameters(), binding);
117122

118123
// appendReplacement does not like unescaped $ sign and others, so we need to quote that stuff first
119124
matcher.appendReplacement(buffer, Matcher.quoteReplacement(valueForBinding));
125+
if (StringUtils.hasText(placeholder.getSuffix())) {
126+
buffer.append(placeholder.getSuffix());
127+
}
120128

121-
if (binding.isQuoted()) {
122-
postProcessQuotedBinding(buffer, valueForBinding);
129+
if (placeholder.isQuoted()) {
130+
postProcessQuotedBinding(buffer, valueForBinding,
131+
!binding.isExpression() ? accessor.getBindableValue(binding.getParameterIndex()) : null,
132+
binding.isExpression());
123133
}
124134
}
125135

@@ -134,8 +144,10 @@ private String replacePlaceholders(String input, MongoParameterAccessor accessor
134144
*
135145
* @param buffer the {@link StringBuffer} to operate upon.
136146
* @param valueForBinding the actual binding value.
147+
* @param raw the raw binding value
148+
* @param isExpression {@literal true} if the binding value results from a SpEL expression.
137149
*/
138-
private void postProcessQuotedBinding(StringBuffer buffer, String valueForBinding) {
150+
private void postProcessQuotedBinding(StringBuffer buffer, String valueForBinding, Object raw, boolean isExpression) {
139151

140152
int quotationMarkIndex = buffer.length() - valueForBinding.length() - 1;
141153
char quotationMark = buffer.charAt(quotationMarkIndex);
@@ -151,7 +163,8 @@ private void postProcessQuotedBinding(StringBuffer buffer, String valueForBindin
151163
quotationMark = buffer.charAt(quotationMarkIndex);
152164
}
153165

154-
if (valueForBinding.startsWith("{")) { // remove quotation char before the complex object string
166+
// remove quotation char before the complex object string
167+
if (valueForBinding.startsWith("{") && (raw instanceof DBObject || isExpression)) {
155168

156169
buffer.deleteCharAt(quotationMarkIndex);
157170

@@ -181,7 +194,12 @@ private String getParameterValueForBinding(MongoParameterAccessor accessor, Mong
181194
: accessor.getBindableValue(binding.getParameterIndex());
182195

183196
if (value instanceof String && binding.isQuoted()) {
184-
return ((String) value).startsWith("{") ? (String) value : ((String) value).replace("\"", "\\\"");
197+
198+
if (binding.isExpression() && ((String) value).startsWith("{")) {
199+
return (String) value;
200+
}
201+
202+
return QuotedString.unquote(JSON.serialize(value));
185203
}
186204

187205
if (value instanceof byte[]) {
@@ -228,8 +246,9 @@ private Pattern createReplacementPattern(List<ParameterBinding> bindings) {
228246
for (ParameterBinding binding : bindings) {
229247

230248
regex.append("|");
231-
regex.append(Pattern.quote(binding.getParameter()));
232-
regex.append("['\"]?"); // potential quotation char (as in { foo : '?0' }).
249+
regex.append("(" + Pattern.quote(binding.getParameter()) + ")");
250+
regex.append("([\\w.]*");
251+
regex.append("(\\W?['\"]|\\w*')?)");
233252
}
234253

235254
return Pattern.compile(regex.substring(1));
@@ -239,14 +258,40 @@ private Pattern createReplacementPattern(List<ParameterBinding> bindings) {
239258
* Extract the placeholder stripping any trailing trailing quotation mark that might have resulted from the
240259
* {@link #createReplacementPattern(List) pattern} used.
241260
*
242-
* @param groupName The actual {@link Matcher#group() group}.
261+
* @param parameterIndex The actual parameter index.
262+
* @param matcher The actual {@link Matcher}.
243263
* @return
244264
*/
245-
private Placeholder extractPlaceholder(String groupName) {
265+
private Placeholder extractPlaceholder(int parameterIndex, Matcher matcher) {
266+
267+
String rawPlaceholder = matcher.group(parameterIndex * 3 + 1);
268+
String suffix = matcher.group(parameterIndex * 3 + 2);
269+
270+
if (!StringUtils.hasText(rawPlaceholder)) {
246271

247-
return !groupName.endsWith("'") && !groupName.endsWith("\"") ? //
248-
Placeholder.of(groupName, false) : //
249-
Placeholder.of(groupName.substring(0, groupName.length() - 1), true);
272+
rawPlaceholder = matcher.group();
273+
if (rawPlaceholder.matches(".*\\d$")) {
274+
suffix = "";
275+
} else {
276+
int index = rawPlaceholder.replaceAll("[^\\?0-9]*$", "").length() - 1;
277+
if (index > 0 && rawPlaceholder.length() > index) {
278+
suffix = rawPlaceholder.substring(index + 1);
279+
}
280+
}
281+
if (QuotedString.endsWithQuote(rawPlaceholder)) {
282+
rawPlaceholder = rawPlaceholder.substring(0,
283+
rawPlaceholder.length() - (StringUtils.hasText(suffix) ? suffix.length() : 1));
284+
}
285+
}
286+
287+
if (StringUtils.hasText(suffix)) {
288+
289+
boolean quoted = QuotedString.endsWithQuote(suffix);
290+
291+
return Placeholder.of(parameterIndex, rawPlaceholder, quoted,
292+
quoted ? QuotedString.unquoteSuffix(suffix) : suffix);
293+
}
294+
return Placeholder.of(parameterIndex, rawPlaceholder, false, null);
250295
}
251296

252297
/**
@@ -317,8 +362,9 @@ private static Map<Placeholder, ParameterBinding> mapBindings(List<ParameterBind
317362

318363
Map<Placeholder, ParameterBinding> map = new LinkedHashMap<Placeholder, ParameterBinding>(bindings.size(), 1);
319364

365+
int parameterIndex = 0;
320366
for (ParameterBinding binding : bindings) {
321-
map.put(Placeholder.of(binding.getParameter(), binding.isQuoted()), binding);
367+
map.put(Placeholder.of(parameterIndex++, binding.getParameter(), binding.isQuoted(), null), binding);
322368
}
323369

324370
return map;
@@ -332,18 +378,60 @@ private static Map<Placeholder, ParameterBinding> mapBindings(List<ParameterBind
332378
* @since 1.9
333379
*/
334380
@Value(staticConstructor = "of")
381+
@EqualsAndHashCode(exclude = { "quoted", "suffix" })
335382
static class Placeholder {
336383

384+
private int parameterIndex;
337385
private final String parameter;
338386
private final boolean quoted;
387+
private final String suffix;
339388

340389
/*
341390
* (non-Javadoc)
342391
* @see java.lang.Object#toString()
343392
*/
344393
@Override
345394
public String toString() {
346-
return quoted ? String.format("'%s'", parameter) : parameter;
395+
return quoted ? String.format("'%s'", parameter + (suffix != null ? suffix : ""))
396+
: parameter + (suffix != null ? suffix : "");
397+
}
398+
399+
}
400+
401+
/**
402+
* Utility to handle quoted strings using single/double quotes.
403+
*
404+
* @author Mark Paluch
405+
*/
406+
@UtilityClass
407+
static class QuotedString {
408+
409+
/**
410+
* @param string
411+
* @return {@literal true} if {@literal string} ends with a single/double quote.
412+
*/
413+
static boolean endsWithQuote(String string) {
414+
return string.endsWith("'") || string.endsWith("\"");
415+
}
416+
417+
/**
418+
* Remove trailing quoting from {@literal quoted}.
419+
*
420+
* @param quoted
421+
* @return {@literal quoted} with removed quotes.
422+
*/
423+
public static String unquoteSuffix(String quoted) {
424+
return quoted.substring(0, quoted.length() - 1);
425+
}
426+
427+
/**
428+
* Remove leading and trailing quoting from {@literal quoted}.
429+
*
430+
* @param quoted
431+
* @return {@literal quoted} with removed quotes.
432+
*/
433+
public static String unquote(String quoted) {
434+
return quoted.substring(1, quoted.length() - 1);
347435
}
348436
}
349437
}

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

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2011-2015 the original author or authors.
2+
* Copyright 2011-2017 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -38,10 +38,11 @@
3838

3939
/**
4040
* Query to use a plain JSON String to create the {@link Query} to actually execute.
41-
*
41+
*
4242
* @author Oliver Gierke
4343
* @author Christoph Strobl
4444
* @author Thomas Darimont
45+
* @author Mark Paluch
4546
*/
4647
public class StringBasedMongoQuery extends AbstractMongoQuery {
4748

@@ -59,7 +60,7 @@ public class StringBasedMongoQuery extends AbstractMongoQuery {
5960

6061
/**
6162
* Creates a new {@link StringBasedMongoQuery} for the given {@link MongoQueryMethod} and {@link MongoOperations}.
62-
*
63+
*
6364
* @param method must not be {@literal null}.
6465
* @param mongoOperations must not be {@literal null}.
6566
* @param expressionParser must not be {@literal null}.
@@ -126,7 +127,7 @@ protected Query createQuery(ConvertingParameterAccessor accessor) {
126127
return query;
127128
}
128129

129-
/*
130+
/*
130131
* (non-Javadoc)
131132
* @see org.springframework.data.mongodb.repository.query.AbstractMongoQuery#isCountQuery()
132133
*/
@@ -146,10 +147,10 @@ protected boolean isDeleteQuery() {
146147

147148
/**
148149
* A parser that extracts the parameter bindings from a given query string.
149-
*
150+
*
150151
* @author Thomas Darimont
151152
*/
152-
private static enum ParameterBindingParser {
153+
private enum ParameterBindingParser {
153154

154155
INSTANCE;
155156

@@ -169,7 +170,7 @@ private static enum ParameterBindingParser {
169170
/**
170171
* Returns a list of {@link ParameterBinding}s found in the given {@code input} or an
171172
* {@link Collections#emptyList()}.
172-
*
173+
*
173174
* @param input can be {@literal null} or empty.
174175
* @param bindings must not be {@literal null}.
175176
* @return
@@ -256,15 +257,15 @@ private static void collectParameterReferencesIntoBindings(List<ParameterBinding
256257

257258
} else if (value instanceof Pattern) {
258259

259-
String string = ((Pattern) value).toString().trim();
260+
String string = value.toString().trim();
260261
Matcher valueMatcher = PARSEABLE_BINDING_PATTERN.matcher(string);
261262

262263
while (valueMatcher.find()) {
263264

264265
int paramIndex = Integer.parseInt(valueMatcher.group(PARAMETER_INDEX_GROUP));
265266

266267
/*
267-
* The pattern is used as a direct parameter replacement, e.g. 'field': ?1,
268+
* The pattern is used as a direct parameter replacement, e.g. 'field': ?1,
268269
* therefore we treat it as not quoted to remain backwards compatible.
269270
*/
270271
boolean quoted = !string.equals(PARAMETER_PREFIX + paramIndex);
@@ -297,8 +298,7 @@ private static void potentiallyAddBinding(String source, List<ParameterBinding>
297298
while (valueMatcher.find()) {
298299

299300
int paramIndex = Integer.parseInt(valueMatcher.group(PARAMETER_INDEX_GROUP));
300-
boolean quoted = (source.startsWith("'") && source.endsWith("'"))
301-
|| (source.startsWith("\"") && source.endsWith("\""));
301+
boolean quoted = source.startsWith("'") || source.startsWith("\"");
302302

303303
bindings.add(new ParameterBinding(paramIndex, quoted));
304304
}
@@ -315,7 +315,7 @@ private static int getIndexOfExpressionParameter(String input, int position) {
315315

316316
/**
317317
* A generic parameter binding with name or position information.
318-
*
318+
*
319319
* @author Thomas Darimont
320320
*/
321321
static class ParameterBinding {
@@ -326,7 +326,7 @@ static class ParameterBinding {
326326

327327
/**
328328
* Creates a new {@link ParameterBinding} with the given {@code parameterIndex} and {@code quoted} information.
329-
*
329+
*
330330
* @param parameterIndex
331331
* @param quoted whether or not the parameter is already quoted.
332332
*/

0 commit comments

Comments
 (0)