Skip to content

Commit 5e5459b

Browse files
committed
Invoke PropertyValueConverter only for matching types.
When the type does not match we simply use the unconverted value. This happens when comparing properties to regexes. Applying the conversion to the String value of the regex doesn't makes sense since we are not dealing with complete values. Users may still provide converters taking Objects and mangle Regex patterns as they desire. Also fixed the null handling of PropertyValueConverters. Removed a few superfluous `@Nullable` annotations where they became obvious Closes #4346
1 parent d6fd555 commit 5e5459b

File tree

5 files changed

+145
-37
lines changed

5 files changed

+145
-37
lines changed

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,8 @@
1515
*/
1616
package org.springframework.data.mongodb.core.convert;
1717

18-
import java.util.AbstractMap;
19-
import java.util.ArrayList;
20-
import java.util.Arrays;
21-
import java.util.Collection;
22-
import java.util.Iterator;
23-
import java.util.LinkedHashMap;
24-
import java.util.List;
25-
import java.util.Map;
18+
import java.util.*;
2619
import java.util.Map.Entry;
27-
import java.util.Optional;
28-
import java.util.Set;
2920
import java.util.regex.Matcher;
3021
import java.util.regex.Pattern;
3122
import java.util.stream.Collectors;
@@ -38,7 +29,7 @@
3829
import org.bson.conversions.Bson;
3930
import org.bson.types.ObjectId;
4031
import org.jspecify.annotations.Nullable;
41-
32+
import org.springframework.core.GenericTypeResolver;
4233
import org.springframework.core.convert.ConversionService;
4334
import org.springframework.core.convert.converter.Converter;
4435
import org.springframework.data.annotation.Reference;
@@ -713,7 +704,7 @@ protected Object convertAssociation(@Nullable Object source, @Nullable MongoPers
713704

714705
List<Object> converted = new ArrayList<>(collection.size());
715706
for (Object o : collection) {
716-
converted.add(valueConverter.write(o, conversionContext));
707+
converted.add(typeSafeSingleWriteConversion(valueConverter, conversionContext, o));
717708
}
718709

719710
return converted;
@@ -730,7 +721,28 @@ protected Object convertAssociation(@Nullable Object source, @Nullable MongoPers
730721
});
731722
}
732723

733-
return value != null ? valueConverter.write(value, conversionContext) : value;
724+
return typeSafeSingleWriteConversion(valueConverter, conversionContext, value);
725+
}
726+
727+
private static @Nullable Object typeSafeSingleWriteConversion(PropertyValueConverter<Object, Object, ValueConversionContext<MongoPersistentProperty>> valueConverter, MongoConversionContext conversionContext, @Nullable Object o) {
728+
729+
Class<?>[] typeArguments = GenericTypeResolver.resolveTypeArguments(valueConverter.getClass(), PropertyValueConverter.class);
730+
731+
if (o == null) {
732+
return valueConverter.writeNull(conversionContext);
733+
}
734+
735+
// don't apply the converter if we can determine, that the types don't match.
736+
// this happens for regex comparisons.
737+
if (typeArguments != null && typeArguments.length > 0) {
738+
739+
Class<?> converterWriteArgumentType = typeArguments[0];
740+
if (!converterWriteArgumentType.isAssignableFrom(o.getClass())) {
741+
return o;
742+
}
743+
}
744+
745+
return valueConverter.write(o, conversionContext);
734746
}
735747

736748
@Nullable

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3315,6 +3315,21 @@ void enumConverter() {
33153315
assertThat(read.converterEnum).isEqualTo("spring");
33163316
}
33173317

