Skip to content

Commit 2cc5e42

Browse files
christophstroblmp911de
authored andcommitted
Delegate Bson conversion to MongoDB Codec.
Instead of reimplementing conversion we now try to delegate to the native MongoDB codec infrastructure using a custom writer that will only capture values without actually pushing values to an output stream. See #4432 Original pull request: #4439
1 parent a8f08ba commit 2cc5e42

File tree

2 files changed

+251
-31
lines changed

2 files changed

+251
-31
lines changed

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

+199-27
Original file line numberDiff line numberDiff line change
@@ -15,32 +15,33 @@
1515
*/
1616
package org.springframework.data.mongodb.util;
1717

18-
import java.time.Instant;
19-
import java.time.LocalDate;
20-
import java.time.LocalDateTime;
21-
import java.time.LocalTime;
22-
import java.time.ZoneOffset;
23-
import java.time.temporal.Temporal;
18+
import java.util.ArrayList;
2419
import java.util.Arrays;
2520
import java.util.Collection;
2621
import java.util.Collections;
2722
import java.util.Date;
23+
import java.util.List;
2824
import java.util.Map;
2925
import java.util.StringJoiner;
3026
import java.util.function.Function;
3127
import java.util.stream.StreamSupport;
3228

3329
import org.bson.*;
30+
import org.bson.codecs.Codec;
3431
import org.bson.codecs.DocumentCodec;
32+
import org.bson.codecs.EncoderContext;
33+
import org.bson.codecs.configuration.CodecConfigurationException;
3534
import org.bson.codecs.configuration.CodecRegistry;
3635
import org.bson.conversions.Bson;
3736
import org.bson.json.JsonParseException;
3837
import org.bson.types.Binary;
38+
import org.bson.types.Decimal128;
3939
import org.bson.types.ObjectId;
4040
import org.springframework.core.convert.converter.Converter;
4141
import org.springframework.data.mongodb.CodecRegistryProvider;
4242
import org.springframework.lang.Nullable;
4343
import org.springframework.util.Assert;
44+
import org.springframework.util.ClassUtils;
4445
import org.springframework.util.CollectionUtils;
4546
import org.springframework.util.ObjectUtils;
4647
import org.springframework.util.StringUtils;
@@ -109,7 +110,7 @@ public static Map<String, Object> asMap(@Nullable Bson bson, CodecRegistry codec
109110
return dbo.toMap();
110111
}
111112

112-
return new Document((Map) bson.toBsonDocument(Document.class, codecRegistry));
113+
return new Document(bson.toBsonDocument(Document.class, codecRegistry));
113114
}
114115

