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 1078294 commit 5e241c6
Show file tree
Hide file tree
Showing 4 changed files with 216 additions and 42 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 ENTIRE_QUERY_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 @@ -372,14 +377,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 @@ -408,14 +423,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 @@ -452,7 +477,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 @@ -504,7 +529,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,14 +25,15 @@
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.UUID;

import org.bson.BsonBinary;
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 @@ -369,11 +370,11 @@ public void capturingExpressionDependenciesShouldNotThrowParseErrorForSpelOnlyJs
new SpelExpressionParser());
}

@Test // GH-3871
public void bindEntireQueryUsingSpelExpressionWhenEvaluationResultIsJsonString() {
@Test // GH-3871, GH-4089
public void bindEntireQueryUsingSpelExpressionWhenEvaluationResultIsDocument() {

Object[] args = new Object[] { "expected", "unexpected" };
String json = "?#{ true ? \"{ 'name': ?0 }\" : \"{ 'name' : ?1 }\" }";
String json = "?#{ true ? { 'name': ?0 } : { 'name' : ?1 } }";
StandardEvaluationContext evaluationContext = (StandardEvaluationContext) EvaluationContextProvider.DEFAULT
.getEvaluationContext(args);

Expand All @@ -384,25 +385,27 @@ public void bindEntireQueryUsingSpelExpressionWhenEvaluationResultIsJsonString()
assertThat(target).isEqualTo(new Document("name", "expected"));
}

@Test // GH-3871
public void throwsExceptionWhenbindEntireQueryUsingSpelExpressionResultsInInvalidJsonString() {
@Test // GH-3871, GH-4089
public void throwsExceptionWhenBindEntireQueryUsingSpelExpressionIsMalFormatted() {

Object[] args = new Object[] { "expected", "unexpected" };
String json = "?#{ true ? \"{ 'name': ?0 { }\" : \"{ 'name' : ?1 }\" }";
String json = "?#{ true ? { 'name': ?0 { } } : { 'name' : ?1 } }";
StandardEvaluationContext evaluationContext = (StandardEvaluationContext) EvaluationContextProvider.DEFAULT
.getEvaluationContext(args);

ParameterBindingJsonReader reader = new ParameterBindingJsonReader(json,
new ParameterBindingContext((index) -> args[index], new SpelExpressionParser(), evaluationContext));
assertThatExceptionOfType(ParseException.class).isThrownBy(() -> {
ParameterBindingJsonReader reader = new ParameterBindingJsonReader(json,
new ParameterBindingContext((index) -> args[index], new SpelExpressionParser(), evaluationContext));

assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> new ParameterBindingDocumentCodec().decode(reader, DecoderContext.builder().build()));
new ParameterBindingDocumentCodec().decode(reader, DecoderContext.builder().build());
});
}

@Test // GH-3871
@Test // GH-3871, GH-4089
public void bindEntireQueryUsingSpelExpressionWhenEvaluationResultIsJsonStringContainingUUID() {

Object[] args = new Object[] { "UUID('cfbca728-4e39-4613-96bc-f920b5c37e16')", "unexpected" };
String json = "?#{ true ? \"{ 'name': ?0 }\" : \"{ 'name' : ?1 }\" }";
Object[] args = new Object[] { UUID.fromString("cfbca728-4e39-4613-96bc-f920b5c37e16"), "unexpected" };
String json = "?#{ true ? { 'name': ?0 } : { 'name' : ?1 } }";
StandardEvaluationContext evaluationContext = (StandardEvaluationContext) EvaluationContextProvider.DEFAULT
.getEvaluationContext(args);

Expand All @@ -411,7 +414,7 @@ public void bindEntireQueryUsingSpelExpressionWhenEvaluationResultIsJsonStringCo

Document target = new ParameterBindingDocumentCodec().decode(reader, DecoderContext.builder().build());

assertThat(target.get("name")).isInstanceOf(BsonBinary.class);
assertThat(target.get("name")).isInstanceOf(UUID.class);
}

@Test // GH-3871
Expand Down Expand Up @@ -481,6 +484,69 @@ 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 errorsOnNonDocument() {

String source = "new java.lang.Object()";
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> parse(":#{?0}", source));
}

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

Document source = new Document();
assertThat(parse(":#{?0}", source)).isSameAs(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 5e241c6

Please sign in to comment.