From f2ca3b3f58743e8ef2828679d42cb0c2dcbdd381 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 14 Nov 2025 10:13:48 +0100 Subject: [PATCH] Consider context when binding string parameters. Closes: #5095 --- .../util/json/ParameterBindingJsonReader.java | 14 +++- ...tractPersonRepositoryIntegrationTests.java | 8 +++ .../mongodb/repository/PersonRepository.java | 2 + .../ParameterBindingJsonReaderUnitTests.java | 64 +++++++------------ 4 files changed, 46 insertions(+), 42 deletions(-) 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 c1e519e2f..850cafea0 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 @@ -70,6 +70,8 @@ public class ParameterBindingJsonReader extends AbstractBsonReader { 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 static final String QUOTE_START = "\\Q"; + private static final String QUOTE_END = "\\E"; private final ParameterBindingContext bindingContext; @@ -458,7 +460,13 @@ public class ParameterBindingJsonReader extends AbstractBsonReader { String group = matcher.group(); int index = computeParameterIndex(group); - computedValue = computedValue.replace(group, nullSafeToString(getBindableValueForIndex(index))); + + String bindValue = nullSafeToString(getBindableValueForIndex(index)); + if(isQuoted(tokenValue)) { + bindValue = bindValue.replaceAll("\\%s".formatted(QUOTE_START), Matcher.quoteReplacement("\\%s".formatted(QUOTE_START))) // + .replaceAll("\\%s".formatted(QUOTE_END), Matcher.quoteReplacement("\\%s".formatted(QUOTE_END))); + } + computedValue = computedValue.replace(group, bindValue); } if (isRegularExpression) { @@ -484,6 +492,10 @@ public class ParameterBindingJsonReader extends AbstractBsonReader { return ObjectUtils.nullSafeToString(value); } + private static boolean isQuoted(String value) { + return value.contains(QUOTE_START) || value.contains(QUOTE_END); + } + private static int computeParameterIndex(String parameter) { return NumberUtils.parseNumber(parameter.replace("?", "").replace("'", ""), Integer.class); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java index 9cd2127a1..cdcecc8a6 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java @@ -826,6 +826,14 @@ public abstract class AbstractPersonRepositoryIntegrationTests implements Dirtie assertThat(result.get(0)).isEqualTo(dave); } + @Test // DATAMONGO-770 + void findByFirstnameStartingWith() { + + String inputString = "\\E.*\\Q"; + List result = repository.findByFirstnameStartingWith(inputString); + assertThat(result).isEmpty(); + } + @Test // DATAMONGO-770 void findByFirstnameEndingWithIgnoreCase() { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java index cf8e265ca..ace340e9d 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java @@ -295,6 +295,8 @@ public interface PersonRepository extends MongoRepository, Query // DATAMONGO-770 List findByFirstnameNotIgnoreCase(String firstName); + List findByFirstnameStartingWith(String firstName); + // DATAMONGO-770 List findByFirstnameStartingWithIgnoreCase(String firstName); 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 dc3cae8bd..071e756ad 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 @@ -23,6 +23,7 @@ import java.util.Collections; import java.util.Date; import java.util.List; import java.util.UUID; +import java.util.stream.Stream; import org.bson.BsonBinary; import org.bson.BsonBinarySubType; @@ -30,7 +31,9 @@ import org.bson.BsonRegularExpression; import org.bson.Document; import org.bson.codecs.DecoderContext; import org.junit.jupiter.api.Test; - +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.data.expression.ValueExpressionParser; import org.springframework.data.spel.EvaluationContextProvider; import org.springframework.data.spel.ExpressionDependencies; @@ -84,47 +87,12 @@ class ParameterBindingJsonReaderUnitTests { assertThat(target).isEqualTo(new Document("lastname", "100")); } - @Test // GH-4806 - void regexConsidersOptions() { - - Document target = parse("{ 'c': /^true$/i }"); - - BsonRegularExpression pattern = target.get("c", BsonRegularExpression.class); - assertThat(pattern.getPattern()).isEqualTo("^true$"); - assertThat(pattern.getOptions()).isEqualTo("i"); - } - - @Test // GH-4806 - void regexConsidersBindValueWithOptions() { - - Document target = parse("{ 'c': /^?0$/i }", "foo"); - - BsonRegularExpression pattern = target.get("c", BsonRegularExpression.class); - assertThat(pattern.getPattern()).isEqualTo("^foo$"); - assertThat(pattern.getOptions()).isEqualTo("i"); - } - - @Test // GH-4806 - void treatsQuotedValueThatLooksLikeRegexAsPlainString() { - - Document target = parse("{ 'c': '/^?0$/i' }", "foo"); - - assertThat(target.get("c")).isInstanceOf(String.class); - } - - @Test // GH-4806 - void treatsStringParameterValueThatLooksLikeRegexAsPlainString() { - - Document target = parse("{ 'c': ?0 }", "/^foo$/i"); - - assertThat(target.get("c")).isInstanceOf(String.class); - } - - @Test - void bindValueToRegex() { + @ParameterizedTest // GH-4806 + @MethodSource("treatNestedStringParametersArgs") + void treatNestedStringParameters(String source, String value, Object expected) { - Document target = parse("{ 'lastname' : { '$regex' : '^(?0)'} }", "kohlin"); - assertThat(target).isEqualTo(Document.parse("{ 'lastname' : { '$regex' : '^(kohlin)'} }")); + Document target = parse(source, value); + assertThat(target.get("value")).isEqualTo(expected); } @Test @@ -634,6 +602,20 @@ class ParameterBindingJsonReaderUnitTests { assertThat(value.getType()).isEqualTo(BsonBinarySubType.UUID_STANDARD.getValue()); } + static Stream treatNestedStringParametersArgs() { + return Stream.of( // + Arguments.of("{ 'value': '/^?0$/i' }", "foo", "/^foo$/i"), + Arguments.of("{ 'value': /^true$/i }", null, new BsonRegularExpression("^true$", "i")), + Arguments.of("{ 'value': /^?0$/i }", "foo", new BsonRegularExpression("^foo$", "i")), // + Arguments.of("{ 'value': '/^?0$/i' }", "\\Qfoo\\E", "/^\\Qfoo\\E$/i"), + Arguments.of("{ 'value': '?0' }", "/^foo$/i", "/^foo$/i"), // + Arguments.of("{ 'value': /^\\Q?0\\E/}", "foo", new BsonRegularExpression("^\\Qfoo\\E")), // + Arguments.of("{ 'value': /^\\Q?0\\E/}", "\\E.*", new BsonRegularExpression("^\\Q\\\\E.*\\E")), // + Arguments.of("{ 'value': ?0 }", "/^foo$/i", "/^foo$/i"), // + Arguments.of("{ 'value': { '$regex' : '^(?0)'} }", "foo", new Document("$regex", "^(foo)")) // + ); + } + private static Document parse(String json, Object... args) { return new ParameterBindingDocumentCodec().decode(json, args); }