Skip to content

Commit f2ca3b3

Browse files
Consider context when binding string parameters.
Closes: #5095
1 parent 45f8631 commit f2ca3b3

File tree

4 files changed

+46
-42
lines changed

4 files changed

+46
-42
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
@@ -70,6 +70,8 @@ public class ParameterBindingJsonReader extends AbstractBsonReader {
7070
private static final Pattern PARAMETER_BINDING_PATTERN = Pattern.compile("\\?(\\d+)");
7171
private static final Pattern EXPRESSION_BINDING_PATTERN = Pattern.compile("[\\?:][#$]\\{.*\\}");
7272
private static final Pattern SPEL_PARAMETER_BINDING_PATTERN = Pattern.compile("('\\?(\\d+)'|\\?(\\d+))");
73+
private static final String QUOTE_START = "\\Q";
74+
private static final String QUOTE_END = "\\E";
7375

7476
private final ParameterBindingContext bindingContext;
7577

@@ -458,7 +460,13 @@ private BindableValue bindableValueFor(JsonToken token) {
458460

459461
String group = matcher.group();
460462
int index = computeParameterIndex(group);
461-
computedValue = computedValue.replace(group, nullSafeToString(getBindableValueForIndex(index)));
463+
464+
String bindValue = nullSafeToString(getBindableValueForIndex(index));
465+
if(isQuoted(tokenValue)) {
466+
bindValue = bindValue.replaceAll("\\%s".formatted(QUOTE_START), Matcher.quoteReplacement("\\%s".formatted(QUOTE_START))) //
467+
.replaceAll("\\%s".formatted(QUOTE_END), Matcher.quoteReplacement("\\%s".formatted(QUOTE_END)));
468+
}
469+
computedValue = computedValue.replace(group, bindValue);
462470
}
463471

464472
if (isRegularExpression) {
@@ -484,6 +492,10 @@ private static String nullSafeToString(@Nullable Object value) {
484492
return ObjectUtils.nullSafeToString(value);
485493
}
486494

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

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
@@ -826,6 +826,14 @@ void findByFirstnameStartingWithIgnoreCase() {
826826
assertThat(result.get(0)).isEqualTo(dave);
827827
}
828828

829+
@Test // DATAMONGO-770
830+
void findByFirstnameStartingWith() {
831+
832+
String inputString = "\\E.*\\Q";
833+
List<Person> result = repository.findByFirstnameStartingWith(inputString);
834+
assertThat(result).isEmpty();
835+
}
836+
829837
@Test // DATAMONGO-770
830838
void findByFirstnameEndingWithIgnoreCase() {
831839

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
@@ -295,6 +295,8 @@ Window<Person> findByLastnameLikeOrderByLastnameAscFirstnameAsc(Pattern lastname
295295
// DATAMONGO-770
296296
List<Person> findByFirstnameNotIgnoreCase(String firstName);
297297

298+
List<Person> findByFirstnameStartingWith(String firstName);
299+
298300
// DATAMONGO-770
299301
List<Person> findByFirstnameStartingWithIgnoreCase(String firstName);
300302

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

Lines changed: 23 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,17 @@
2323
import java.util.Date;
2424
import java.util.List;
2525
import java.util.UUID;
26+
import java.util.stream.Stream;
2627

2728
import org.bson.BsonBinary;
2829
import org.bson.BsonBinarySubType;
2930
import org.bson.BsonRegularExpression;
3031
import org.bson.Document;
3132
import org.bson.codecs.DecoderContext;
3233
import org.junit.jupiter.api.Test;
33-
34+
import org.junit.jupiter.params.ParameterizedTest;
35+
import org.junit.jupiter.params.provider.Arguments;
36+
import org.junit.jupiter.params.provider.MethodSource;
3437
import org.springframework.data.expression.ValueExpressionParser;
3538
import org.springframework.data.spel.EvaluationContextProvider;
3639
import org.springframework.data.spel.ExpressionDependencies;
@@ -84,47 +87,12 @@ void bindQuotedIntegerValue() {
8487
assertThat(target).isEqualTo(new Document("lastname", "100"));
8588
}
8689

87-
@Test // GH-4806
88-
void regexConsidersOptions() {
89-
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() {
90+
@ParameterizedTest // GH-4806
91+
@MethodSource("treatNestedStringParametersArgs")
92+
void treatNestedStringParameters(String source, String value, Object expected) {
12593

126-
Document target = parse("{ 'lastname' : { '$regex' : '^(?0)'} }", "kohlin");
127-
assertThat(target).isEqualTo(Document.parse("{ 'lastname' : { '$regex' : '^(kohlin)'} }"));
94+
Document target = parse(source, value);
95+
assertThat(target.get("value")).isEqualTo(expected);
12896
}
12997

13098
@Test
@@ -634,6 +602,20 @@ void shouldParseUUIDasStandardRepresentation() {
634602
assertThat(value.getType()).isEqualTo(BsonBinarySubType.UUID_STANDARD.getValue());
635603
}
636604

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

0 commit comments

Comments
 (0)