Skip to content

Commit f669711

Browse files
Thomas Darimontodrotbohm
authored andcommitted
DATAMONGO-995 - Improve support of quote handling for custom query parameters.
Introduced ParameterBindingParser which exposes parameter references in query strings as ParameterBindings. This allows us to detect whether a parameter reference in a query string is already quoted avoiding wrongly double-quoting the parameter value. Original pull request: #185. Related ticket: DATAMONGO-420.
1 parent 5f3671f commit f669711

File tree

2 files changed

+122
-22
lines changed

2 files changed

+122
-22
lines changed

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

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

18-
import static java.util.regex.Pattern.*;
19-
2018
import java.util.ArrayList;
2119
import java.util.Collections;
2220
import java.util.List;
@@ -30,6 +28,7 @@
3028
import org.springframework.data.mongodb.core.query.Query;
3129
import org.springframework.util.StringUtils;
3230

31+
import com.mongodb.DBObject;
3332
import com.mongodb.util.JSON;
3433

3534
/**
@@ -188,20 +187,13 @@ private static enum ParameterBindingParser {
188187

189188
INSTANCE;
190189

191-
private static final Pattern PARAMETER_BINDING_PATTERN;
190+
private static final String PARAMETER_PREFIX = "_param_";
191+
private static final String PARSEABLE_PARAMETER = "\"" + PARAMETER_PREFIX + "$1\"";
192+
private static final Pattern PARAMETER_BINDING_PATTERN = Pattern.compile("\\?(\\d+)");
193+
private static final Pattern PARSEABLE_BINDING_PATTERN = Pattern.compile("\"?" + PARAMETER_PREFIX + "(\\d+)\"?");
192194

193195
private final static int PARAMETER_INDEX_GROUP = 1;
194196

195-
static {
196-
197-
StringBuilder builder = new StringBuilder();
198-
builder.append("\\?(\\d+)"); // position parameter and parameter index
199-
builder.append("[^,'\"]*"); // followed by non quotes, non field separators
200-
builder.append("[,\"'}]?");
201-
202-
PARAMETER_BINDING_PATTERN = Pattern.compile(builder.toString(), CASE_INSENSITIVE);
203-
}
204-
205197
/**
206198
* Returns a list of {@link ParameterBinding}s found in the given {@code input} or an
207199
* {@link Collections#emptyList()}.
@@ -217,19 +209,60 @@ public List<ParameterBinding> parseParameterBindingsFrom(String input) {
217209

218210
List<ParameterBinding> bindings = new ArrayList<ParameterBinding>();
219211

212+
String parseableInput = makeParameterReferencesParseable(input);
213+
214+
collectParameterReferencesIntoBindings(bindings, JSON.parse(parseableInput));
215+
216+
return bindings;
217+
}
218+
219+
private String makeParameterReferencesParseable(String input) {
220+
220221
Matcher matcher = PARAMETER_BINDING_PATTERN.matcher(input);
222+
String parseableInput = matcher.replaceAll(PARSEABLE_PARAMETER);
221223

222-
while (matcher.find()) {
224+
return parseableInput;
225+
}
223226

224-
String group = matcher.group();
227+
private void collectParameterReferencesIntoBindings(List<ParameterBinding> bindings, Object value) {
225228

226-
boolean parameterIsQuoted = group.endsWith("'") || group.endsWith("\"");
227-
int parameterIndex = Integer.parseInt(matcher.group(PARAMETER_INDEX_GROUP));
229+
if (value instanceof String) {
228230

229-
bindings.add(new ParameterBinding(parameterIndex, parameterIsQuoted));
230-
}
231+
String string = ((String) value).trim();
231232

232-
return bindings;
233+
Matcher valueMatcher = PARSEABLE_BINDING_PATTERN.matcher(string);
234+
while (valueMatcher.find()) {
235+
int paramIndex = Integer.parseInt(valueMatcher.group(PARAMETER_INDEX_GROUP));
236+
boolean quoted = (string.startsWith("'") && string.endsWith("'"))
237+
|| (string.startsWith("\"") && string.endsWith("\""));
238+
bindings.add(new ParameterBinding(paramIndex, quoted));
239+
}
240+
241+
} else if (value instanceof Pattern) {
242+
243+
String string = ((Pattern) value).toString().trim();
244+
245+
Matcher valueMatcher = PARSEABLE_BINDING_PATTERN.matcher(string);
246+
while (valueMatcher.find()) {
247+
int paramIndex = Integer.parseInt(valueMatcher.group(PARAMETER_INDEX_GROUP));
248+
249+
/*
250+
* The pattern is used as a direct parameter replacement, e.g. 'field': ?1,
251+
* therefore we treat it as not quoted to remain backwards compatible.
252+
*/
253+
boolean quoted = !string.equals(PARAMETER_PREFIX + paramIndex);
254+
255+
bindings.add(new ParameterBinding(paramIndex, quoted));
256+
}
257+
258+
} else if (value instanceof DBObject) {
259+
260+
DBObject dbo = (DBObject) value;
261+
262+
for (String field : dbo.keySet()) {
263+
collectParameterReferencesIntoBindings(bindings, dbo.get(field));
264+
}
265+
}
233266
}
234267
}
235268

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

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,9 +170,9 @@ public void shouldSupportFindByParameterizedCriteriaAndFields() throws Exception
170170

