Skip to content

Commit 8b80789

Browse files
Require explicit BigInteger/BigDecimal conversion settings.
See: #5037 Original Pull Request: #5051
1 parent 49904dc commit 8b80789

File tree

10 files changed

+339
-77
lines changed

10 files changed

+339
-77
lines changed

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,7 @@ static Collection<Object> getBigNumberStringConverters() {
132132
List<Object> converters = new ArrayList<>(4);
133133

134134
converters.add(BigDecimalToStringConverter.INSTANCE);
135-
converters.add(StringToBigDecimalConverter.INSTANCE);
136135
converters.add(BigIntegerToStringConverter.INSTANCE);
137-
converters.add(StringToBigIntegerConverter.INSTANCE);
138136

139137
return converters;
140138
}
@@ -169,6 +167,9 @@ static Collection<Object> getConvertersToRegister() {
169167
converters.add(ListToVectorConverter.INSTANCE);
170168
converters.add(BinaryVectorToMongoVectorConverter.INSTANCE);
171169

170+
converters.add(StringToBigDecimalConverter.INSTANCE);
171+
converters.add(StringToBigIntegerConverter.INSTANCE);
172+
172173
converters.add(reading(BsonUndefined.class, Object.class, it -> null));
173174
converters.add(reading(String.class, URI.class, URI::create).andWriting(URI::toString));
174175

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

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,9 @@
3131
import java.util.Set;
3232
import java.util.function.Consumer;
3333

34+
import org.apache.commons.logging.Log;
35+
import org.apache.commons.logging.LogFactory;
3436
import org.jspecify.annotations.Nullable;
35-
3637
import org.springframework.core.convert.TypeDescriptor;
3738
import org.springframework.core.convert.converter.Converter;
3839
import org.springframework.core.convert.converter.ConverterFactory;
@@ -63,6 +64,7 @@
6364
*/
6465
public class MongoCustomConversions extends org.springframework.data.convert.CustomConversions {
6566

67+
private static final Log LOGGER = LogFactory.getLog(MongoCustomConversions.class);
6668
private static final List<Object> STORE_CONVERTERS;
6769

6870
static {
@@ -153,7 +155,7 @@ public static class MongoConverterConfigurationAdapter {
153155
LocalDateTime.class);
154156

155157
private boolean useNativeDriverJavaTimeCodecs = false;
156-
private BigDecimalRepresentation bigDecimals = BigDecimalRepresentation.DECIMAL128;
158+
private BigDecimalRepresentation @Nullable [] bigDecimals;
157159
private final List<Object> customConverters = new ArrayList<>();
158160

159161
private final PropertyValueConversions internalValueConversion = PropertyValueConversions.simple(it -> {});
@@ -310,14 +312,14 @@ public MongoConverterConfigurationAdapter useSpringDataJavaTimeCodecs() {
310312
* Configures the representation to for {@link java.math.BigDecimal} and {@link java.math.BigInteger} values in
311313
* MongoDB. Defaults to {@link BigDecimalRepresentation#DECIMAL128}.
312314
*
313-
* @param representation the representation to use.
315+
* @param representations ordered list of representations to use (first one is default)
314316
* @return this.
315317
* @since 4.5
316318
*/
317-
public MongoConverterConfigurationAdapter bigDecimal(BigDecimalRepresentation representation) {
319+
public MongoConverterConfigurationAdapter bigDecimal(BigDecimalRepresentation... representations) {
318320

319-
Assert.notNull(representation, "BigDecimalDataType must not be null");
320-
this.bigDecimals = representation;
321+
Assert.notEmpty(representations, "BigDecimalDataType must not be null");
322+
this.bigDecimals = representations;
321323
return this;
322324
}
323325

@@ -372,12 +374,16 @@ ConverterConfiguration createConverterConfiguration() {
372374

373375
List<Object> storeConverters = new ArrayList<>(STORE_CONVERTERS.size() + 10);
374376

375-
if (bigDecimals == BigDecimalRepresentation.STRING) {
376-
storeConverters.addAll(MongoConverters.getBigNumberStringConverters());
377-
}
378-
379-
if (bigDecimals == BigDecimalRepresentation.DECIMAL128) {
380-
storeConverters.addAll(MongoConverters.getBigNumberDecimal128Converters());
377+
if (bigDecimals != null) {
378+
for (BigDecimalRepresentation representation : bigDecimals) {
379+
switch (representation) {
380+
case STRING -> storeConverters.addAll(MongoConverters.getBigNumberStringConverters());
381+
case DECIMAL128 -> storeConverters.addAll(MongoConverters.getBigNumberDecimal128Converters());
382+
}
383+
}
384+
} else if (LOGGER.isInfoEnabled()) {
385+
LOGGER.info(
386+
"No BigDecimal/BigInteger representation set. Choose [DECIMAL128] and/or [String] to store values in desired format.");
381387
}
382388

383389
if (useNativeDriverJavaTimeCodecs) {
@@ -395,9 +401,9 @@ ConverterConfiguration createConverterConfiguration() {
395401

396402
// Avoid default registrations
397403

398-
return !JAVA_DRIVER_TIME_SIMPLE_TYPES.contains(convertiblePair.getSourceType())
399-
|| !Date.class.isAssignableFrom(convertiblePair.getTargetType());
400-
}, this.propertyValueConversions);
404+
return !JAVA_DRIVER_TIME_SIMPLE_TYPES.contains(convertiblePair.getSourceType())
405+
|| !Date.class.isAssignableFrom(convertiblePair.getTargetType());
406+
}, this.propertyValueConversions);
401407

402408
}
403409

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
import org.springframework.beans.factory.annotation.Autowired;
3131
import org.springframework.context.annotation.Configuration;
3232
import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration;
33+
import org.springframework.data.mongodb.core.convert.MongoCustomConversions.BigDecimalRepresentation;
34+
import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter;
3335
import org.springframework.data.mongodb.core.query.Collation;
3436
import org.springframework.data.mongodb.core.query.Collation.Alternate;
3537
import org.springframework.data.mongodb.core.query.Collation.ComparisonLevel;

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

Lines changed: 94 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,14 @@
1515
*/
1616
package org.springframework.data.mongodb.core;
1717

18-
import static org.assertj.core.api.Assertions.*;
19-
import static org.springframework.data.mongodb.core.query.Criteria.*;
20-
import static org.springframework.data.mongodb.core.query.Query.*;
21-
import static org.springframework.data.mongodb.core.query.Update.*;
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
20+
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
21+
import static org.assertj.core.api.Assertions.fail;
22+
import static org.springframework.data.mongodb.core.query.Criteria.expr;
23+
import static org.springframework.data.mongodb.core.query.Criteria.where;
24+
import static org.springframework.data.mongodb.core.query.Query.query;
25+
import static org.springframework.data.mongodb.core.query.Update.update;
2226

2327
import java.lang.reflect.InvocationTargetException;
2428
import java.math.BigDecimal;
@@ -28,17 +32,29 @@
2832
import java.time.LocalDateTime;
2933
import java.time.ZoneId;
3034
import java.time.temporal.ChronoUnit;
31-
import java.util.*;
35+
import java.util.ArrayList;
36+
import java.util.Arrays;
37+
import java.util.Calendar;
38+
import java.util.Collections;
39+
import java.util.Date;
40+
import java.util.HashMap;
41+
import java.util.Iterator;
42+
import java.util.LinkedHashMap;
43+
import java.util.List;
44+
import java.util.Locale;
45+
import java.util.Map;
46+
import java.util.Objects;
47+
import java.util.UUID;
3248
import java.util.concurrent.atomic.AtomicReference;
3349
import java.util.stream.Collectors;
3450
import java.util.stream.IntStream;
3551
import java.util.stream.Stream;
3652

53+
import org.bson.codecs.configuration.CodecConfigurationException;
3754
import org.bson.types.ObjectId;
3855
import org.jspecify.annotations.Nullable;
3956
import org.junit.jupiter.api.AfterEach;
4057
import org.junit.jupiter.api.Test;
41-
4258
import org.springframework.context.ConfigurableApplicationContext;
4359
import org.springframework.context.support.GenericApplicationContext;
4460
import org.springframework.core.convert.ConversionFailedException;
@@ -69,6 +85,7 @@
6985
import org.springframework.data.mongodb.core.convert.LazyLoadingProxy;
7086
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
7187
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
88+
import org.springframework.data.mongodb.core.convert.MongoCustomConversions.BigDecimalRepresentation;
7289
import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter;
7390
import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver;
7491
import org.springframework.data.mongodb.core.geo.GeoJsonPoint;
@@ -144,6 +161,12 @@ public class MongoTemplateTests {
144161
it.defaultDb(DB_NAME);
145162
});
146163

164+
cfg.configureConversion(it -> {
165+
it.customConverters(adapter -> {
166+
adapter.bigDecimal(BigDecimalRepresentation.DECIMAL128);
167+
});
168+
});
169+
147170
cfg.configureMappingContext(it -> {
148171
it.autocreateIndex(false);
149172
it.initialEntitySet(AuditablePerson.class);
@@ -170,7 +193,10 @@ public class MongoTemplateTests {
170193
});
171194

172195
cfg.configureConversion(it -> {
173-
it.customConverters(DateToDateTimeConverter.INSTANCE, DateTimeToDateConverter.INSTANCE);
196+
it.customConverters(adapter -> {
197+
adapter.registerConverters(List.of(DateToDateTimeConverter.INSTANCE, DateTimeToDateConverter.INSTANCE))
198+
.bigDecimal(BigDecimalRepresentation.DECIMAL128);
199+
});
174200
});
175201

176202
cfg.configureMappingContext(it -> {
@@ -732,7 +758,7 @@ public void testDistinct() {
732758
.containsExactlyInAnyOrder(person1.getName(), person2.getName());
733759
assertThat(template.findDistinct(new BasicQuery("{'address.state' : 'PA'}"), "name",
734760
template.getCollectionName(MyPerson.class), MyPerson.class, String.class))
735-
.containsExactlyInAnyOrder(person1.getName(), person2.getName());
761+
.containsExactlyInAnyOrder(person1.getName(), person2.getName());
736762
}
737763

738764
@Test // DATAMONGO-1761
@@ -876,7 +902,7 @@ public void testUsingAnInQueryWithLongId() throws Exception {
876902
}
877903

878904
@Test // DATAMONGO-602, GH-4920
879-
public void testUsingAnInQueryWithBigIntegerId() throws Exception {
905+
public void testUsingAnInQueryWithBigIntegerId() {
880906

881907
template.remove(new Query(), PersonWithIdPropertyOfTypeBigInteger.class);
882908

@@ -887,6 +913,34 @@ public void testUsingAnInQueryWithBigIntegerId() throws Exception {
887913
assertThatExceptionOfType(ConversionFailedException.class).isThrownBy(() -> template.insert(p1));
888914
}
889915

916+
@Test // GH-5037
917+
public void errorsIfNoBigNumberFormatDefined() {
918+
919+
template = new MongoTestTemplate(cfg -> {
920+
921+
cfg.configureDatabaseFactory(it -> {
922+
923+
it.client(client);
924+
it.defaultDb(DB_NAME);
925+
});
926+
927+
cfg.configureConversion(it -> {
928+
it.customConverters(adapter -> {
929+
// no numeric conversion
930+
});
931+
});
932+
933+
});
934+
935+
template.remove(new Query(), PersonWithIdPropertyOfTypeBigInteger.class);
936+
937+
PersonWithIdPropertyOfTypeBigInteger p1 = new PersonWithIdPropertyOfTypeBigInteger();
938+
p1.setFirstName("Sven");
939+
p1.setAge(11);
940+
p1.setId(new BigInteger("2666666666666666665"));
941+
assertThatExceptionOfType(CodecConfigurationException.class).isThrownBy(() -> template.insert(p1));
942+
}
943+
890944
@Test
891945
public void testUsingAnInQueryWithPrimitiveIntId() throws Exception {
892946

@@ -2561,16 +2615,15 @@ public void shouldReadNestedProjection() {
25612615
walter.address = new Address("spring", "data");
25622616
template.save(walter);
25632617

2564-
PersonPWA result = template.query(MyPerson.class)
2565-
.as(PersonPWA.class)
2566-
.matching(where("id").is(walter.id))
2618+
PersonPWA result = template.query(MyPerson.class).as(PersonPWA.class).matching(where("id").is(walter.id))
25672619
.firstValue();
25682620

25692621
assertThat(result.getAddress().getCity()).isEqualTo("data");
25702622
}
25712623

25722624
interface PersonPWA {
25732625
String getName();
2626+
25742627
AdressProjection getAddress();
25752628
}
25762629

@@ -2823,7 +2876,7 @@ public void testFindAllAndRemoveFullyReturnsAndRemovesDocuments() {
28232876

28242877
assertThat(template.getDb().getCollection("sample").countDocuments(
28252878
new org.bson.Document("field", new org.bson.Document("$in", Arrays.asList("spring", "mongodb")))))
2826-
.isEqualTo(0L);
2879+
.isEqualTo(0L);
28272880
assertThat(template.getDb().getCollection("sample").countDocuments(new org.bson.Document("field", "data")))
28282881
.isEqualTo(1L);
28292882
}
@@ -3935,7 +3988,8 @@ void saveEntityWithDotInFieldName() {
39353988

39363989
template.save(source);
39373990

3938-
org.bson.Document raw = template.execute(WithFieldNameContainingDots.class, collection -> collection.find(new org.bson.Document("_id", source.id)).first());
3991+
org.bson.Document raw = template.execute(WithFieldNameContainingDots.class,
3992+
collection -> collection.find(new org.bson.Document("_id", source.id)).first());
39393993
assertThat(raw).containsEntry("field.name.with.dots", "v1");
39403994
}
39413995

@@ -3954,13 +4008,17 @@ void queryEntityWithDotInFieldNameUsingExpr() {
39544008
template.save(source);
39554009
template.save(source2);
39564010

3957-
WithFieldNameContainingDots loaded = template.query(WithFieldNameContainingDots.class) // with property -> fieldname mapping
3958-
.matching(expr(ComparisonOperators.valueOf(ObjectOperators.getValueOf("value")).equalToValue("v1"))).firstValue();
4011+
WithFieldNameContainingDots loaded = template.query(WithFieldNameContainingDots.class) // with property -> fieldname
4012+
// mapping
4013+
.matching(expr(ComparisonOperators.valueOf(ObjectOperators.getValueOf("value")).equalToValue("v1")))
4014+
.firstValue();
39594015

39604016
assertThat(loaded).isEqualTo(source);
39614017

39624018
loaded = template.query(WithFieldNameContainingDots.class) // using raw fieldname
3963-
.matching(expr(ComparisonOperators.valueOf(ObjectOperators.getValueOf("field.name.with.dots")).equalToValue("v1"))).firstValue();
4019+
.matching(
4020+
expr(ComparisonOperators.valueOf(ObjectOperators.getValueOf("field.name.with.dots")).equalToValue("v1")))
4021+
.firstValue();
39644022

39654023
assertThat(loaded).isEqualTo(source);
39664024
}
@@ -3975,20 +4033,20 @@ void updateEntityWithDotInFieldNameUsingAggregations() {
39754033

39764034
template.save(source);
39774035

3978-
template.update(WithFieldNameContainingDots.class)
3979-
.matching(where("id").is(source.id))
3980-
.apply(AggregationUpdate.newUpdate(ReplaceWithOperation.replaceWithValue(ObjectOperators.setValueTo("value", "changed"))))
3981-
.first();
4036+
template.update(WithFieldNameContainingDots.class).matching(where("id").is(source.id)).apply(AggregationUpdate
4037+
.newUpdate(ReplaceWithOperation.replaceWithValue(ObjectOperators.setValueTo("value", "changed")))).first();
39824038

3983-
org.bson.Document raw = template.execute(WithFieldNameContainingDots.class, collection -> collection.find(new org.bson.Document("_id", source.id)).first());
4039+
org.bson.Document raw = template.execute(WithFieldNameContainingDots.class,
4040+
collection -> collection.find(new org.bson.Document("_id", source.id)).first());
39844041
assertThat(raw).containsEntry("field.name.with.dots", "changed");
39854042

3986-
template.update(WithFieldNameContainingDots.class)
3987-
.matching(where("id").is(source.id))
3988-
.apply(AggregationUpdate.newUpdate(ReplaceWithOperation.replaceWithValue(ObjectOperators.setValueTo("field.name.with.dots", "changed-again"))))
4043+
template.update(WithFieldNameContainingDots.class).matching(where("id").is(source.id))
4044+
.apply(AggregationUpdate.newUpdate(
4045+
ReplaceWithOperation.replaceWithValue(ObjectOperators.setValueTo("field.name.with.dots", "changed-again"))))
39894046
.first();
39904047

3991-
raw = template.execute(WithFieldNameContainingDots.class, collection -> collection.find(new org.bson.Document("_id", source.id)).first());
4048+
raw = template.execute(WithFieldNameContainingDots.class,
4049+
collection -> collection.find(new org.bson.Document("_id", source.id)).first());
39924050
assertThat(raw).containsEntry("field.name.with.dots", "changed-again");
39934051
}
39944052

@@ -4013,9 +4071,8 @@ void savesMapWithDotInKey() {
40134071
org.bson.Document raw = template.execute(WithFieldNameContainingDots.class,
40144072
collection -> collection.find(new org.bson.Document("_id", source.id)).first());
40154073

4016-
assertThat(raw.get("mapValue", org.bson.Document.class))
4017-
.containsEntry("k1", "v1")
4018-
.containsEntry("map.key.with.dot", "v2");
4074+
assertThat(raw.get("mapValue", org.bson.Document.class)).containsEntry("k1", "v1").containsEntry("map.key.with.dot",
4075+
"v2");
40194076
}
40204077

40214078
@Test // GH-4464
@@ -4031,16 +4088,13 @@ void readsMapWithDotInKey() {
40314088
MongoTemplate template = new MongoTemplate(new SimpleMongoClientDatabaseFactory(client, DB_NAME), converter);
40324089

40334090
Map<String, String> sourceMap = Map.of("k1", "v1", "sourceMap.key.with.dot", "v2");
4034-
template.execute(WithFieldNameContainingDots.class,
4035-
collection -> {
4036-
collection.insertOne(new org.bson.Document("_id", "id-1").append("mapValue", sourceMap));
4037-
return null;
4038-
}
4039-
);
4091+
template.execute(WithFieldNameContainingDots.class, collection -> {
4092+
collection.insertOne(new org.bson.Document("_id", "id-1").append("mapValue", sourceMap));
4093+
return null;
4094+
});
40404095

40414096
WithFieldNameContainingDots loaded = template.query(WithFieldNameContainingDots.class)
4042-
.matching(where("id").is("id-1"))
4043-
.firstValue();
4097+
.matching(where("id").is("id-1")).firstValue();
40444098

40454099
assertThat(loaded.mapValue).isEqualTo(sourceMap);
40464100
}
@@ -5015,8 +5069,7 @@ static class WithFieldNameContainingDots {
50155069

50165070
String id;
50175071

5018-
@Field(value = "field.name.with.dots", nameType = Type.KEY)
5019-
String value;
5072+
@Field(value = "field.name.with.dots", nameType = Type.KEY) String value;
50205073

50215074
Map<String, String> mapValue;
50225075

@@ -5034,7 +5087,8 @@ public boolean equals(Object o) {
50345087
return false;
50355088
}
50365089
WithFieldNameContainingDots withFieldNameContainingDots = (WithFieldNameContainingDots) o;
5037-
return Objects.equals(id, withFieldNameContainingDots.id) && Objects.equals(value, withFieldNameContainingDots.value)
5090+
return Objects.equals(id, withFieldNameContainingDots.id)
5091+
&& Objects.equals(value, withFieldNameContainingDots.value)
50385092
&& Objects.equals(mapValue, withFieldNameContainingDots.mapValue);
50395093
}
50405094

0 commit comments

Comments
 (0)