Skip to content

DATAMONGO-1290 - Convert byte[] parameter in @Query to $binary representation. #332

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>1.9.0.BUILD-SNAPSHOT</version>
<version>1.9.0.DATAMONGO-1290-SNAPSHOT</version>
<packaging>pom</packaging>

<name>Spring Data MongoDB</name>
4 changes: 2 additions & 2 deletions spring-data-mongodb-cross-store/pom.xml
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>1.9.0.BUILD-SNAPSHOT</version>
<version>1.9.0.DATAMONGO-1290-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

@@ -48,7 +48,7 @@
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb</artifactId>
<version>1.9.0.BUILD-SNAPSHOT</version>
<version>1.9.0.DATAMONGO-1290-SNAPSHOT</version>
</dependency>

<dependency>
2 changes: 1 addition & 1 deletion spring-data-mongodb-distribution/pom.xml
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>1.9.0.BUILD-SNAPSHOT</version>
<version>1.9.0.DATAMONGO-1290-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

2 changes: 1 addition & 1 deletion spring-data-mongodb-log4j/pom.xml
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>1.9.0.BUILD-SNAPSHOT</version>
<version>1.9.0.DATAMONGO-1290-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

2 changes: 1 addition & 1 deletion spring-data-mongodb/pom.xml
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>1.9.0.BUILD-SNAPSHOT</version>
<version>1.9.0.DATAMONGO-1290-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.repository.query;

import java.util.Collections;
import java.util.List;

import javax.xml.bind.DatatypeConverter;

import org.bson.BSON;
import org.springframework.data.mongodb.repository.query.StringBasedMongoQuery.ParameterBinding;
import org.springframework.data.repository.query.EvaluationContextProvider;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import com.mongodb.util.JSON;

