DATAMONGO-1290 - Move parameter binding for String based queries.

Moved parameter binding for string based queries into separate class.

Original pull request: #332.
This commit is contained in:
Christoph Strobl
2015-11-18 13:20:13 +01:00
committed by Oliver Gierke
parent cbfc46270e
commit 143b0b73b9
2 changed files with 241 additions and 122 deletions

View File

@@ -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;
}
}
}

View File

@@ -21,17 +21,13 @@ import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.bind.DatatypeConverter;
import org.bson.BSON;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;
@@ -59,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}.
@@ -92,9 +87,6 @@ public class StringBasedMongoQuery extends AbstractMongoQuery {
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);
@@ -109,6 +101,8 @@ public class StringBasedMongoQuery extends AbstractMongoQuery {
if (isCountQuery && isDeleteQuery) {
throw new IllegalArgumentException(String.format(COUND_AND_DELETE, method));
}
this.parameterBinder = new ExpressionEvaluatingParameterBinder(expressionParser, evaluationContextProvider);
}
/*
@@ -118,21 +112,15 @@ public class StringBasedMongoQuery extends AbstractMongoQuery {
@Override
protected Query createQuery(ConvertingParameterAccessor accessor) {
String queryString = replacePlaceholders(query, accessor, queryParameterBindings);
String queryString = parameterBinder.bind(this.query, accessor, new BindingContext(getQueryMethod()
.getParameters(), queryParameterBindings));
String fieldsString = parameterBinder.bind(this.fieldSpec, accessor, new BindingContext(getQueryMethod()
.getParameters(), fieldSpecParameterBindings));
Query query = null;
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;
@@ -156,105 +144,6 @@ public class StringBasedMongoQuery extends AbstractMongoQuery {
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;
}
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 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.
*
@@ -429,7 +318,7 @@ public class StringBasedMongoQuery extends AbstractMongoQuery {
*
* @author Thomas Darimont
*/
private static class ParameterBinding {
static class ParameterBinding {
private final int parameterIndex;
private final boolean quoted;