Skip to content

Commit 21c2297

Browse files
Consider context when binding string parameters.
Closes: #5095
1 parent 7f54f82 commit 21c2297

File tree

4 files changed

+46
-41
lines changed

4 files changed

+46
-41
lines changed

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ public class ParameterBindingJsonReader extends AbstractBsonReader {
6868
private static final Pattern PARAMETER_BINDING_PATTERN = Pattern.compile("\\?(\\d+)");
6969
private static final Pattern EXPRESSION_BINDING_PATTERN = Pattern.compile("[\\?:][#$]\\{.*\\}");
7070
private static final Pattern SPEL_PARAMETER_BINDING_PATTERN = Pattern.compile("('\\?(\\d+)'|\\?(\\d+))");
71+
private static final String QUOTE_START = "\\Q";
72+
private static final String QUOTE_END = "\\E";
7173

7274
private final ParameterBindingContext bindingContext;
7375

@@ -456,7 +458,13 @@ private BindableValue bindableValueFor(JsonToken token) {
456458

457459
String group = matcher.group();
458460
int index = computeParameterIndex(group);
459-
computedValue = computedValue.replace(group, nullSafeToString(getBindableValueForIndex(index)));
461+
462+
String bindValue = nullSafeToString(getBindableValueForIndex(index));
463+
if(isQuoted(tokenValue)) {
464+
bindValue = bindValue.replaceAll("\\%s".formatted(QUOTE_START), Matcher.quoteReplacement("\\%s".formatted(QUOTE_START))) //
465+
.replaceAll("\\%s".formatted(QUOTE_END), Matcher.quoteReplacement("\\%s".formatted(QUOTE_END)));
466+
}
467+
computedValue = computedValue.replace(group, bindValue);
460468
}
461469

462470
if (isRegularExpression) {
@@ -482,6 +490,10 @@ private static String nullSafeToString(@Nullable Object value) {
482490
return ObjectUtils.nullSafeToString(value);
483491
}
484492

493+
private static boolean isQuoted(String value) {
494+
return value.contains(QUOTE_START) || value.contains(QUOTE_END);
495+
}
496+
485497
private static int computeParameterIndex(String parameter) {
486498
return NumberUtils.parseNumber(parameter.replace("?", "").replace("'", ""), Integer.class);
487499
}

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -728,6 +728,14 @@ void findByFirstnameStartingWithIgnoreCase() {
728728
assertThat(result.get(0)).isEqualTo(dave);
729729
}
730730

731+
@Test // DATAMONGO-770
732+
void findByFirstnameStartingWith() {
733+
734+
String inputString = "\\E.*\\Q";
735+
List<Person> result = repository.findByFirstnameStartingWith(inputString);
736+
assertThat(result).isEmpty();
737+
}
738+
731739
@Test // DATAMONGO-770
732740
void findByFirstnameEndingWithIgnoreCase() {
733741

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,8 @@ Window<Person> findByLastnameLikeOrderByLastnameAscFirstnameAsc(String lastname,
273273
// DATAMONGO-770
274274
List<Person> findByFirstnameNotIgnoreCase(String firstName);
275275

276+
List<Person> findByFirstnameStartingWith(String firstName);
277+
276278
// DATAMONGO-770
277279
List<Person> findByFirstnameStartingWithIgnoreCase(String firstName);
278280

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

Lines changed: 23 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,17 @@
2424
import java.util.Date;
2525
import java.util.List;
2626
import java.util.UUID;
27+
import java.util.stream.Stream;
2728

2829
import org.bson.BsonBinary;
2930
import org.bson.BsonBinarySubType;
3031
import org.bson.BsonRegularExpression;
3132
import org.bson.Document;
3233
import org.bson.codecs.DecoderContext;
3334
import org.junit.jupiter.api.Test;
35+
import org.junit.jupiter.params.ParameterizedTest;
36+
import org.junit.jupiter.params.provider.Arguments;
37+
import org.junit.jupiter.params.provider.MethodSource;
3438
import org.springframework.data.expression.ValueExpressionParser;
3539
import org.springframework.data.spel.EvaluationContextProvider;
3640
import org.springframework.data.spel.ExpressionDependencies;
@@ -84,47 +88,12 @@ void bindQuotedIntegerValue() {
8488
assertThat(target).isEqualTo(new Document("lastname", "100"));
8589
}
8690

87-
@Test // GH-4806
88-
void regexConsidersOptions() {
91+
@ParameterizedTest // GH-4806
92+
@MethodSource("treatNestedStringParametersArgs")
93+
void treatNestedStringParameters(String source, String value, Object expected) {
8994

90-
Document target = parse("{ 'c': /^true$/i }");
91-
92-
BsonRegularExpression pattern = target.get("c", BsonRegularExpression.class);
93-
assertThat(pattern.getPattern()).isEqualTo("^true$");
94-
assertThat(pattern.getOptions()).isEqualTo("i");
95-
}
96-
97-
@Test // GH-4806
98-
void regexConsidersBindValueWithOptions() {
99-
100-
Document target = parse("{ 'c': /^?0$/i }", "foo");
101-
102-
BsonRegularExpression pattern = target.get("c", BsonRegularExpression.class);
103-
assertThat(pattern.getPattern()).isEqualTo("^foo$");
104-
assertThat(pattern.getOptions()).isEqualTo("i");
105-
}
106-
107-
@Test // GH-4806
108-
void treatsQuotedValueThatLooksLikeRegexAsPlainString() {
109-
110-
Document target = parse("{ 'c': '/^?0$/i' }", "foo");
111-
112-
assertThat(target.get("c")).isInstanceOf(String.class);
113-
}
114-
115-
@Test // GH-4806
116-
void treatsStringParameterValueThatLooksLikeRegexAsPlainString() {
117-
118-
Document target = parse("{ 'c': ?0 }", "/^foo$/i");
119-
120-
assertThat(target.get("c")).isInstanceOf(String.class);
121-
}
122-
123-
@Test
124-
void bindValueToRegex() {
125-
126-
Document target = parse("{ 'lastname' : { '$regex' : '^(?0)'} }", "kohlin");
127-
assertThat(target).isEqualTo(Document.parse("{ 'lastname' : { '$regex' : '^(kohlin)'} }"));
95+
Document target = parse(source, value);
96+
assertThat(target.get("value")).isEqualTo(expected);
12897
}
12998

13099
@Test
@@ -634,6 +603,20 @@ void shouldParseUUIDasStandardRepresentation() {
634603
assertThat(value.getType()).isEqualTo(BsonBinarySubType.UUID_STANDARD.getValue());
635604
}
636605

606+
static Stream<Arguments> treatNestedStringParametersArgs() {
607+
return Stream.of( //
608+
Arguments.of("{ 'value': '/^?0$/i' }", "foo", "/^foo$/i"),
609+
Arguments.of("{ 'value': /^true$/i }", null, new BsonRegularExpression("^true$", "i")),
610+
Arguments.of("{ 'value': /^?0$/i }", "foo", new BsonRegularExpression("^foo$", "i")), //
611+
Arguments.of("{ 'value': '/^?0$/i' }", "\\Qfoo\\E", "/^\\Qfoo\\E$/i"),
612+
Arguments.of("{ 'value': '?0' }", "/^foo$/i", "/^foo$/i"), //
613+
Arguments.of("{ 'value': /^\\Q?0\\E/}", "foo", new BsonRegularExpression("^\\Qfoo\\E")), //
614+
Arguments.of("{ 'value': /^\\Q?0\\E/}", "\\E.*", new BsonRegularExpression("^\\Q\\\\E.*\\E")), //
615+
Arguments.of("{ 'value': ?0 }", "/^foo$/i", "/^foo$/i"), //
616+
Arguments.of("{ 'value': { '$regex' : '^(?0)'} }", "foo", new Document("$regex", "^(foo)")) //
617+
);
618+
}
619+
637620
private static Document parse(String json, Object... args) {
638621

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

0 commit comments

Comments
 (0)