115116
/**
@@ -327,6 +328,20 @@ public static Object toJavaType(BsonValue value) {
327328
* @since 3.0
328329
*/
329330
public static BsonValue simpleToBsonValue(Object source) {
331+
return simpleToBsonValue(source, MongoClientSettings.getDefaultCodecRegistry());
332+
}
333+
334+
/**
335+
* Convert a given simple value (eg. {@link String}, {@link Long}) to its corresponding {@link BsonValue}.
336+
*
337+
* @param source must not be {@literal null}.
338+
* @param codecRegistry The {@link CodecRegistry} used as a fallback to convert types using native {@link Codec}. Must
339+
* not be {@literal null}.
340+
* @return the corresponding {@link BsonValue} representation.
341+
* @throws IllegalArgumentException if {@literal source} does not correspond to a {@link BsonValue} type.
342+
* @since 4.2
343+
*/
344+
public static BsonValue simpleToBsonValue(Object source, CodecRegistry codecRegistry) {
330345

331346
if (source instanceof BsonValue bsonValue) {
332347
return bsonValue;
@@ -364,31 +379,30 @@ public static BsonValue simpleToBsonValue(Object source) {
364379
return new BsonDouble(floatValue);
365380
}
366381

367-
if(source instanceof Binary binary) {
382+
if (source instanceof Binary binary) {
368383
return new BsonBinary(binary.getType(), binary.getData());
369384
}
370385

371-
if(source instanceof Temporal) {
372-
if (source instanceof Instant value) {
373-
return new BsonDateTime(value.toEpochMilli());
374-
}
375-
if (source instanceof LocalDateTime value) {
376-
return new BsonDateTime(value.toInstant(ZoneOffset.UTC).toEpochMilli());
377-
}
378-
if(source instanceof LocalDate value) {
379-
return new BsonDateTime(value.atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli());
380-
}
381-
if(source instanceof LocalTime value) {
382-
return new BsonDateTime(value.atDate(LocalDate.ofEpochDay(0L)).toInstant(ZoneOffset.UTC).toEpochMilli());
383-
}
384-
}
385-
386-
if(source instanceof Date date) {
386+
if (source instanceof Date date) {
387387
new BsonDateTime(date.getTime());
388388
}
389389

390-
throw new IllegalArgumentException(String.format("Unable to convert %s (%s) to BsonValue.", source,
391-
source != null ? source.getClass().getName() : "null"));
390+
try {
391+
392+
Object value = source;
393+
if (ClassUtils.isPrimitiveArray(source.getClass())) {
394+
value = CollectionUtils.arrayToList(source);
395+
}
396+
397+
Codec codec = codecRegistry.get(value.getClass());
398+
BsonCapturingWriter writer = new BsonCapturingWriter(value.getClass());
399+
codec.encode(writer, value,
400+
ObjectUtils.isArray(value) || value instanceof Collection<?> ? EncoderContext.builder().build() : null);
401+
return writer.getCapturedValue();
402+
} catch (CodecConfigurationException e) {
403+
throw new IllegalArgumentException(
404+
String.format("Unable to convert %s to BsonValue.", source != null ? source.getClass().getName() : "null"));
405+
}
392406
}
393407

394408
/**
@@ -694,7 +708,7 @@ private static String toJson(@Nullable Object value) {
694708

695709
if (value instanceof Collection<?> collection) {
696710
return toString(collection);
697-
} else if (value instanceof Map<?,?> map) {
711+
} else if (value instanceof Map<?, ?> map) {
698712
return toString(map);
699713
} else if (ObjectUtils.isArray(value)) {
700714
return toString(Arrays.asList(ObjectUtils.toObjectArray(value)));
@@ -733,4 +747,162 @@ private static <T> String iterableToDelimitedString(Iterable<T> source, String p
733747

734748
return joiner.toString();
735749
}
750+
751+
private static class BsonCapturingWriter extends AbstractBsonWriter {
752+
753+
List<BsonValue> values = new ArrayList<>(0);
754+
755+
public BsonCapturingWriter(Class<?> type) {
756+
super(new BsonWriterSettings());
757+
if (ClassUtils.isAssignable(Map.class, type)) {
758+
setContext(new Context(null, BsonContextType.DOCUMENT));
759+
} else if (ClassUtils.isAssignable(List.class, type) || type.isArray()) {
760+
setContext(new Context(null, BsonContextType.ARRAY));
761+
} else {
762+
setContext(new Context(null, BsonContextType.DOCUMENT));
763+
}
764+
}
765+
766+
BsonValue getCapturedValue() {
767+
768+
if (values.isEmpty()) {
769+
return null;
770+
}
771+
if (!getContext().getContextType().equals(BsonContextType.ARRAY)) {
772+
return values.get(0);
773+
}
774+
775+
return new BsonArray(values);
776+
}
777+
778+
@Override
779+
protected void doWriteStartDocument() {
780+
781+
}
782+
783+
@Override
784+
protected void doWriteEndDocument() {
785+
786+
}
787+
788+
@Override
789+
public void writeStartArray() {
790+
setState(State.VALUE);
791+
}
792+
793+
@Override
794+
public void writeEndArray() {
795+
setState(State.NAME);
796+
}
797+
798+
@Override
799+
protected void doWriteStartArray() {
800+
801+
}
802+
803+
@Override
804+
protected void doWriteEndArray() {
805+
806+
}
807+
808+
@Override
809+
protected void doWriteBinaryData(BsonBinary value) {
810+
values.add(value);
811+
}
812+
813+
@Override
814+
protected void doWriteBoolean(boolean value) {
815+
values.add(BsonBoolean.valueOf(value));
816+
}
817+
818+
@Override
819+
protected void doWriteDateTime(long value) {
820+
values.add(new BsonDateTime(value));
821+
}
822+
823+
@Override
824+
protected void doWriteDBPointer(BsonDbPointer value) {
825+
values.add(value);
826+
}
827+
828+
@Override
829+
protected void doWriteDouble(double value) {
830+
values.add(new BsonDouble(value));
831+
}
832+
833+
@Override
834+
protected void doWriteInt32(int value) {
835+
values.add(new BsonInt32(value));
836+
}
837+
838+
@Override
839+
protected void doWriteInt64(long value) {
840+
values.add(new BsonInt64(value));
841+
}
842+
843+
@Override
844+
protected void doWriteDecimal128(Decimal128 value) {
845+
values.add(new BsonDecimal128(value));
846+
}
847+
848+
@Override
849+
protected void doWriteJavaScript(String value) {
850+
values.add(new BsonJavaScript(value));
851+
}
852+
853+
@Override
854+
protected void doWriteJavaScriptWithScope(String value) {
855+
values.add(new BsonJavaScriptWithScope(value, null));
856+
}
857+
858+
@Override
859+
protected void doWriteMaxKey() {
860+
861+
}
862+
863+
@Override
864+
protected void doWriteMinKey() {
865+
866+
}
867+
868+
@Override
869+
protected void doWriteNull() {
870+
values.add(new BsonNull());
871+
}
872+
873+
@Override
874+
protected void doWriteObjectId(ObjectId value) {
875+
values.add(new BsonObjectId(value));
876+
}
877+
878+
@Override
879+
protected void doWriteRegularExpression(BsonRegularExpression value) {
880+
values.add(value);
881+
}
882+
883+
@Override
884+
protected void doWriteString(String value) {
885+
values.add(new BsonString(value));
886+
}
887+
888+
@Override
889+
protected void doWriteSymbol(String value) {
890+
values.add(new BsonSymbol(value));
891+
}
892+
893+
@Override
894+
protected void doWriteTimestamp(BsonTimestamp value) {
895+
values.add(value);
896+
}
897+
898+
@Override
899+
protected void doWriteUndefined() {
900+
values.add(new BsonUndefined());
901+
}
902+
903+
@Override
904+
public void flush() {
905+
values.clear();
906+
}
907+
}
736908
}

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

+52-4
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,19 @@
1717

1818
import static org.assertj.core.api.Assertions.*;
1919

20+
import java.time.Instant;
21+
import java.time.LocalDate;
22+
import java.time.LocalDateTime;
23+
import java.time.LocalTime;
24+
import java.time.temporal.Temporal;
2025
import java.util.ArrayList;
2126
import java.util.Collection;
2227
import java.util.Collections;
28+
import java.util.Date;
29+
import java.util.List;
30+
import java.util.stream.Stream;
2331

32+
import org.bson.BsonArray;
2433
import org.bson.BsonDouble;
2534
import org.bson.BsonInt32;
2635
import org.bson.BsonInt64;
@@ -29,7 +38,9 @@
2938
import org.bson.Document;
3039
import org.bson.types.ObjectId;
3140
import org.junit.jupiter.api.Test;
32-
41+
import org.junit.jupiter.params.ParameterizedTest;
42+
import org.junit.jupiter.params.provider.Arguments;
43+
import org.junit.jupiter.params.provider.MethodSource;
3344
import org.springframework.data.mongodb.util.BsonUtils;
3445

3546
import com.mongodb.BasicDBList;
@@ -105,17 +116,17 @@ void asCollectionDoesNotModifyCollection() {
105116
@Test // GH-3571
106117
void asCollectionConvertsArrayToCollection() {
107118

108-
Object source = new String[]{"one", "two"};
119+
Object source = new String[] { "one", "two" };
109120

110-
assertThat((Collection)BsonUtils.asCollection(source)).containsExactly("one", "two");
121+
assertThat((Collection) BsonUtils.asCollection(source)).containsExactly("one", "two");
111122
}
112123

113124
@Test // GH-3571
114125
void asCollectionConvertsWrapsNonIterable() {
115126

116127
Object source = 100L;
117128

118-
assertThat((Collection)BsonUtils.asCollection(source)).containsExactly(source);
129+
assertThat((Collection) BsonUtils.asCollection(source)).containsExactly(source);
119130
}
120131

121132
@Test // GH-3702
@@ -126,4 +137,41 @@ void supportsBsonShouldReportIfConversionSupported() {
126137
assertThat(BsonUtils.supportsBson(new BasicDBList())).isTrue();
127138
assertThat(BsonUtils.supportsBson(Collections.emptyMap())).isTrue();
128139
}
140+
141+
@ParameterizedTest // GH-4432
142+
@MethodSource("javaTimeInstances")
143+
void convertsJavaTimeTypesToBsonDateTime(Temporal source) {
144+
145+
assertThat(BsonUtils.simpleToBsonValue(source))
146+
.isEqualTo(new Document("value", source).toBsonDocument().get("value"));
147+
}
148+
149+
@ParameterizedTest // GH-4432
150+
@MethodSource("collectionLikeInstances")
151+
void convertsCollectionLikeToBsonArray(Object source) {
152+
153+
assertThat(BsonUtils.simpleToBsonValue(source))
154+
.isEqualTo(new Document("value", source).toBsonDocument().get("value"));
155+
}
156+
157+
@Test // GH-4432
158+
void convertsPrimitiveArrayToBsonArray() {
159+
160+
assertThat(BsonUtils.simpleToBsonValue(new int[] { 1, 2, 3 }))
161+
.isEqualTo(new BsonArray(List.of(new BsonInt32(1), new BsonInt32(2), new BsonInt32(3))));
162+
}
163+
164+
static Stream<Arguments> javaTimeInstances() {
165+
166+
return Stream.of(Arguments.of(Instant.now()), Arguments.of(LocalDate.now()), Arguments.of(LocalDateTime.now()),
167+
Arguments.of(LocalTime.now()));
168+
}
169+
170+
static Stream<Arguments> collectionLikeInstances() {
171+
172+
return Stream.of(Arguments.of(new String[] { "1", "2", "3" }), Arguments.of(List.of("1", "2", "3")),
173+
Arguments.of(new Integer[] { 1, 2, 3 }), Arguments.of(List.of(1, 2, 3)),
174+
Arguments.of(new Date[] { new Date() }), Arguments.of(List.of(new Date())),
175+
Arguments.of(new LocalDate[] { LocalDate.now() }), Arguments.of(List.of(LocalDate.now())));
176+
}
129177
}

0 commit comments

Comments
 (0)