Skip to content

Commit 3a301db

Browse files
christophstroblhenrikplate
authored andcommitted
Retain parameter type when binding parameters in annotated Query/Aggregation.
This commit ensures the parameter type is preserved when binding parameters used within the value of the Query or Aggregation annotation Closes: spring-projects#4089 (cherry picked from commit 7c5ac76) Signed-off-by: henrikplate <[email protected]>
1 parent 7ff1106 commit 3a301db

File tree

4 files changed

+187
-28
lines changed

4 files changed

+187
-28
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.mongodb.util.json;
17+
18+
import java.util.Collections;
19+
import java.util.List;
20+
import java.util.Map;
21+
import java.util.function.Supplier;
22+
23+
import org.springframework.data.mapping.model.SpELExpressionEvaluator;
24+
import org.springframework.expression.EvaluationContext;
25+
import org.springframework.expression.ExpressionParser;
26+
import org.springframework.expression.spel.standard.SpelExpression;
27+
import org.springframework.expression.spel.standard.SpelExpressionParser;
28+
import org.springframework.expression.spel.support.StandardEvaluationContext;
29+
import org.springframework.lang.Nullable;
30+
31+
/**
32+
* @author Christoph Strobl
33+
* @since 3.3.5
34+
*/
35+
class EvaluationContextExpressionEvaluator implements SpELExpressionEvaluator {
36+
37+
ValueProvider valueProvider;
38+
ExpressionParser expressionParser;
39+
Supplier<EvaluationContext> evaluationContext;
40+
41+
public EvaluationContextExpressionEvaluator(ValueProvider valueProvider, ExpressionParser expressionParser,
42+
Supplier<EvaluationContext> evaluationContext) {
43+
44+
this.valueProvider = valueProvider;
45+
this.expressionParser = expressionParser;
46+
this.evaluationContext = evaluationContext;
47+
}
48+
49+
@Nullable
50+
@Override
51+
public <T> T evaluate(String expression) {
52+
return evaluateExpression(expression, Collections.emptyMap());
53+
}
54+
55+
public EvaluationContext getEvaluationContext(String expressionString) {
56+
return evaluationContext != null ? evaluationContext.get() : new StandardEvaluationContext();
57+
}
58+
59+
public SpelExpression getParsedExpression(String expressionString) {
60+
return (SpelExpression) (expressionParser != null ? expressionParser : new SpelExpressionParser())
61+
.parseExpression(expressionString);
62+
}
63+
64+
public <T> T evaluateExpression(String expressionString, Map<String, Object> variables) {
65+
66+
SpelExpression expression = getParsedExpression(expressionString);
67+
EvaluationContext ctx = getEvaluationContext(expressionString);
68+
variables.entrySet().forEach(entry -> ctx.setVariable(entry.getKey(), entry.getValue()));
69+
70+
Object result = expression.getValue(ctx, Object.class);
71+
return (T) result;
72+
}
73+
}

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingContext.java

+23-18
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package org.springframework.data.mongodb.util.json;
1717

18+
import java.util.Map;
1819
import java.util.function.Function;
1920
import java.util.function.Supplier;
2021