3318+
@Test // GH-4346
3319+
void nullConverter() {
3320+
3321+
WithValueConverters wvc = new WithValueConverters();
3322+
wvc.nullConverter = null;
3323+
3324+
org.bson.Document target = new org.bson.Document();
3325+
converter.write(wvc, target);
3326+
3327+
assertThat(target).containsEntry("nullConverter", "W");
3328+
3329+
WithValueConverters read = converter.read(WithValueConverters.class, org.bson.Document.parse("{ nullConverter : null}"));
3330+
assertThat(read.nullConverter).isEqualTo("R");
3331+
}
3332+
33183333
@Test // GH-3596
33193334
void beanConverter() {
33203335

@@ -3357,12 +3372,12 @@ void beanConverter() {
33573372
new PropertyValueConverter<String, org.bson.Document, MongoConversionContext>() {
33583373

33593374
@Override
3360-
public @Nullable String read(org.bson.@Nullable Document nativeValue, MongoConversionContext context) {
3375+
public @Nullable String read(org.bson.Document nativeValue, MongoConversionContext context) {
33613376
return nativeValue.getString("bar");
33623377
}
33633378

33643379
@Override
3365-
public org.bson.@Nullable Document write(@Nullable String domainValue, MongoConversionContext context) {
3380+
public org.bson.Document write(String domainValue, MongoConversionContext context) {
33663381
return new org.bson.Document("bar", domainValue);
33673382
}
33683383
});
@@ -4615,6 +4630,8 @@ static class WithValueConverters {
46154630

46164631
@ValueConverter(Converter2.class) String converterEnum;
46174632

4633+
@ValueConverter(NullReplacingValueConverter.class) String nullConverter;
4634+
46184635
String viaRegisteredConverter;
46194636
}
46204637

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright 2022-2025 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.core.convert;
17+
18+
import org.jspecify.annotations.Nullable;
19+
20+
/**
21+
* @author Jens Schauder
22+
*/
23+
class NullReplacingValueConverter implements MongoValueConverter<String, String> {
24+
25+
@Override
26+
public @Nullable String read(String value, MongoConversionContext context) {
27+
return value;
28+
}
29+
30+
@Override
31+
public @Nullable String readNull(MongoConversionContext context) {
32+
return "R";
33+
}
34+
35+
@Override
36+
public @Nullable String write(String value, MongoConversionContext context) {
37+
return value;
38+
}
39+
40+
@Override
41+
public @Nullable String writeNull(MongoConversionContext context) {
42+
return "W";
43+
}
44+
}

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java

Lines changed: 55 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@
3838
import org.bson.types.ObjectId;
3939
import org.junit.jupiter.api.BeforeEach;
4040
import org.junit.jupiter.api.Test;
41-
4241
import org.springframework.core.convert.converter.Converter;
4342
import org.springframework.data.annotation.Id;
4443
import org.springframework.data.annotation.Transient;
@@ -58,17 +57,8 @@
5857
import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter;
5958
import org.springframework.data.mongodb.core.geo.GeoJsonPoint;
6059
import org.springframework.data.mongodb.core.geo.GeoJsonPolygon;
61-
import org.springframework.data.mongodb.core.mapping.DBRef;
62-
import org.springframework.data.mongodb.core.mapping.Document;
63-
import org.springframework.data.mongodb.core.mapping.DocumentReference;
64-
import org.springframework.data.mongodb.core.mapping.Field;
60+
import org.springframework.data.mongodb.core.mapping.*;
6561
import org.springframework.data.mongodb.core.mapping.FieldName.Type;
66-
import org.springframework.data.mongodb.core.mapping.FieldType;
67-
import org.springframework.data.mongodb.core.mapping.MongoId;
68-
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
69-
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
70-
import org.springframework.data.mongodb.core.mapping.TextScore;
71-
import org.springframework.data.mongodb.core.mapping.Unwrapped;
7262
import org.springframework.data.mongodb.core.query.BasicQuery;
7363
import org.springframework.data.mongodb.core.query.Criteria;
7464
import org.springframework.data.mongodb.core.query.Query;
@@ -98,7 +88,8 @@ public class QueryMapperUnitTests {
9888
@BeforeEach
9989
void beforeEach() {
10090

101-
MongoCustomConversions conversions = new MongoCustomConversions(new MongoConverterConfigurationAdapter().bigDecimal(BigDecimalRepresentation.DECIMAL128));
91+
MongoCustomConversions conversions = new MongoCustomConversions(
92+
new MongoConverterConfigurationAdapter().bigDecimal(BigDecimalRepresentation.DECIMAL128));
10293
this.context = new MongoMappingContext();
10394
this.context.setSimpleTypeHolder(conversions.getSimpleTypeHolder());
10495

@@ -133,7 +124,8 @@ void convertsStringIntoObjectId() {
133124
void handlesBigIntegerIdsCorrectly/*in legacy string format*/() {
134125

135126
MappingMongoConverter converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, context);
136-
converter.setCustomConversions(MongoCustomConversions.create(adapter -> adapter.bigDecimal(BigDecimalRepresentation.STRING)));
127+
converter.setCustomConversions(
128+
MongoCustomConversions.create(adapter -> adapter.bigDecimal(BigDecimalRepresentation.STRING)));
137129
converter.afterPropertiesSet();
138130

139131
QueryMapper mapper = new QueryMapper(converter);
@@ -1663,7 +1655,7 @@ void usageOfUntypedAggregationShouldRenderOperationsAsIs() {
16631655

16641656
Query query = query(expr(Expr.valueOf(ComparisonOperators.valueOf("field").greaterThan("budget"))));
16651657
org.bson.Document mappedObject = mapper.getMappedObject(query.getQueryObject(),
1666-
context.getPersistentEntity(Object.class));
1658+
context.getPersistentEntity(Object.class));
16671659
assertThat(mappedObject).isEqualTo("{ $expr : { $expr : { $gt : [ '$field', '$budget'] } } }");
16681660
}
16691661

@@ -1730,11 +1722,49 @@ void allOperatorShouldConvertIdCollection() {
17301722
Criteria criteria = new Criteria().andOperator(where("name").isNull().and("id").all(List.of(oid.toString())));
17311723

17321724
org.bson.Document mappedObject = mapper.getMappedObject(criteria.getCriteriaObject(),
1733-
context.getPersistentEntity(Customer.class));
1725+
context.getPersistentEntity(Customer.class));
17341726

17351727
assertThat(mappedObject).containsEntry("$and.[0]._id.$all", List.of(oid));
17361728
}
17371729

1730+
@Test // GH-4346
1731+
void propertyValueConverterOnlyGetsInvokedOnMatchingType() {
1732+
1733+
Criteria criteria = new Criteria().andOperator(where("text").regex("abc"));
1734+
org.bson.Document mappedObject = mapper.getMappedObject(criteria.getCriteriaObject(),
1735+
context.getPersistentEntity(WithPropertyValueConverter.class));
1736+
1737+
org.bson.Document parsedExpected = org.bson.Document.parse("{ '$and' : [{ 'text' : /abc/ }] }");
1738+
1739+
// We are comparing BsonDocument instances instead of Document instances, because Document.parse applies a Codec,
1740+
// which the ObjectMapper doesn't, yielding slightly different results.
1741+
// converting both to BsonDocuments applies the Codec to both.
1742+
assertThat(mappedObject.toBsonDocument()).isEqualTo(parsedExpected.toBsonDocument());
1743+
}
1744+
1745+
@Test // GH-4346
1746+
void nullConversionIsApplied(){
1747+
1748+
org.bson.Document mappedObject = mapper.getMappedObject(new org.bson.Document("text", null),
1749+
context.getPersistentEntity(WithNullReplacingPropertyValueConverter.class));
1750+
1751+
assertThat(mappedObject).isEqualTo(new org.bson.Document("text", "W"));
1752+
}
1753+
1754+
@Test // GH-4346
1755+
void nullConversionIsAppliedToLists(){
1756+
1757+
List<String> listOfStrings = new ArrayList<>();
1758+
listOfStrings.add("alpha");
1759+
listOfStrings.add(null);
1760+
listOfStrings.add("beta");
1761+
1762+
org.bson.Document mappedObject = mapper.getMappedObject(new org.bson.Document("text", listOfStrings),
1763+
context.getPersistentEntity(WithNullReplacingPropertyValueConverter.class));
1764+
1765+
assertThat(mappedObject).isEqualTo(new org.bson.Document("text", List.of("alpha", "W", "beta")));
1766+
}
1767+
17381768
class WithSimpleMap {
17391769
Map<String, String> simpleMap;
17401770
}
@@ -2021,6 +2051,16 @@ static class WithPropertyValueConverter {
20212051
@ValueConverter(ReversingValueConverter.class) String text;
20222052
}
20232053

2054+
static class WithNullReplacingPropertyValueConverter {
2055+
2056+
@ValueConverter(NullReplacingValueConverter.class) String text;
2057+
}
2058+
2059+
static class WithNullReplacingPropertyValueConverterOnList {
2060+
2061+
@ValueConverter(NullReplacingValueConverter.class) List<String> text;
2062+
}
2063+
20242064
@WritingConverter
20252065
public static class MyAddressToDocumentConverter implements Converter<MyAddress, org.bson.Document> {
20262066

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/ReversingValueConverter.java

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,22 +24,17 @@ class ReversingValueConverter implements MongoValueConverter<String, String> {
2424

2525
@Nullable
2626
@Override
27-
public String read(@Nullable String value, MongoConversionContext context) {
27+
public String read(String value, MongoConversionContext context) {
2828
return reverse(value);
2929
}
3030

3131
@Nullable
3232
@Override
33-
public String write(@Nullable String value, MongoConversionContext context) {
33+
public String write(String value, MongoConversionContext context) {
3434
return reverse(value);
3535
}
3636

3737
private String reverse(String source) {
38-
39-
if (source == null) {
40-
return null;
41-
}
42-
4338
return new StringBuilder(source).reverse().toString();
4439
}
4540
}

0 commit comments

Comments
 (0)