/**
* {@link ExpressionEvaluatingParameterBinder} allows to evaluate, convert and bind parameters to placholders within a
* {@link String}.
*
* @author Christoph Strobl
* @author Thomas Darimont
* @since 1.9
*/
class ExpressionEvaluatingParameterBinder {

private final SpelExpressionParser expressionParser;
private final EvaluationContextProvider evaluationContextProvider;

/**
* Creates new {@link ExpressionEvaluatingParameterBinder}
*
* @param expressionParser must not be {@literal null}.
* @param evaluationContextProvider must not be {@literal null}.
*/
public ExpressionEvaluatingParameterBinder(SpelExpressionParser expressionParser,
EvaluationContextProvider evaluationContextProvider) {

Assert.notNull(expressionParser, "ExpressionParser must not be null!");
Assert.notNull(evaluationContextProvider, "EvaluationContextProvider must not be null!");

this.expressionParser = expressionParser;
this.evaluationContextProvider = evaluationContextProvider;
}

/**
* Bind values provided by {@link MongoParameterAccessor} to placeholders in {@literal raw} while consisdering
* potential conversions and parameter types.
*
* @param raw
* @param accessor
* @param bindingContext
* @return {@literal null} if given {@literal raw} value is empty.
*/
public String bind(String raw, MongoParameterAccessor accessor, BindingContext bindingContext) {

if (!StringUtils.hasText(raw)) {
return null;
}

return replacePlaceholders(raw, accessor, bindingContext);
}

/**
* Replaced the parameter place-holders with the actual parameter values from the given {@link ParameterBinding}s.
*
* @param input
* @param accessor
* @param parameters
* @param bindings
* @return
*/
private String replacePlaceholders(String input, MongoParameterAccessor accessor, BindingContext bindingContext) {

if (!bindingContext.hasBindings()) {
return input;
}

boolean isCompletlyParameterizedQuery = input.matches("^\\?\\d+$");

StringBuilder result = new StringBuilder(input);

for (ParameterBinding binding : bindingContext.getBindings()) {

String parameter = binding.getParameter();
int idx = result.indexOf(parameter);

if (idx != -1) {
String valueForBinding = getParameterValueForBinding(accessor, bindingContext.getParameters(), binding);

// if the value to bind is an object literal we need to remove the quoting around
// the expression insertion point.
boolean shouldPotentiallyRemoveQuotes = valueForBinding.startsWith("{") && !isCompletlyParameterizedQuery;

int start = idx;
int end = idx + parameter.length();

if (shouldPotentiallyRemoveQuotes) {

// is the insertion point actually surrounded by quotes?
char beforeStart = result.charAt(start - 1);
char afterEnd = result.charAt(end);

if ((beforeStart == '\'' || beforeStart == '"') && (afterEnd == '\'' || afterEnd == '"')) {

// skip preceeding and following quote
start -= 1;
end += 1;
}
}

result.replace(start, end, valueForBinding);
}
}

return result.toString();
}

/**
* Returns the serialized value to be used for the given {@link ParameterBinding}.
*
* @param accessor
* @param parameters
* @param binding
* @return
*/
private String getParameterValueForBinding(MongoParameterAccessor accessor, MongoParameters parameters,
ParameterBinding binding) {

Object value = binding.isExpression() ? evaluateExpression(binding.getExpression(), parameters,
accessor.getValues()) : accessor.getBindableValue(binding.getParameterIndex());

if (value instanceof String && binding.isQuoted()) {
return (String) value;
}

if (value instanceof byte[]) {

String base64representation = DatatypeConverter.printBase64Binary((byte[]) value);
if (!binding.isQuoted()) {
return "{ '$binary' : '" + base64representation + "', '$type' : " + BSON.B_GENERAL + "}";
}
return base64representation;
}

return JSON.serialize(value);
}

/**
* Evaluates the given {@code expressionString}.
*
* @param expressionString
* @param parameters
* @param parameterValues
* @return
*/
private Object evaluateExpression(String expressionString, MongoParameters parameters, Object[] parameterValues) {

EvaluationContext evaluationContext = evaluationContextProvider.getEvaluationContext(parameters, parameterValues);
Expression expression = expressionParser.parseExpression(expressionString);

return expression.getValue(evaluationContext, Object.class);
}

/**
* @author Christoph Strobl
* @since 1.9
*/
static class BindingContext {

final MongoParameters parameters;
final List<ParameterBinding> bindings;

/**
* Creates new {@link BindingContext}.
*
* @param parameters
* @param bindings
*/
public BindingContext(MongoParameters parameters, List<ParameterBinding> bindings) {

this.parameters = parameters;
this.bindings = bindings;
}

/**
* @return {@literal true} when list of bindings is not empty.
*/
boolean hasBindings() {
return !CollectionUtils.isEmpty(bindings);
}

/**
* Get unmodifiable list of {@link ParameterBinding}s.
*
* @return never {@literal null}.
*/
public List<ParameterBinding> getBindings() {
return Collections.unmodifiableList(bindings);
}

/**
* Get the associated {@link MongoParameters}.
*
* @return
*/
public MongoParameters getParameters() {
return parameters;
}

}
}
Original file line number Diff line number Diff line change
@@ -26,9 +26,8 @@
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.query.BasicQuery;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.repository.query.ExpressionEvaluatingParameterBinder.BindingContext;
import org.springframework.data.repository.query.EvaluationContextProvider;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -56,8 +55,7 @@ public class StringBasedMongoQuery extends AbstractMongoQuery {
private final boolean isDeleteQuery;
private final List<ParameterBinding> queryParameterBindings;
private final List<ParameterBinding> fieldSpecParameterBindings;
private final SpelExpressionParser expressionParser;
private final EvaluationContextProvider evaluationContextProvider;
private final ExpressionEvaluatingParameterBinder parameterBinder;

/**
* Creates a new {@link StringBasedMongoQuery} for the given {@link MongoQueryMethod} and {@link MongoOperations}.
@@ -89,9 +87,6 @@ public StringBasedMongoQuery(String query, MongoQueryMethod method, MongoOperati
Assert.notNull(query, "Query must not be null!");
Assert.notNull(expressionParser, "SpelExpressionParser must not be null!");

this.expressionParser = expressionParser;
this.evaluationContextProvider = evaluationContextProvider;

this.queryParameterBindings = new ArrayList<ParameterBinding>();
this.query = BINDING_PARSER.parseAndCollectParameterBindingsFromQueryIntoBindings(query,
this.queryParameterBindings);
@@ -106,6 +101,8 @@ public StringBasedMongoQuery(String query, MongoQueryMethod method, MongoOperati
if (isCountQuery && isDeleteQuery) {
throw new IllegalArgumentException(String.format(COUND_AND_DELETE, method));
}

this.parameterBinder = new ExpressionEvaluatingParameterBinder(expressionParser, evaluationContextProvider);
}

/*
@@ -115,21 +112,15 @@ public StringBasedMongoQuery(String query, MongoQueryMethod method, MongoOperati
@Override
protected Query createQuery(ConvertingParameterAccessor accessor) {

String queryString = replacePlaceholders(query, accessor, queryParameterBindings);

Query query = null;
String queryString = parameterBinder.bind(this.query, accessor, new BindingContext(getQueryMethod()
.getParameters(), queryParameterBindings));
String fieldsString = parameterBinder.bind(this.fieldSpec, accessor, new BindingContext(getQueryMethod()
.getParameters(), fieldSpecParameterBindings));

if (fieldSpec != null) {
String fieldString = replacePlaceholders(fieldSpec, accessor, fieldSpecParameterBindings);
query = new BasicQuery(queryString, fieldString);
} else {
query = new BasicQuery(queryString);
}

query.with(accessor.getSort());
Query query = new BasicQuery(queryString, fieldsString).with(accessor.getSort());

if (LOG.isDebugEnabled()) {
LOG.debug(String.format("Created query %s", query.getQueryObject()));
LOG.debug(String.format("Created query %s for %s fields.", query.getQueryObject(), query.getFieldsObject()));
}

return query;
@@ -153,96 +144,6 @@ protected boolean isDeleteQuery() {
return this.isDeleteQuery;
}

/**
* Replaced the parameter place-holders with the actual parameter values from the given {@link ParameterBinding}s.
*
* @param input
* @param accessor
* @param bindings
* @return
*/
private String replacePlaceholders(String input, ConvertingParameterAccessor accessor,
List<ParameterBinding> bindings) {

if (bindings.isEmpty()) {
return input;
}

boolean isCompletlyParameterizedQuery = input.matches("^\\?\\d+$");

StringBuilder result = new StringBuilder(input);

for (ParameterBinding binding : bindings) {

String parameter = binding.getParameter();
int idx = result.indexOf(parameter);

if (idx != -1) {
String valueForBinding = getParameterValueForBinding(accessor, binding);

// if the value to bind is an object literal we need to remove the quoting around
// the expression insertion point.
boolean shouldPotentiallyRemoveQuotes = valueForBinding.startsWith("{") && !isCompletlyParameterizedQuery;

int start = idx;
int end = idx + parameter.length();

if (shouldPotentiallyRemoveQuotes) {

// is the insertion point actually surrounded by quotes?
char beforeStart = result.charAt(start - 1);
char afterEnd = result.charAt(end);

if ((beforeStart == '\'' || beforeStart == '"') && (afterEnd == '\'' || afterEnd == '"')) {

// skip preceeding and following quote
start -= 1;
end += 1;
}
}

result.replace(start, end, valueForBinding);
}
}

return result.toString();
}

/**
* Returns the serialized value to be used for the given {@link ParameterBinding}.
*
* @param accessor
* @param binding
* @return
*/
private String getParameterValueForBinding(ConvertingParameterAccessor accessor, ParameterBinding binding) {

Object value = binding.isExpression() ? evaluateExpression(binding.getExpression(), accessor.getValues())
: accessor.getBindableValue(binding.getParameterIndex());

if (value instanceof String && binding.isQuoted()) {
return (String) value;
}

return JSON.serialize(value);
}

/**
* Evaluates the given {@code expressionString}.
*
* @param expressionString
* @param parameterValues
* @return
*/
private Object evaluateExpression(String expressionString, Object[] parameterValues) {

EvaluationContext evaluationContext = evaluationContextProvider
.getEvaluationContext(getQueryMethod().getParameters(), parameterValues);
Expression expression = expressionParser.parseExpression(expressionString);

return expression.getValue(evaluationContext, Object.class);
}

/**
* A parser that extracts the parameter bindings from a given query string.
*
@@ -417,7 +318,7 @@ private static int getIndexOfExpressionParameter(String input, int position) {
*
* @author Thomas Darimont
*/
private static class ParameterBinding {
static class ParameterBinding {

private final int parameterIndex;
private final boolean quoted;
Original file line number Diff line number Diff line change
@@ -24,6 +24,9 @@
import java.util.List;
import java.util.Map;

import javax.xml.bind.DatatypeConverter;

import org.bson.BSON;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -343,6 +346,23 @@ public void shouldSupportExpressionsInCustomQueriesWithMultipleNestedObjects() t
assertThat(query.getQueryObject(), is(reference.getQueryObject()));
}

/**
* @see DATAMONGO-1290
*/
@Test
public void shouldSupportNonQuotedBinaryDataReplacement() throws Exception {

byte[] binaryData = "Matthews".getBytes("UTF-8");
ConvertingParameterAccessor accesor = StubParameterAccessor.getAccessor(converter, binaryData);
StringBasedMongoQuery mongoQuery = createQueryForMethod("findByLastnameAsBinary", byte[].class);

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

assertThat(query.getQueryObject(), is(reference.getQueryObject()));
}

private StringBasedMongoQuery createQueryForMethod(String name, Class<?>... parameters) throws Exception {

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

@Query("{ 'lastname' : ?0 }")
Person findByLastnameAsBinary(byte[] lastname);

@Query("{ 'lastname' : '?0' }")
Person findByLastnameQuoted(String lastname);

1 change: 1 addition & 0 deletions spring-data-mongodb/template.mf
Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@ Import-Template:
javax.tools.*;version="0",
javax.net.*;version="0",
javax.validation.*;version="${validation:[=.=.=.=,+1.0.0)}";resolution:=optional,
javax.xml.bind.*;version=0,
org.aopalliance.*;version="[1.0.0, 2.0.0)";resolution:=optional,
org.bson.*;version="0",
org.objenesis.*;version="${objenesis:[=.=.=, +1.0.0)}";resolution:=optional,