@@ -58,13 +59,7 @@ public ParameterBindingContext(ValueProvider valueProvider, SpelExpressionParser
5859
*/
5960
public ParameterBindingContext(ValueProvider valueProvider, ExpressionParser expressionParser,
6061
Supplier<EvaluationContext> evaluationContext) {
61-
62-
this(valueProvider, new SpELExpressionEvaluator() {
63-
@Override
64-
public <T> T evaluate(String expressionString) {
65-
return (T) expressionParser.parseExpression(expressionString).getValue(evaluationContext.get(), Object.class);
66-
}
67-
});
62+
this(valueProvider, new EvaluationContextExpressionEvaluator(valueProvider, expressionParser, evaluationContext));
6863
}
6964

7065
/**
@@ -87,20 +82,20 @@ public ParameterBindingContext(ValueProvider valueProvider, SpELExpressionEvalua
8782
* @return
8883
* @since 3.1
8984
*/
90-
public static ParameterBindingContext forExpressions(ValueProvider valueProvider,
91-
ExpressionParser expressionParser, Function<ExpressionDependencies, EvaluationContext> contextFunction) {
85+
public static ParameterBindingContext forExpressions(ValueProvider valueProvider, ExpressionParser expressionParser,
86+
Function<ExpressionDependencies, EvaluationContext> contextFunction) {
9287

93-
return new ParameterBindingContext(valueProvider, new SpELExpressionEvaluator() {
94-
@Override
95-
public <T> T evaluate(String expressionString) {
88+
return new ParameterBindingContext(valueProvider,
89+
new EvaluationContextExpressionEvaluator(valueProvider, expressionParser, null) {
9690

97-
Expression expression = expressionParser.parseExpression(expressionString);
98-
ExpressionDependencies dependencies = ExpressionDependencies.discover(expression);
99-
EvaluationContext evaluationContext = contextFunction.apply(dependencies);
91+
@Override
92+
public EvaluationContext getEvaluationContext(String expressionString) {
10093

101-
return (T) expression.getValue(evaluationContext, Object.class);
102-
}
103-
});
94+
Expression expression = getParsedExpression(expressionString);
95+
ExpressionDependencies dependencies = ExpressionDependencies.discover(expression);
96+
return contextFunction.apply(dependencies);
97+
}
98+
});
10499
}
105100

106101
@Nullable
@@ -113,6 +108,16 @@ public Object evaluateExpression(String expressionString) {
113108
return expressionEvaluator.evaluate(expressionString);
114109
}
115110

111+
@Nullable
112+
public Object evaluateExpression(String expressionString, Map<String, Object> variables) {
113+
114+
if (expressionEvaluator instanceof EvaluationContextExpressionEvaluator) {
115+
return ((EvaluationContextExpressionEvaluator) expressionEvaluator).evaluateExpression(expressionString,
116+
variables);
117+
}
118+
return expressionEvaluator.evaluate(expressionString);
119+
}
120+
116121
public ValueProvider getValueProvider() {
117122
return valueProvider;
118123
}

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReader.java

+40-10
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,12 @@
2020
import java.text.DateFormat;
2121
import java.text.ParsePosition;
2222
import java.text.SimpleDateFormat;
23+
import java.util.ArrayList;
2324
import java.util.Calendar;
25+
import java.util.Collections;
2426
import java.util.Date;
27+
import java.util.HashMap;
28+
import java.util.List;
2529
import java.util.Locale;
2630
import java.util.Map;
2731
import java.util.TimeZone;
@@ -64,6 +68,7 @@ public class ParameterBindingJsonReader extends AbstractBsonReader {
6468
private static final Pattern PARAMETER_ONLY_BINDING_PATTERN = Pattern.compile("^\\?(\\d+)$");
6569
private static final Pattern PARAMETER_BINDING_PATTERN = Pattern.compile("\\?(\\d+)");
6670
private static final Pattern EXPRESSION_BINDING_PATTERN = Pattern.compile("[\\?:]#\\{.*\\}");
71+
private static final Pattern SPEL_PARAMETER_BINDING_PATTERN = Pattern.compile("('\\?(\\d+)'|\\?(\\d+))");
6772

6873
private final ParameterBindingContext bindingContext;
6974

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

382-
Matcher inSpelMatcher = PARAMETER_BINDING_PATTERN.matcher(expression);
387+
Matcher inSpelMatcher = SPEL_PARAMETER_BINDING_PATTERN.matcher(expression); // ?0 '?0'
388+
Map<String, Object> innerSpelVariables = new HashMap<>();
389+
383390
while (inSpelMatcher.find()) {
384391

385-
int index = computeParameterIndex(inSpelMatcher.group());
386-
expression = expression.replace(inSpelMatcher.group(), getBindableValueForIndex(index).toString());
392+
String group = inSpelMatcher.group();
393+
int index = computeParameterIndex(group);
394+
Object value = getBindableValueForIndex(index);
395+
String varName = "__QVar" + innerSpelVariables.size();
396+
expression = expression.replace(group, "#" + varName);
397+
if(group.startsWith("'")) { // retain the string semantic
398+
innerSpelVariables.put(varName, nullSafeToString(value));
399+
} else {
400+
innerSpelVariables.put(varName, value);
401+
}
387402
}
388403

389-
Object value = evaluateExpression(expression);
404+
Object value = evaluateExpression(expression, innerSpelVariables);
390405
bindableValue.setValue(value);
391406
bindableValue.setType(bsonTypeForValue(value));
392407
return bindableValue;
@@ -415,14 +430,24 @@ private BindableValue bindableValueFor(JsonToken token) {
415430
String binding = regexMatcher.group();
416431
String expression = binding.substring(3, binding.length() - 1);
417432

418-
Matcher inSpelMatcher = PARAMETER_BINDING_PATTERN.matcher(expression);
433+
Matcher inSpelMatcher = SPEL_PARAMETER_BINDING_PATTERN.matcher(expression);
434+
Map<String, Object> innerSpelVariables = new HashMap<>();
435+
419436
while (inSpelMatcher.find()) {
420437

421-
int index = computeParameterIndex(inSpelMatcher.group());
422-
expression = expression.replace(inSpelMatcher.group(), getBindableValueForIndex(index).toString());
438+
String group = inSpelMatcher.group();
439+
int index = computeParameterIndex(group);
440+
Object value = getBindableValueForIndex(index);
441+
String varName = "__QVar" + innerSpelVariables.size();
442+
expression = expression.replace(group, "#" + varName);
443+
if(group.startsWith("'")) { // retain the string semantic
444+
innerSpelVariables.put(varName, nullSafeToString(value));
445+
} else {
446+
innerSpelVariables.put(varName, value);
447+
}
423448
}
424449

425-
computedValue = computedValue.replace(binding, nullSafeToString(evaluateExpression(expression)));
450+
computedValue = computedValue.replace(binding, nullSafeToString(evaluateExpression(expression, innerSpelVariables)));
426451

427452
bindableValue.setValue(computedValue);
428453
bindableValue.setType(BsonType.STRING);
@@ -459,7 +484,7 @@ private static String nullSafeToString(@Nullable Object value) {
459484
}
460485

461486
private static int computeParameterIndex(String parameter) {
462-
return NumberUtils.parseNumber(parameter.replace("?", ""), Integer.class);
487+
return NumberUtils.parseNumber(parameter.replace("?", "").replace("'", ""), Integer.class);
463488
}
464489

465490
private Object getBindableValueForIndex(int index) {
@@ -511,7 +536,12 @@ private BsonType bsonTypeForValue(Object value) {
511536

512537
@Nullable
513538
private Object evaluateExpression(String expressionString) {
514-
return bindingContext.evaluateExpression(expressionString);
539+
return bindingContext.evaluateExpression(expressionString, Collections.emptyMap());
540+
}
541+
542+
@Nullable
543+
private Object evaluateExpression(String expressionString, Map<String,Object> variables) {
544+
return bindingContext.evaluateExpression(expressionString, variables);
515545
}
516546

517547
// Spring Data Customization END

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReaderUnitTests.java

+51
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,15 @@
2525
import java.util.Collections;
2626
import java.util.Date;
2727
import java.util.List;
28+
import java.util.UUID;
2829

2930
import org.bson.Document;
3031
import org.bson.codecs.DecoderContext;
3132
import org.junit.jupiter.api.Test;
3233
import org.springframework.data.spel.EvaluationContextProvider;
3334
import org.springframework.data.spel.ExpressionDependencies;
3435
import org.springframework.expression.EvaluationContext;
36+
import org.springframework.expression.ParseException;
3537
import org.springframework.expression.TypedValue;
3638
import org.springframework.expression.spel.standard.SpelExpressionParser;
3739
import org.springframework.expression.spel.support.StandardEvaluationContext;
@@ -390,6 +392,55 @@ void parsesNullValue() {
390392
assertThat(target).isEqualTo(new Document("parent", null));
391393
}
392394

395+
396+
@Test // GH-4089
397+
void retainsSpelArgumentTypeViaArgumentIndex() {
398+
399+
String source = "new java.lang.Object()";
400+
Document target = parse("{ arg0 : ?#{[0]} }", source);
401+
assertThat(target.get("arg0")).isEqualTo(source);
402+
}
403+
404+
@Test // GH-4089
405+
void retainsSpelArgumentTypeViaParameterPlaceholder() {
406+
407+
String source = "new java.lang.Object()";
408+
Document target = parse("{ arg0 : :#{?0} }", source);
409+
assertThat(target.get("arg0")).isEqualTo(source);
410+
}
411+
412+
@Test // GH-4089
413+
void enforcesStringSpelArgumentTypeViaParameterPlaceholderWhenQuoted() {
414+
415+
Integer source = 10;
416+
Document target = parse("{ arg0 : :#{'?0'} }", source);
417+
assertThat(target.get("arg0")).isEqualTo("10");
418+
}
419+
420+
@Test // GH-4089
421+
void enforcesSpelArgumentTypeViaParameterPlaceholderWhenQuoted() {
422+
423+
String source = "new java.lang.Object()";
424+
Document target = parse("{ arg0 : :#{'?0'} }", source);
425+
assertThat(target.get("arg0")).isEqualTo(source);
426+
}
427+
428+
@Test // GH-4089
429+
void retainsSpelArgumentTypeViaParameterPlaceholderWhenValueContainsSingleQuotes() {
430+
431+
String source = "' + new java.lang.Object() + '";
432+
Document target = parse("{ arg0 : :#{?0} }", source);
433+
assertThat(target.get("arg0")).isEqualTo(source);
434+
}
435+
436+
@Test // GH-4089
437+
void retainsSpelArgumentTypeViaParameterPlaceholderWhenValueContainsDoubleQuotes() {
438+
439+
String source = "\\\" + new java.lang.Object() + \\\"";
440+
Document target = parse("{ arg0 : :#{?0} }", source);
441+
assertThat(target.get("arg0")).isEqualTo(source);
442+
}
443+
393444
private static Document parse(String json, Object... args) {
394445

395446
ParameterBindingJsonReader reader = new ParameterBindingJsonReader(json, args);

0 commit comments

Comments
 (0)