Skip to content

Commit

Permalink
Retain parameter type when binding parameters in annotated Query/Aggr…
Browse files Browse the repository at this point in the history
…egation.

This commit ensures the parameter type is preserved when binding parameters used within the value of the Query or Aggregation annotation

Closes: #4089
  • Loading branch information
christophstrobl committed Jun 20, 2022
1 parent 864c94f commit 7c5ac76
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 28 deletions.
@@ -0,0 +1,73 @@
/*
* Copyright 2022 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
*
* https://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.util.json;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;

import org.springframework.data.mapping.model.SpELExpressionEvaluator;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.lang.Nullable;

/**
* @author Christoph Strobl
* @since 3.3.5
*/
class EvaluationContextExpressionEvaluator implements SpELExpressionEvaluator {

ValueProvider valueProvider;
ExpressionParser expressionParser;
Supplier<EvaluationContext> evaluationContext;

public EvaluationContextExpressionEvaluator(ValueProvider valueProvider, ExpressionParser expressionParser,
Supplier<EvaluationContext> evaluationContext) {

this.valueProvider = valueProvider;
this.expressionParser = expressionParser;
this.evaluationContext = evaluationContext;
}

@Nullable
@Override
public <T> T evaluate(String expression) {
return evaluateExpression(expression, Collections.emptyMap());
}

public EvaluationContext getEvaluationContext(String expressionString) {
return evaluationContext != null ? evaluationContext.get() : new StandardEvaluationContext();
}

public SpelExpression getParsedExpression(String expressionString) {
return (SpelExpression) (expressionParser != null ? expressionParser : new SpelExpressionParser())
.parseExpression(expressionString);
}

public <T> T evaluateExpression(String expressionString, Map<String, Object> variables) {

SpelExpression expression = getParsedExpression(expressionString);
EvaluationContext ctx = getEvaluationContext(expressionString);
variables.entrySet().forEach(entry -> ctx.setVariable(entry.getKey(), entry.getValue()));

Object result = expression.getValue(ctx, Object.class);
return (T) result;
}
}
Expand Up @@ -15,6 +15,7 @@
*/
package org.springframework.data.mongodb.util.json;

import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier;

