Skip to content

Commit b1de52f

Browse files
DATAMONGO-1565 - Ignore placeholder pattern in replacement values for annotated queries.
We now make sure to quote single and double ticks in the replacement values before actually appending them to the query. We also replace single ticks around parameters in the actual raw annotated query by double quotes to make sure they are treated as a single string parameter.
1 parent 646b525 commit b1de52f

File tree

6 files changed

+229
-44
lines changed

6 files changed

+229
-44
lines changed

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.util.Map.Entry;
2424
import java.util.Set;
2525

26+
import org.bson.BsonValue;
2627
import org.bson.Document;
2728
import org.bson.conversions.Bson;
2829
import org.bson.types.ObjectId;
@@ -408,6 +409,10 @@ protected Object convertSimpleOrDocument(Object source, MongoPersistentEntity<?>
408409
return getMappedObject((BasicDBObject) source, entity);
409410
}
410411

412+
if(source instanceof BsonValue) {
413+
return source;
414+
}
415+
411416
return delegateConvertToMongoType(source, entity);
412417
}
413418

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/BasicQuery.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,9 @@ public BasicQuery(Document queryObject) {
6363
* @param fields may be {@literal null}.
6464
*/
6565
public BasicQuery(String query, String fields) {
66-
this.queryObject = query != null ? new Document(((DBObject) JSON.parse(query)).toMap()) : null;
67-
this.fieldsObject = fields != null ? new Document(((DBObject) JSON.parse(fields)).toMap()) : null;
66+
67+
this.queryObject = query != null ? Document.parse(query) : null;
68+
this.fieldsObject = fields != null ? Document.parse(fields) : null;
6869
}
6970

7071
/**

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

Lines changed: 118 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,13 @@
1515
*/
1616
package org.springframework.data.mongodb.repository.query;
1717

18-
import java.util.Collections;
18+
import java.util.ArrayList;
19+
import java.util.LinkedHashMap;
1920
import java.util.List;
21+
import java.util.Map;
22+
import java.util.NoSuchElementException;
23+
import java.util.regex.Matcher;
24+
import java.util.regex.Pattern;
2025

2126
import javax.xml.bind.DatatypeConverter;
2227

@@ -94,47 +99,67 @@ private String replacePlaceholders(String input, MongoParameterAccessor accessor
9499
return input;
95100
}
96101

97-
boolean isCompletlyParameterizedQuery = input.matches("^\\?\\d+$");
98-
StringBuilder result = new StringBuilder(input);
99-
100-
for (ParameterBinding binding : bindingContext.getBindings()) {
102+
if (input.matches("^\\?\\d+$")) {
103+
return getParameterValueForBinding(accessor, bindingContext.getParameters(),
104+
bindingContext.getBindings().iterator().next());
105+
}
101106

102-
String parameter = binding.getParameter();
103-
int idx = result.indexOf(parameter);
107+
Matcher matcher = createReplacementPattern(bindingContext.getBindings()).matcher(input);
108+
StringBuffer buffer = new StringBuffer();
104109

105-
if (idx == -1) {
106-
continue;
107-
}
110+
while (matcher.find()) {
108111

112+
ParameterBinding binding = bindingContext.getBindingFor(extractPlaceholder(matcher.group()));
109113
String valueForBinding = getParameterValueForBinding(accessor, bindingContext.getParameters(), binding);
110114

111-
int start = idx;
112-
int end = idx + parameter.length();
115+
// appendReplacement does not like unescaped $ sign and others, so we need to quote that stuff first
116+
matcher.appendReplacement(buffer, Matcher.quoteReplacement(valueForBinding));
117+
118+
if (binding.isQuoted()) {
119+
postProcessQuotedBinding(buffer, valueForBinding);
120+
}
121+
}
122+
123+
matcher.appendTail(buffer);
124+
return buffer.toString();
125+
}
113126

114-
// If the value to bind is an object literal we need to remove the quoting around the expression insertion point.
115-
if (valueForBinding.startsWith("{") && !isCompletlyParameterizedQuery) {
127+
/**
128+
* Sanitize String binding by replacing single quoted values with double quotes which prevents potential single quotes
129+
* contained in replacement to interfere with the Json parsing. Also take care of complex objects by removing the
130+
* quotation entirely.
131+
*
132+
* @param buffer the {@link StringBuffer} to operate upon.
133+
* @param valueForBinding the actual binding value.
134+
*/
135+
private void postProcessQuotedBinding(StringBuffer buffer, String valueForBinding) {
116136

117-
// Is the insertion point actually surrounded by quotes?
118-
char beforeStart = result.charAt(start - 1);
119-
char afterEnd = result.charAt(end);
137+
int quotationMarkIndex = buffer.length() - valueForBinding.length() - 1;
138+
char quotationMark = buffer.charAt(quotationMarkIndex);
120139

121-
if ((beforeStart == '\'' || beforeStart == '"') && (afterEnd == '\'' || afterEnd == '"')) {
140+
while (quotationMark != '\'' && quotationMark != '"') {
122141

123-
// Skip preceding and following quote
124-
start -= 1;
125-
end += 1;
126-
}
142+
quotationMarkIndex--;
143+
if (quotationMarkIndex < 0) {
144+
throw new IllegalArgumentException("Could not find opening quotes for quoted parameter");
127145
}
128-
129-
result.replace(start, end, valueForBinding);
146+
quotationMark = buffer.charAt(quotationMarkIndex);
130147
}
131148

132-
return result.toString();
149+
if (valueForBinding.startsWith("{")) { // remove quotation char before the complex object string
150+
buffer.deleteCharAt(quotationMarkIndex);
151+
} else {
152+
153+
if (quotationMark == '\'') {
154+
buffer.replace(quotationMarkIndex, quotationMarkIndex + 1, "\"");
155+
}
156+
buffer.append("\"");
157+
}
133158
}
134159

135160
/**
136161
* Returns the serialized value to be used for the given {@link ParameterBinding}.
137-
*
162+
*
138163
* @param accessor must not be {@literal null}.
139164
* @param parameters
140165
* @param binding must not be {@literal null}.
@@ -148,15 +173,15 @@ private String getParameterValueForBinding(MongoParameterAccessor accessor, Mong
148173
: accessor.getBindableValue(binding.getParameterIndex());
149174

150175
if (value instanceof String && binding.isQuoted()) {
151-
return (String) value;
176+
return ((String) value).startsWith("{") ? (String) value : ((String) value).replace("\"", "\\\"");
152177
}
153178

154179
if (value instanceof byte[]) {
155180

156181
String base64representation = DatatypeConverter.printBase64Binary((byte[]) value);
157182

158183
if (!binding.isQuoted()) {
159-
return "{ '$binary' : '" + base64representation + "', '$type' : " + BSON.B_GENERAL + "}";
184+
return "{ '$binary' : '" + base64representation + "', '$type' : '" + BSON.B_GENERAL + "'}";
160185
}
161186

162187
return base64representation;
@@ -167,7 +192,7 @@ private String getParameterValueForBinding(MongoParameterAccessor accessor, Mong
167192

168193
/**
169194
* Evaluates the given {@code expressionString}.
170-
*
195+
*
171196
* @param expressionString must not be {@literal null} or empty.
172197
* @param parameters must not be {@literal null}.
173198
* @param parameterValues must not be {@literal null}.
@@ -181,25 +206,59 @@ private Object evaluateExpression(String expressionString, MongoParameters param
181206
return expression.getValue(evaluationContext, Object.class);
182207
}
183208

209+
/**
210+
* Creates a replacement {@link Pattern} for all {@link ParameterBinding#getParameter() binding parameters} including
211+
* a potentially trailing quotation mark.
212+
*
213+
* @param bindings
214+
* @return
215+
*/
216+
private Pattern createReplacementPattern(List<ParameterBinding> bindings) {
217+
218+
StringBuilder regex = new StringBuilder();
219+
for (ParameterBinding binding : bindings) {
220+
regex.append("|");
221+
regex.append(Pattern.quote(binding.getParameter()));
222+
regex.append("['\"]?"); // potential quotation char (as in { foo : '?0' }).
223+
}
224+
225+
return Pattern.compile(regex.substring(1));
226+
}
227+
228+
/**
229+
* Extract the placeholder stripping any trailing trailing quotation mark that might have resulted from the
230+
* {@link #createReplacementPattern(List) pattern} used.
231+
*
232+
* @param groupName The actual {@link Matcher#group() group}.
233+
* @return
234+
*/
235+
private String extractPlaceholder(String groupName) {
236+
237+
if (!groupName.endsWith("'") && !groupName.endsWith("\"")) {
238+
return groupName;
239+
}
240+
return groupName.substring(0, groupName.length() - 1);
241+
}
242+
184243
/**
185244
* @author Christoph Strobl
186245
* @since 1.9
187246
*/
188247
static class BindingContext {
189248

190249
final MongoParameters parameters;
191-
final List<ParameterBinding> bindings;
250+
final Map<String, ParameterBinding> bindings;
192251

193252
/**
194253
* Creates new {@link BindingContext}.
195-
*
254+
*
196255
* @param parameters
197256
* @param bindings
198257
*/
199258
public BindingContext(MongoParameters parameters, List<ParameterBinding> bindings) {
200259

201260
this.parameters = parameters;
202-
this.bindings = bindings;
261+
this.bindings = mapBindings(bindings);
203262
}
204263

205264
/**
@@ -211,11 +270,28 @@ boolean hasBindings() {
211270

212271
/**
213272
* Get unmodifiable list of {@link ParameterBinding}s.
214-
*
273+
*
215274
* @return never {@literal null}.
216275
*/
217276
public List<ParameterBinding> getBindings() {
218-
return Collections.unmodifiableList(bindings);
277+
return new ArrayList<ParameterBinding>(bindings.values());
278+
}
279+
280+
/**
281+
* Get the concrete {@link ParameterBinding} for a given {@literal placeholder}.
282+
*
283+
* @param placeholder must not be {@literal null}.
284+
* @return
285+
* @throws java.util.NoSuchElementException
286+
* @since 1.10
287+
*/
288+
ParameterBinding getBindingFor(String placeholder) {
289+
290+
if (!bindings.containsKey(placeholder)) {
291+
throw new NoSuchElementException(String.format("Could not to find binding for placeholder '%s'.", placeholder));
292+
}
293+
294+
return bindings.get(placeholder);
219295
}
220296

221297
/**
@@ -227,5 +303,13 @@ public MongoParameters getParameters() {
227303
return parameters;
228304
}
229305

306+
private static Map<String, ParameterBinding> mapBindings(List<ParameterBinding> bindings) {
307+
308+
Map<String, ParameterBinding> map = new LinkedHashMap<String, ParameterBinding>(bindings.size(), 1);
309+
for (ParameterBinding binding : bindings) {
310+
map.put(binding.getParameter(), binding);
311+
}
312+
return map;
313+
}
230314
}
231315
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ public interface PersonRepository extends MongoRepository<Person, String>, Query
119119
*/
120120
Page<Person> findByLastnameLike(String lastname, Pageable pageable);
121121

122-
@Query("{ 'lastname' : { '$regex' : ?0, '$options' : ''}}")
122+
@Query("{ 'lastname' : { '$regex' : '?0', '$options' : 'i'}}")
123123
Page<Person> findByLastnameLikeWithPageable(String lastname, Pageable pageable);
124124

125125
/**
@@ -335,7 +335,7 @@ public interface PersonRepository extends MongoRepository<Person, String>, Query
335335
/**
336336
* @see DATAMONGO-745
337337
*/
338-
@Query("{lastname:?0, address.street:{$in:?1}}")
338+
@Query("{lastname:?0, 'address.street':{$in:?1}}")
339339
Page<Person> findByCustomQueryLastnameAndAddressStreetInList(String lastname, List<String> streetNames,
340340
Pageable page);
341341

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ public void shouldSupportNonQuotedBinaryDataReplacement() throws Exception {
240240

241241
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accesor);
242242
org.springframework.data.mongodb.core.query.Query reference = new BasicQuery("{'lastname' : { '$binary' : '"
243-
+ DatatypeConverter.printBase64Binary(binaryData) + "', '$type' : " + BSON.B_GENERAL + "}}");
243+
+ DatatypeConverter.printBase64Binary(binaryData) + "', '$type' : '" + BSON.B_GENERAL + "'}}");
244244

245245
assertThat(query.getQueryObject().toJson(), is(reference.getQueryObject().toJson()));
246246
}

0 commit comments

Comments
 (0)