Browse Source

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.
pull/669/head
Christoph Strobl 9 years ago
parent
commit
b1de52f05a
  1. 5
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java
  2. 5
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/BasicQuery.java
  3. 142
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ExpressionEvaluatingParameterBinder.java
  4. 4
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java
  5. 2
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQueryUnitTests.java
  6. 105
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQueryUnitTests.java

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

@ -23,6 +23,7 @@ import java.util.List;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Set; import java.util.Set;
import org.bson.BsonValue;
import org.bson.Document; import org.bson.Document;
import org.bson.conversions.Bson; import org.bson.conversions.Bson;
import org.bson.types.ObjectId; import org.bson.types.ObjectId;
@ -408,6 +409,10 @@ public class QueryMapper {
return getMappedObject((BasicDBObject) source, entity); return getMappedObject((BasicDBObject) source, entity);
} }
if(source instanceof BsonValue) {
return source;
}
return delegateConvertToMongoType(source, entity); return delegateConvertToMongoType(source, entity);
} }

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

@ -63,8 +63,9 @@ public class BasicQuery extends Query {
* @param fields may be {@literal null}. * @param fields may be {@literal null}.
*/ */
public BasicQuery(String query, String fields) { public BasicQuery(String query, String fields) {
this.queryObject = query != null ? new Document(((DBObject) JSON.parse(query)).toMap()) : null;
this.fieldsObject = fields != null ? new Document(((DBObject) JSON.parse(fields)).toMap()) : null; this.queryObject = query != null ? Document.parse(query) : null;
this.fieldsObject = fields != null ? Document.parse(fields) : null;
} }
/** /**

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

@ -15,8 +15,13 @@
*/ */
package org.springframework.data.mongodb.repository.query; package org.springframework.data.mongodb.repository.query;
import java.util.Collections; import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.bind.DatatypeConverter; import javax.xml.bind.DatatypeConverter;
@ -94,42 +99,62 @@ class ExpressionEvaluatingParameterBinder {
return input; return input;
} }
boolean isCompletlyParameterizedQuery = input.matches("^\\?\\d+$"); if (input.matches("^\\?\\d+$")) {
StringBuilder result = new StringBuilder(input); return getParameterValueForBinding(accessor, bindingContext.getParameters(),
bindingContext.getBindings().iterator().next());
for (ParameterBinding binding : bindingContext.getBindings()) { }
String parameter = binding.getParameter(); Matcher matcher = createReplacementPattern(bindingContext.getBindings()).matcher(input);
int idx = result.indexOf(parameter); StringBuffer buffer = new StringBuffer();
if (idx == -1) { while (matcher.find()) {
continue;
}
ParameterBinding binding = bindingContext.getBindingFor(extractPlaceholder(matcher.group()));
String valueForBinding = getParameterValueForBinding(accessor, bindingContext.getParameters(), binding); String valueForBinding = getParameterValueForBinding(accessor, bindingContext.getParameters(), binding);
int start = idx; // appendReplacement does not like unescaped $ sign and others, so we need to quote that stuff first
int end = idx + parameter.length(); matcher.appendReplacement(buffer, Matcher.quoteReplacement(valueForBinding));
if (binding.isQuoted()) {
postProcessQuotedBinding(buffer, valueForBinding);
}
}
// If the value to bind is an object literal we need to remove the quoting around the expression insertion point. matcher.appendTail(buffer);
if (valueForBinding.startsWith("{") && !isCompletlyParameterizedQuery) { return buffer.toString();
}
/**
* Sanitize String binding by replacing single quoted values with double quotes which prevents potential single quotes
* contained in replacement to interfere with the Json parsing. Also take care of complex objects by removing the
* quotation entirely.
*
* @param buffer the {@link StringBuffer} to operate upon.
* @param valueForBinding the actual binding value.
*/
private void postProcessQuotedBinding(StringBuffer buffer, String valueForBinding) {
// Is the insertion point actually surrounded by quotes? int quotationMarkIndex = buffer.length() - valueForBinding.length() - 1;
char beforeStart = result.charAt(start - 1); char quotationMark = buffer.charAt(quotationMarkIndex);
char afterEnd = result.charAt(end);
if ((beforeStart == '\'' || beforeStart == '"') && (afterEnd == '\'' || afterEnd == '"')) { while (quotationMark != '\'' && quotationMark != '"') {
// Skip preceding and following quote quotationMarkIndex--;
start -= 1; if (quotationMarkIndex < 0) {
end += 1; throw new IllegalArgumentException("Could not find opening quotes for quoted parameter");
} }
quotationMark = buffer.charAt(quotationMarkIndex);
} }
result.replace(start, end, valueForBinding); if (valueForBinding.startsWith("{")) { // remove quotation char before the complex object string
} buffer.deleteCharAt(quotationMarkIndex);
} else {
return result.toString(); if (quotationMark == '\'') {
buffer.replace(quotationMarkIndex, quotationMarkIndex + 1, "\"");
}
buffer.append("\"");
}
} }
/** /**
@ -148,7 +173,7 @@ class ExpressionEvaluatingParameterBinder {
: accessor.getBindableValue(binding.getParameterIndex()); : accessor.getBindableValue(binding.getParameterIndex());
if (value instanceof String && binding.isQuoted()) { if (value instanceof String && binding.isQuoted()) {
return (String) value; return ((String) value).startsWith("{") ? (String) value : ((String) value).replace("\"", "\\\"");
} }
if (value instanceof byte[]) { if (value instanceof byte[]) {
@ -156,7 +181,7 @@ class ExpressionEvaluatingParameterBinder {
String base64representation = DatatypeConverter.printBase64Binary((byte[]) value); String base64representation = DatatypeConverter.printBase64Binary((byte[]) value);
if (!binding.isQuoted()) { if (!binding.isQuoted()) {
return "{ '$binary' : '" + base64representation + "', '$type' : " + BSON.B_GENERAL + "}"; return "{ '$binary' : '" + base64representation + "', '$type' : '" + BSON.B_GENERAL + "'}";
} }
return base64representation; return base64representation;
@ -181,6 +206,40 @@ class ExpressionEvaluatingParameterBinder {
return expression.getValue(evaluationContext, Object.class); return expression.getValue(evaluationContext, Object.class);
} }
/**
* Creates a replacement {@link Pattern} for all {@link ParameterBinding#getParameter() binding parameters} including
* a potentially trailing quotation mark.
*
* @param bindings
* @return
*/
private Pattern createReplacementPattern(List<ParameterBinding> bindings) {
StringBuilder regex = new StringBuilder();
for (ParameterBinding binding : bindings) {
regex.append("|");
regex.append(Pattern.quote(binding.getParameter()));
regex.append("['\"]?"); // potential quotation char (as in { foo : '?0' }).
}
return Pattern.compile(regex.substring(1));
}
/**
* 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}.
* @return
*/
private String extractPlaceholder(String groupName) {
if (!groupName.endsWith("'") && !groupName.endsWith("\"")) {
return groupName;
}
return groupName.substring(0, groupName.length() - 1);
}
/** /**
* @author Christoph Strobl * @author Christoph Strobl
* @since 1.9 * @since 1.9
@ -188,7 +247,7 @@ class ExpressionEvaluatingParameterBinder {
static class BindingContext { static class BindingContext {
final MongoParameters parameters; final MongoParameters parameters;
final List<ParameterBinding> bindings; final Map<String, ParameterBinding> bindings;
/** /**
* Creates new {@link BindingContext}. * Creates new {@link BindingContext}.
@ -199,7 +258,7 @@ class ExpressionEvaluatingParameterBinder {
public BindingContext(MongoParameters parameters, List<ParameterBinding> bindings) { public BindingContext(MongoParameters parameters, List<ParameterBinding> bindings) {
this.parameters = parameters; this.parameters = parameters;
this.bindings = bindings; this.bindings = mapBindings(bindings);
} }
/** /**
@ -215,7 +274,24 @@ class ExpressionEvaluatingParameterBinder {
* @return never {@literal null}. * @return never {@literal null}.
*/ */
public List<ParameterBinding> getBindings() { public List<ParameterBinding> getBindings() {
return Collections.unmodifiableList(bindings); return new ArrayList<ParameterBinding>(bindings.values());
}
/**
* Get the concrete {@link ParameterBinding} for a given {@literal placeholder}.
*
* @param placeholder must not be {@literal null}.
* @return
* @throws java.util.NoSuchElementException
* @since 1.10
*/
ParameterBinding getBindingFor(String placeholder) {
if (!bindings.containsKey(placeholder)) {
throw new NoSuchElementException(String.format("Could not to find binding for placeholder '%s'.", placeholder));
}
return bindings.get(placeholder);
} }
/** /**
@ -227,5 +303,13 @@ class ExpressionEvaluatingParameterBinder {
return parameters; return parameters;
} }
private static Map<String, ParameterBinding> mapBindings(List<ParameterBinding> bindings) {
Map<String, ParameterBinding> map = new LinkedHashMap<String, ParameterBinding>(bindings.size(), 1);
for (ParameterBinding binding : bindings) {
map.put(binding.getParameter(), binding);
}
return map;
}
} }
} }

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

@ -119,7 +119,7 @@ public interface PersonRepository extends MongoRepository<Person, String>, Query
*/ */
Page<Person> findByLastnameLike(String lastname, Pageable pageable); Page<Person> findByLastnameLike(String lastname, Pageable pageable);
@Query("{ 'lastname' : { '$regex' : ?0, '$options' : ''}}") @Query("{ 'lastname' : { '$regex' : '?0', '$options' : 'i'}}")
Page<Person> findByLastnameLikeWithPageable(String lastname, Pageable pageable); Page<Person> findByLastnameLikeWithPageable(String lastname, Pageable pageable);
/** /**
@ -335,7 +335,7 @@ public interface PersonRepository extends MongoRepository<Person, String>, Query
/** /**
* @see DATAMONGO-745 * @see DATAMONGO-745
*/ */
@Query("{lastname:?0, address.street:{$in:?1}}") @Query("{lastname:?0, 'address.street':{$in:?1}}")
Page<Person> findByCustomQueryLastnameAndAddressStreetInList(String lastname, List<String> streetNames, Page<Person> findByCustomQueryLastnameAndAddressStreetInList(String lastname, List<String> streetNames,
Pageable page); Pageable page);

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