Expand Down Expand Up @@ -58,13 +59,7 @@ public ParameterBindingContext(ValueProvider valueProvider, SpelExpressionParser
*/
public ParameterBindingContext(ValueProvider valueProvider, ExpressionParser expressionParser,
Supplier<EvaluationContext> evaluationContext) {

this(valueProvider, new SpELExpressionEvaluator() {
@Override
public <T> T evaluate(String expressionString) {
return (T) expressionParser.parseExpression(expressionString).getValue(evaluationContext.get(), Object.class);
}
});
this(valueProvider, new EvaluationContextExpressionEvaluator(valueProvider, expressionParser, evaluationContext));
}

/**
Expand All @@ -87,20 +82,20 @@ public ParameterBindingContext(ValueProvider valueProvider, SpELExpressionEvalua
* @return
* @since 3.1
*/
public static ParameterBindingContext forExpressions(ValueProvider valueProvider,
ExpressionParser expressionParser, Function<ExpressionDependencies, EvaluationContext> contextFunction) {
public static ParameterBindingContext forExpressions(ValueProvider valueProvider, ExpressionParser expressionParser,
Function<ExpressionDependencies, EvaluationContext> contextFunction) {

return new ParameterBindingContext(valueProvider, new SpELExpressionEvaluator() {
@Override
public <T> T evaluate(String expressionString) {
return new ParameterBindingContext(valueProvider,
new EvaluationContextExpressionEvaluator(valueProvider, expressionParser, null) {

Expression expression = expressionParser.parseExpression(expressionString);
ExpressionDependencies dependencies = ExpressionDependencies.discover(expression);
EvaluationContext evaluationContext = contextFunction.apply(dependencies);
@Override
public EvaluationContext getEvaluationContext(String expressionString) {

return (T) expression.getValue(evaluationContext, Object.class);
}
});
Expression expression = getParsedExpression(expressionString);
ExpressionDependencies dependencies = ExpressionDependencies.discover(expression);
return contextFunction.apply(dependencies);
}
});
}

@Nullable
Expand All @@ -113,6 +108,16 @@ public Object evaluateExpression(String expressionString) {
return expressionEvaluator.evaluate(expressionString);
}

@Nullable
public Object evaluateExpression(String expressionString, Map<String, Object> variables) {

if (expressionEvaluator instanceof EvaluationContextExpressionEvaluator) {
return ((EvaluationContextExpressionEvaluator) expressionEvaluator).evaluateExpression(expressionString,
variables);
}
return expressionEvaluator.evaluate(expressionString);
}

public ValueProvider getValueProvider() {
return valueProvider;
}
Expand Down
Expand Up @@ -20,8 +20,12 @@
import java.text.DateFormat;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
Expand Down Expand Up @@ -64,6 +68,7 @@ public class ParameterBindingJsonReader extends AbstractBsonReader {
private static final Pattern PARAMETER_ONLY_BINDING_PATTERN = Pattern.compile("^\\?(\\d+)$");
private static final Pattern PARAMETER_BINDING_PATTERN = Pattern.compile("\\?(\\d+)");
private static final Pattern EXPRESSION_BINDING_PATTERN = Pattern.compile("[\\?:]#\\{.*\\}");
private static final Pattern SPEL_PARAMETER_BINDING_PATTERN = Pattern.compile("('\\?(\\d+)'|\\?(\\d+))");

private final ParameterBindingContext bindingContext;

Expand Down Expand Up @@ -379,14 +384,24 @@ private BindableValue bindableValueFor(JsonToken token) {
String binding = regexMatcher.group();
String expression = binding.substring(3, binding.length() - 1);

Matcher inSpelMatcher = PARAMETER_BINDING_PATTERN.matcher(expression);
Matcher inSpelMatcher = SPEL_PARAMETER_BINDING_PATTERN.matcher(expression); // ?0 '?0'
Map<String, Object> innerSpelVariables = new HashMap<>();

while (inSpelMatcher.find()) {

int index = computeParameterIndex(inSpelMatcher.group());
expression = expression.replace(inSpelMatcher.group(), getBindableValueForIndex(index).toString());
String group = inSpelMatcher.group();
int index = computeParameterIndex(group);
Object value = getBindableValueForIndex(index);
String varName = "__QVar" + innerSpelVariables.size();
expression = expression.replace(group, "#" + varName);
if(group.startsWith("'")) { // retain the string semantic
innerSpelVariables.put(varName, nullSafeToString(value));
} else {
innerSpelVariables.put(varName, value);
}
}

Object value = evaluateExpression(expression);
Object value = evaluateExpression(expression, innerSpelVariables);
bindableValue.setValue(value);
bindableValue.setType(bsonTypeForValue(value));
return bindableValue;
Expand Down Expand Up @@ -415,14 +430,24 @@ private BindableValue bindableValueFor(JsonToken token) {
String binding = regexMatcher.group();
String expression = binding.substring(3, binding.length() - 1);

Matcher inSpelMatcher = PARAMETER_BINDING_PATTERN.matcher(expression);
Matcher inSpelMatcher = SPEL_PARAMETER_BINDING_PATTERN.matcher(expression);
Map<String, Object> innerSpelVariables = new HashMap<>();

while (inSpelMatcher.find()) {

int index = computeParameterIndex(inSpelMatcher.group());
expression = expression.replace(inSpelMatcher.group(), getBindableValueForIndex(index).toString());
String group = inSpelMatcher.group();
int index = computeParameterIndex(group);
Object value = getBindableValueForIndex(index);
String varName = "__QVar" + innerSpelVariables.size();
expression = expression.replace(group, "#" + varName);
if(group.startsWith("'")) { // retain the string semantic
innerSpelVariables.put(varName, nullSafeToString(value));
} else {
innerSpelVariables.put(varName, value);
}
}

computedValue = computedValue.replace(binding, nullSafeToString(evaluateExpression(expression)));
computedValue = computedValue.replace(binding, nullSafeToString(evaluateExpression(expression, innerSpelVariables)));

bindableValue.setValue(computedValue);
bindableValue.setType(BsonType.STRING);
Expand Down Expand Up @@ -459,7 +484,7 @@ private static String nullSafeToString(@Nullable Object value) {
}

private static int computeParameterIndex(String parameter) {
return NumberUtils.parseNumber(parameter.replace("?", ""), Integer.class);
return NumberUtils.parseNumber(parameter.replace("?", "").replace("'", ""), Integer.class);
}

private Object getBindableValueForIndex(int index) {
Expand Down Expand Up @@ -511,7 +536,12 @@ private BsonType bsonTypeForValue(Object value) {

@Nullable
private Object evaluateExpression(String expressionString) {
return bindingContext.evaluateExpression(expressionString);
return bindingContext.evaluateExpression(expressionString, Collections.emptyMap());
}

@Nullable
private Object evaluateExpression(String expressionString, Map<String,Object> variables) {
return bindingContext.evaluateExpression(expressionString, variables);
}

// Spring Data Customization END
Expand Down
Expand Up @@ -25,13 +25,15 @@
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.UUID;

import org.bson.Document;
import org.bson.codecs.DecoderContext;
import org.junit.jupiter.api.Test;
import org.springframework.data.spel.EvaluationContextProvider;
import org.springframework.data.spel.ExpressionDependencies;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ParseException;
import org.springframework.expression.TypedValue;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
Expand Down Expand Up @@ -390,6 +392,55 @@ void parsesNullValue() {
assertThat(target).isEqualTo(new Document("parent", null));
}


@Test // GH-4089
void retainsSpelArgumentTypeViaArgumentIndex() {

String source = "new java.lang.Object()";
Document target = parse("{ arg0 : ?#{[0]} }", source);
assertThat(target.get("arg0")).isEqualTo(source);
}

@Test // GH-4089
void retainsSpelArgumentTypeViaParameterPlaceholder() {

String source = "new java.lang.Object()";
Document target = parse("{ arg0 : :#{?0} }", source);
assertThat(target.get("arg0")).isEqualTo(source);
}

@Test // GH-4089
void enforcesStringSpelArgumentTypeViaParameterPlaceholderWhenQuoted() {

Integer source = 10;
Document target = parse("{ arg0 : :#{'?0'} }", source);
assertThat(target.get("arg0")).isEqualTo("10");
}

@Test // GH-4089
void enforcesSpelArgumentTypeViaParameterPlaceholderWhenQuoted() {

String source = "new java.lang.Object()";
Document target = parse("{ arg0 : :#{'?0'} }", source);
assertThat(target.get("arg0")).isEqualTo(source);
}

@Test // GH-4089
void retainsSpelArgumentTypeViaParameterPlaceholderWhenValueContainsSingleQuotes() {

String source = "' + new java.lang.Object() + '";
Document target = parse("{ arg0 : :#{?0} }", source);
assertThat(target.get("arg0")).isEqualTo(source);
}

@Test // GH-4089
void retainsSpelArgumentTypeViaParameterPlaceholderWhenValueContainsDoubleQuotes() {

String source = "\\\" + new java.lang.Object() + \\\"";
Document target = parse("{ arg0 : :#{?0} }", source);
assertThat(target.get("arg0")).isEqualTo(source);
}

private static Document parse(String json, Object... args) {

ParameterBindingJsonReader reader = new ParameterBindingJsonReader(json, args);
Expand Down

0 comments on commit 7c5ac76

Please sign in to comment.