171171
ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter, new Object[] {
172172
new BasicDBObject("firstname", "first").append("lastname", "last"), Collections.singletonMap("lastname", 1) });
173-
174173
StringBasedMongoQuery mongoQuery = createQueryForMethod("findByParameterizedCriteriaAndFields", DBObject.class,
175174
Map.class);
175+
176176
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accessor);
177177

178178
assertThat(query.getQueryObject(),
@@ -187,13 +187,74 @@ public void shouldSupportFindByParameterizedCriteriaAndFields() throws Exception
187187
public void shouldSupportRespectExistingQuotingInFindByTitleBeginsWithExplicitQuoting() throws Exception {
188188

189189
ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter, new Object[] { "fun" });
190-
191190
StringBasedMongoQuery mongoQuery = createQueryForMethod("findByTitleBeginsWithExplicitQuoting", String.class);
191+
192192
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accessor);
193193

194194
assertThat(query.getQueryObject(), is(new BasicQuery("{title: {$regex: '^fun', $options: 'i'}}").getQueryObject()));
195195
}
196196

197+
/**
198+
* @see DATAMONGO-995, DATAMONGO-420
199+
*/
200+
@Test
201+
public void shouldParseQueryWithParametersInExpression() throws Exception {
202+
203+
ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter, new Object[] { 1, 2, 3, 4 });
204+
StringBasedMongoQuery mongoQuery = createQueryForMethod("findByQueryWithParametersInExpression", int.class,
205+
int.class, int.class, int.class);
206+
207+
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accessor);
208+
209+
assertThat(query.getQueryObject(), is(new BasicQuery(
210+
"{$where: 'return this.date.getUTCMonth() == 3 && this.date.getUTCDay() == 4;'}").getQueryObject()));
211+
}
212+
213+
/**
214+
* @see DATAMONGO-995, DATAMONGO-420
215+
*/
216+
@Test
217+
public void bindsSimplePropertyAlreadyQuotedCorrectly() throws Exception {
218+
219+
ConvertingParameterAccessor accesor = StubParameterAccessor.getAccessor(converter, "Matthews");
220+
StringBasedMongoQuery mongoQuery = createQueryForMethod("findByLastnameQuoted", String.class);
221+
222+
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accesor);
223+
org.springframework.data.mongodb.core.query.Query reference = new BasicQuery("{'lastname' : 'Matthews'}");
224+
225+
assertThat(query.getQueryObject(), is(reference.getQueryObject()));
226+
}
227+
228+
/**
229+
* @see DATAMONGO-995, DATAMONGO-420
230+
*/
231+
@Test
232+
public void bindsSimplePropertyAlreadyQuotedWithRegexCorrectly() throws Exception {
233+
234+
ConvertingParameterAccessor accesor = StubParameterAccessor.getAccessor(converter, "^Mat.*");
235+
StringBasedMongoQuery mongoQuery = createQueryForMethod("findByLastnameQuoted", String.class);
236+
237+
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accesor);
238+
org.springframework.data.mongodb.core.query.Query reference = new BasicQuery("{'lastname' : '^Mat.*'}");
239+
240+
assertThat(query.getQueryObject(), is(reference.getQueryObject()));
241+
}
242+
243+
/**
244+
* @see DATAMONGO-995, DATAMONGO-420
245+
*/
246+
@Test
247+
public void bindsSimplePropertyWithRegexCorrectly() throws Exception {
248+
249+
StringBasedMongoQuery mongoQuery = createQueryForMethod("findByLastname", String.class);
250+
ConvertingParameterAccessor accesor = StubParameterAccessor.getAccessor(converter, "^Mat.*");
251+
252+
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accesor);
253+
org.springframework.data.mongodb.core.query.Query reference = new BasicQuery("{'lastname' : '^Mat.*'}");
254+
255+
assertThat(query.getQueryObject(), is(reference.getQueryObject()));
256+
}
257+
197258
private StringBasedMongoQuery createQueryForMethod(String name, Class<?>... parameters) throws Exception {
198259

199260
Method method = SampleRepository.class.getMethod(name, parameters);
@@ -206,6 +267,9 @@ private interface SampleRepository {
206267
@Query("{ 'lastname' : ?0 }")
207268
Person findByLastname(String lastname);
208269

270+
@Query("{ 'lastname' : '?0' }")
271+
Person findByLastnameQuoted(String lastname);
272+
209273
@Query("{ 'address' : ?0 }")
210274
Person findByAddress(Address address);
211275

@@ -226,5 +290,8 @@ private interface SampleRepository {
226290

227291
@Query("{'title': { $regex : '^?0', $options : 'i'}}")
228292
List<DBObject> findByTitleBeginsWithExplicitQuoting(String title);
293+
294+
@Query(value = "{$where: 'return this.date.getUTCMonth() == ?2 && this.date.getUTCDay() == ?3;'}")
295+
List<DBObject> findByQueryWithParametersInExpression(int param1, int param2, int param3, int param4);
229296
}
230297
}

0 commit comments

Comments
 (0)