diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/EvaluationContextExpressionEvaluator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/EvaluationContextExpressionEvaluator.java new file mode 100644 index 0000000000..094afe1999 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/EvaluationContextExpressionEvaluator.java @@ -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; + + public EvaluationContextExpressionEvaluator(ValueProvider valueProvider, ExpressionParser expressionParser, + Supplier evaluationContext) { + + this.valueProvider = valueProvider; + this.expressionParser = expressionParser; + this.evaluationContext = evaluationContext; + } + + @Nullable + @Override + public 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 evaluateExpression(String expressionString, Map 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; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingContext.java index cae6651a22..f2b7235872 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingContext.java @@ -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; @@ -58,13 +59,7 @@ public ParameterBindingContext(ValueProvider valueProvider, SpelExpressionParser */ public ParameterBindingContext(ValueProvider valueProvider, ExpressionParser expressionParser, Supplier evaluationContext) { - - this(valueProvider, new SpELExpressionEvaluator() { - @Override - public T evaluate(String expressionString) { - return (T) expressionParser.parseExpression(expressionString).getValue(evaluationContext.get(), Object.class); - } - }); + this(valueProvider, new EvaluationContextExpressionEvaluator(valueProvider, expressionParser, evaluationContext)); } /** @@ -87,20 +82,20 @@ public ParameterBindingContext(ValueProvider valueProvider, SpELExpressionEvalua * @return * @since 3.1 */ - public static ParameterBindingContext forExpressions(ValueProvider valueProvider, - ExpressionParser expressionParser, Function contextFunction) { + public static ParameterBindingContext forExpressions(ValueProvider valueProvider, ExpressionParser expressionParser, + Function contextFunction) { - return new ParameterBindingContext(valueProvider, new SpELExpressionEvaluator() { - @Override - public 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 @@ -113,6 +108,16 @@ public Object evaluateExpression(String expressionString) { return expressionEvaluator.evaluate(expressionString); } + @Nullable + public Object evaluateExpression(String expressionString, Map variables) { + + if (expressionEvaluator instanceof EvaluationContextExpressionEvaluator) { + return ((EvaluationContextExpressionEvaluator) expressionEvaluator).evaluateExpression(expressionString, + variables); + } + return expressionEvaluator.evaluate(expressionString); + } + public ValueProvider getValueProvider() { return valueProvider; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReader.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReader.java index 0f5ec66c86..d19a811364 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReader.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReader.java @@ -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; @@ -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; @@ -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 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; @@ -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 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); @@ -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) { @@ -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 variables) { + return bindingContext.evaluateExpression(expressionString, variables); } // Spring Data Customization END diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReaderUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReaderUnitTests.java index b490848fd2..e2648cf050 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReaderUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReaderUnitTests.java @@ -25,6 +25,7 @@ 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; @@ -32,6 +33,7 @@ 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; @@ -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);