Skip to content

Commit 30a417d

Browse files
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: #4089
1 parent 1671f96 commit 30a417d

File tree

4 files changed

+216
-42
lines changed

4 files changed

+216
-42
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 ENTIRE_QUERY_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

@@ -372,14 +377,24 @@ private BindableValue bindableValueFor(JsonToken token) {
372377
String binding = regexMatcher.group();
373378
String expression = binding.substring(3, binding.length() - 1);
374379

375-
Matcher inSpelMatcher = PARAMETER_BINDING_PATTERN.matcher(expression);
380+
Matcher inSpelMatcher = SPEL_PARAMETER_BINDING_PATTERN.matcher(expression); // ?0 '?0'
381+
Map<String, Object> innerSpelVariables = new HashMap<>();
382+
376383
while (inSpelMatcher.find()) {
377384

378-
int index = computeParameterIndex(inSpelMatcher.group());
379-
expression = expression.replace(inSpelMatcher.group(), getBindableValueForIndex(index).toString());
385+
String group = inSpelMatcher.group();
386+
int index = computeParameterIndex(group);
387+
Object value = getBindableValueForIndex(index);
388+
String varName = "__QVar" + innerSpelVariables.size();
389+
expression = expression.replace(group, "#" + varName);
390+
if(group.startsWith("'")) { // retain the string semantic
391+
innerSpelVariables.put(varName, nullSafeToString(value));
392+
} else {
393+
innerSpelVariables.put(varName, value);
394+
}
380395
}
381396

382-
Object value = evaluateExpression(expression);
397+
Object value = evaluateExpression(expression, innerSpelVariables);
383398
bindableValue.setValue(value);
384399
bindableValue.setType(bsonTypeForValue(value));
385400
return bindableValue;
@@ -408,14 +423,24 @@ private BindableValue bindableValueFor(JsonToken token) {
408423
String binding = regexMatcher.group();
409424
String expression = binding.substring(3, binding.length() - 1);
410425

411-
Matcher inSpelMatcher = PARAMETER_BINDING_PATTERN.matcher(expression);
426+
Matcher inSpelMatcher = SPEL_PARAMETER_BINDING_PATTERN.matcher(expression);
427+
Map<String, Object> innerSpelVariables = new HashMap<>();
428+
412429
while (inSpelMatcher.find()) {
413430

414-
int index = computeParameterIndex(inSpelMatcher.group());
415-
expression = expression.replace(inSpelMatcher.group(), getBindableValueForIndex(index).toString());
431+
String group = inSpelMatcher.group();
432+
int index = computeParameterIndex(group);
433+
Object value = getBindableValueForIndex(index);
434+
String varName = "__QVar" + innerSpelVariables.size();
435+
expression = expression.replace(group, "#" + varName);
436+
if(group.startsWith("'")) { // retain the string semantic
437+
innerSpelVariables.put(varName, nullSafeToString(value));
438+
} else {
439+
innerSpelVariables.put(varName, value);
440+
}
416441
}
417442

418-
computedValue = computedValue.replace(binding, nullSafeToString(evaluateExpression(expression)));
443+
computedValue = computedValue.replace(binding, nullSafeToString(evaluateExpression(expression, innerSpelVariables)));
419444

420445
bindableValue.setValue(computedValue);
421446
bindableValue.setType(BsonType.STRING);
@@ -452,7 +477,7 @@ private static String nullSafeToString(@Nullable Object value) {
452477
}
453478

454479
private static int computeParameterIndex(String parameter) {
455-
return NumberUtils.parseNumber(parameter.replace("?", ""), Integer.class);
480+
return NumberUtils.parseNumber(parameter.replace("?", "").replace("'", ""), Integer.class);
456481
}
457482

458483
private Object getBindableValueForIndex(int index) {
@@ -504,7 +529,12 @@ private BsonType bsonTypeForValue(Object value) {
504529

505530
@Nullable
506531
private Object evaluateExpression(String expressionString) {
507-
return bindingContext.evaluateExpression(expressionString);
532+
return bindingContext.evaluateExpression(expressionString, Collections.emptyMap());
533+
}
534+
535+
@Nullable
536+
private Object evaluateExpression(String expressionString, Map<String,Object> variables) {
537+
return bindingContext.evaluateExpression(expressionString, variables);
508538
}
509539

510540
// Spring Data Customization END

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

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

29-
import org.bson.BsonBinary;
3030
import org.bson.Document;
3131
import org.bson.codecs.DecoderContext;
3232
import org.junit.jupiter.api.Test;
3333
import org.springframework.data.spel.EvaluationContextProvider;
3434
import org.springframework.data.spel.ExpressionDependencies;
3535
import org.springframework.expression.EvaluationContext;
36+
import org.springframework.expression.ParseException;
3637
import org.springframework.expression.TypedValue;
3738
import org.springframework.expression.spel.standard.SpelExpressionParser;
3839
import org.springframework.expression.spel.support.StandardEvaluationContext;
@@ -369,11 +370,11 @@ public void capturingExpressionDependenciesShouldNotThrowParseErrorForSpelOnlyJs
369370
new SpelExpressionParser());
370371
}
371372

372-
@Test // GH-3871
373-
public void bindEntireQueryUsingSpelExpressionWhenEvaluationResultIsJsonString() {
373+
@Test // GH-3871, GH-4089
374+
public void bindEntireQueryUsingSpelExpressionWhenEvaluationResultIsDocument() {
374375

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

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

387-
@Test // GH-3871
388-
public void throwsExceptionWhenbindEntireQueryUsingSpelExpressionResultsInInvalidJsonString() {
388+
@Test // GH-3871, GH-4089
389+
public void throwsExceptionWhenBindEntireQueryUsingSpelExpressionIsMalFormatted() {
389390

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

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

398-
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> new ParameterBindingDocumentCodec().decode(reader, DecoderContext.builder().build()));
400+
new ParameterBindingDocumentCodec().decode(reader, DecoderContext.builder().build());
401+
});
399402
}
400403

401-
@Test // GH-3871
404+
@Test // GH-3871, GH-4089
402405
public void bindEntireQueryUsingSpelExpressionWhenEvaluationResultIsJsonStringContainingUUID() {
403406

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

@@ -411,7 +414,7 @@ public void bindEntireQueryUsingSpelExpressionWhenEvaluationResultIsJsonStringCo
411414

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

414-
assertThat(target.get("name")).isInstanceOf(BsonBinary.class);
417+
assertThat(target.get("name")).isInstanceOf(UUID.class);
415418
}
416419

417420
@Test // GH-3871
@@ -481,6 +484,69 @@ void parsesNullValue() {
481484
assertThat(target).isEqualTo(new Document("parent", null));
482485
}
483486

487+
488+
@Test // GH-4089
489+
void retainsSpelArgumentTypeViaArgumentIndex() {
490+
491+
String source = "new java.lang.Object()";
492+
Document target = parse("{ arg0 : ?#{[0]} }", source);
493+
assertThat(target.get("arg0")).isEqualTo(source);
494+
}
495+
496+
@Test // GH-4089
497+
void retainsSpelArgumentTypeViaParameterPlaceholder() {
498+
499+
String source = "new java.lang.Object()";
500+
Document target = parse("{ arg0 : :#{?0} }", source);
501+
assertThat(target.get("arg0")).isEqualTo(source);
502+
}
503+
504+
@Test // GH-4089
505+
void errorsOnNonDocument() {
506+
507+
String source = "new java.lang.Object()";
508+
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> parse(":#{?0}", source));
509+
}
510+
511+
@Test // GH-4089
512+
void bindsFullDocument() {
513+
514+
Document source = new Document();
515+
assertThat(parse(":#{?0}", source)).isSameAs(source);
516+
}
517+
518+
@Test // GH-4089
519+
void enforcesStringSpelArgumentTypeViaParameterPlaceholderWhenQuoted() {
520+
521+
Integer source = 10;
522+
Document target = parse("{ arg0 : :#{'?0'} }", source);
523+
assertThat(target.get("arg0")).isEqualTo("10");
524+
}
525+
526+
@Test // GH-4089
527+
void enforcesSpelArgumentTypeViaParameterPlaceholderWhenQuoted() {
528+
529+
String source = "new java.lang.Object()";
530+
Document target = parse("{ arg0 : :#{'?0'} }", source);
531+
assertThat(target.get("arg0")).isEqualTo(source);
532+
}
533+
534+
@Test // GH-4089
535+
void retainsSpelArgumentTypeViaParameterPlaceholderWhenValueContainsSingleQuotes() {
536+
537+
String source = "' + new java.lang.Object() + '";
538+
Document target = parse("{ arg0 : :#{?0} }", source);
539+
assertThat(target.get("arg0")).isEqualTo(source);
540+
}
541+
542+
@Test // GH-4089
543+
void retainsSpelArgumentTypeViaParameterPlaceholderWhenValueContainsDoubleQuotes() {
544+
545+
String source = "\\\" + new java.lang.Object() + \\\"";
546+
Document target = parse("{ arg0 : :#{?0} }", source);
547+
assertThat(target.get("arg0")).isEqualTo(source);
548+
}
549+
484550
private static Document parse(String json, Object... args) {
485551

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

0 commit comments

Comments
 (0)