@ -240,7 +240,7 @@ public class ReactiveStringBasedMongoQueryUnitTests {
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accesor); org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accesor);
org.springframework.data.mongodb.core.query.Query reference = new BasicQuery("{'lastname' : { '$binary' : '" org.springframework.data.mongodb.core.query.Query reference = new BasicQuery("{'lastname' : { '$binary' : '"
+ DatatypeConverter.printBase64Binary(binaryData) + "', '$type' : " + BSON.B_GENERAL + "}}"); + DatatypeConverter.printBase64Binary(binaryData) + "', '$type' : '" + BSON.B_GENERAL + "'}}");
assertThat(query.getQueryObject().toJson(), is(reference.getQueryObject().toJson())); assertThat(query.getQueryObject().toJson(), is(reference.getQueryObject().toJson()));
} }

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

@ -28,6 +28,7 @@ import javax.xml.bind.DatatypeConverter;
import org.bson.BSON; import org.bson.BSON;
import org.bson.Document; import org.bson.Document;
import org.bson.conversions.Bson;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@ -44,6 +45,7 @@ import org.springframework.data.mongodb.core.query.BasicQuery;
import org.springframework.data.mongodb.repository.Address; import org.springframework.data.mongodb.repository.Address;
import org.springframework.data.mongodb.repository.Person; import org.springframework.data.mongodb.repository.Person;
import org.springframework.data.mongodb.repository.Query; import org.springframework.data.mongodb.repository.Query;
import org.springframework.data.mongodb.test.util.IsBsonObject;
import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.Repository; import org.springframework.data.repository.Repository;
@ -52,6 +54,7 @@ import org.springframework.data.repository.query.DefaultEvaluationContextProvide
import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser;
import com.mongodb.DBRef; import com.mongodb.DBRef;
import org.bson.Document;
/** /**
* Unit tests for {@link StringBasedMongoQuery}. * Unit tests for {@link StringBasedMongoQuery}.
@ -184,7 +187,6 @@ public class StringBasedMongoQueryUnitTests {
StringBasedMongoQuery mongoQuery = createQueryForMethod("findByParameterizedCriteriaAndFields", Document.class, StringBasedMongoQuery mongoQuery = createQueryForMethod("findByParameterizedCriteriaAndFields", Document.class,
Map.class); Map.class);
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accessor); org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accessor);
assertThat(query.getQueryObject(), assertThat(query.getQueryObject(),
@ -280,9 +282,8 @@ public class StringBasedMongoQueryUnitTests {
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(parameterAccessor); org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(parameterAccessor);
DBRef dbRef = DocumentTestUtils.getTypedValue(query.getQueryObject(), "reference", DBRef.class); Document dbRef = DocumentTestUtils.getTypedValue(query.getQueryObject(), "reference", Document.class);
assertThat(dbRef.getId(), is((Object) "myid")); assertThat(dbRef, is(new Document("$ref", "reference").append("$id", "myid")));
assertThat(dbRef.getCollectionName(), is("reference"));
} }
/** /**
@ -360,7 +361,7 @@ public class StringBasedMongoQueryUnitTests {
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accesor); org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accesor);
org.springframework.data.mongodb.core.query.Query reference = new BasicQuery("{'lastname' : { '$binary' : '" org.springframework.data.mongodb.core.query.Query reference = new BasicQuery("{'lastname' : { '$binary' : '"
+ DatatypeConverter.printBase64Binary(binaryData) + "', '$type' : " + BSON.B_GENERAL + "}}"); + DatatypeConverter.printBase64Binary(binaryData) + "', '$type' : '" + BSON.B_GENERAL + "'}}");
assertThat(query.getQueryObject().toJson(), is(reference.getQueryObject().toJson())); assertThat(query.getQueryObject().toJson(), is(reference.getQueryObject().toJson()));
} }
@ -376,6 +377,97 @@ public class StringBasedMongoQueryUnitTests {
assertThat(mongoQuery.isExistsQuery(), is(true)); assertThat(mongoQuery.isExistsQuery(), is(true));
} }
/**
* @see DATAMONGO-1565
*/
@Test
public void shouldIgnorePlaceholderPatternInReplacementValue() throws Exception {
ConvertingParameterAccessor accesor = StubParameterAccessor.getAccessor(converter, "argWith?1andText",
"nothing-special");
StringBasedMongoQuery mongoQuery = createQueryForMethod("findByStringWithWildcardChar", String.class, String.class);
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accesor);
assertThat(query.getQueryObject(),
is(Document.parse("{ \"arg0\" : \"argWith?1andText\" , \"arg1\" : \"nothing-special\"}")));
}
/**
* @see DATAMONGO-1565
*/
@Test
public void shouldQuoteStringReplacementCorrectly() throws Exception {
StringBasedMongoQuery mongoQuery = createQueryForMethod("findByLastnameQuoted", String.class);
ConvertingParameterAccessor accesor = StubParameterAccessor.getAccessor(converter, "Matthews', password: 'foo");
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accesor);
assertThat(query.getQueryObject(),
is(not(new Document().append("lastname", "Matthews").append("password", "foo"))));
assertThat(query.getQueryObject(), is(new Document("lastname", "Matthews', password: 'foo")));
}
/**
* @see DATAMONGO-1565
*/
@Test
public void shouldQuoteStringReplacementContainingQuotesCorrectly() throws Exception {
StringBasedMongoQuery mongoQuery = createQueryForMethod("findByLastnameQuoted", String.class);
ConvertingParameterAccessor accesor = StubParameterAccessor.getAccessor(converter, "Matthews\", password: \"foo");
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accesor);
assertThat(query.getQueryObject(),
is(not(new Document().append("lastname", "Matthews").append("password", "foo"))));
assertThat(query.getQueryObject(), is( new Document("lastname", "Matthews\", password: \"foo")));
}
/**
* @see DATAMONGO-1565
*/
@Test
public void shouldQuoteStringReplacementWithQuotationsCorrectly() throws Exception {
StringBasedMongoQuery mongoQuery = createQueryForMethod("findByLastnameQuoted", String.class);
ConvertingParameterAccessor accesor = StubParameterAccessor.getAccessor(converter,
"\"Dave Matthews\", password: 'foo");
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accesor);
assertThat(query.getQueryObject(),
is(new Document("lastname", "\"Dave Matthews\", password: 'foo")));
}
/**
* @see DATAMONGO-1565
*/
@Test
public void shouldQuoteComplexQueryStringCorreclty() throws Exception {
StringBasedMongoQuery mongoQuery = createQueryForMethod("findByLastnameQuoted", String.class);
ConvertingParameterAccessor accesor = StubParameterAccessor.getAccessor(converter, "{ $ne : \"calamity\" }");
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accesor);
assertThat(query.getQueryObject(),
is( new Document("lastname", new Document("$ne", "calamity"))));
}
/**
* @see DATAMONGO-1565
*/
@Test
public void shouldQuotationInQuotedComplexQueryString() throws Exception {
StringBasedMongoQuery mongoQuery = createQueryForMethod("findByLastnameQuoted", String.class);
ConvertingParameterAccessor accesor = StubParameterAccessor.getAccessor(converter,
"{ $ne : \"\\\"calamity\\\"\" }");
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accesor);
assertThat(query.getQueryObject(),
is(new Document("lastname", new Document("$ne", "\"calamity\""))));
}
private StringBasedMongoQuery createQueryForMethod(String name, Class<?>... parameters) throws Exception { private StringBasedMongoQuery createQueryForMethod(String name, Class<?>... parameters) throws Exception {
Method method = SampleRepository.class.getMethod(name, parameters); Method method = SampleRepository.class.getMethod(name, parameters);
@ -437,5 +529,8 @@ public class StringBasedMongoQueryUnitTests {
@Query(value = "{ 'lastname' : ?0 }", exists = true) @Query(value = "{ 'lastname' : ?0 }", exists = true)
boolean existsByLastname(String lastname); boolean existsByLastname(String lastname);
@Query("{ 'arg0' : ?0, 'arg1' : ?1 }")
List<Person> findByStringWithWildcardChar(String arg0, String arg1);
} }
} }

Loading…
Cancel
Save