Skip to content

Commit ee6276f

Browse files
DATAMONGO-2400 - Consider java.time.Instant a store supported native type and add configuration options for other java.time types.
We now use the MongoDB Java driver InstantCodec instead of a dedicated converter. Other java.time types like LocalDate use a different zone offset when writing values which can lead to unexpected behavior. Therefore we added configuration options to MongoCustomConversions that allow to tell the conversion sub system which approach to use when writing those kind of types.
1 parent 6278d00 commit ee6276f

File tree

6 files changed

+331
-26
lines changed

6 files changed

+331
-26
lines changed

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoConfigurationSupport.java

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.springframework.data.mapping.model.FieldNamingStrategy;
3232
import org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy;
3333
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
34+
import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter;
3435
import org.springframework.data.mongodb.core.mapping.Document;
3536
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
3637
import org.springframework.util.ClassUtils;
@@ -88,14 +89,35 @@ public MongoMappingContext mongoMappingContext() throws ClassNotFoundException {
8889

8990
/**
9091
* Register custom {@link Converter}s in a {@link CustomConversions} object if required. These
91-
* {@link CustomConversions} will be registered with the {@link #mappingMongoConverter()} and
92-
* {@link #mongoMappingContext()}. Returns an empty {@link MongoCustomConversions} instance by default.
92+
* {@link CustomConversions} will be registered with the
93+
* {@link org.springframework.data.mongodb.core.convert.MappingMongoConverter} and {@link #mongoMappingContext()}.
94+
* Returns an empty {@link MongoCustomConversions} instance by default. <br />
95+
* <strong>NOTE:</strong> Use {@link #customConversionsConfiguration(MongoConverterConfigurationAdapter)} to configure
96+
* MongoDB native simple types and register custom {@link Converter converters}.
9397
*
9498
* @return must not be {@literal null}.
9599
*/
96100
@Bean
97101
public CustomConversions customConversions() {
98-
return new MongoCustomConversions(Collections.emptyList());
102+
return new MongoCustomConversions(this::customConversionsConfiguration);
103+
}
104+
105+
/**
106+
* Configuration hook for {@link MongoCustomConversions} creation.
107+
*
108+
* @param converterConfigurationAdapter never {@literal null}.
109+
* @since 2.3
110+
*/
111+
protected void customConversionsConfiguration(MongoConverterConfigurationAdapter converterConfigurationAdapter) {
112+
113+
/*
114+
* In case you want to use the MongoDB Java Driver native Codecs for java.time types instead of the converters SpringData
115+
* ships with, then you may want to call the following here.
116+
*
117+
* converterConfigurationAdapter.useNativeDriverJavaTimeCodecs()
118+
*
119+
* But please, be careful! LocalDate, LocalTime and LocalDateTime will be stored with different values by doing so.
120+
*/
99121
}
100122

101123
/**

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

Lines changed: 194 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,26 +15,42 @@
1515
*/
1616
package org.springframework.data.mongodb.core.convert;
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.ZoneId;
23+
import java.time.ZoneOffset;
1824
import java.util.ArrayList;
1925
import java.util.Arrays;
26+
import java.util.Collection;
2027
import java.util.Collections;
28+
import java.util.Date;
2129
import java.util.HashSet;
2230
import java.util.List;
2331
import java.util.Locale;
2432
import java.util.Set;
33+
import java.util.function.Consumer;
34+
import java.util.stream.Collectors;
2535

2636
import org.springframework.core.convert.TypeDescriptor;
37+
import org.springframework.core.convert.converter.Converter;
38+
import org.springframework.core.convert.converter.ConverterFactory;
2739
import org.springframework.core.convert.converter.GenericConverter;
40+
import org.springframework.core.convert.converter.GenericConverter.ConvertiblePair;
2841
import org.springframework.data.convert.JodaTimeConverters;
2942
import org.springframework.data.convert.WritingConverter;
43+
import org.springframework.data.mapping.model.SimpleTypeHolder;
3044
import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes;
3145
import org.springframework.lang.Nullable;
46+
import org.springframework.util.Assert;
3247

3348
/**
3449
* Value object to capture custom conversion. {@link MongoCustomConversions} also act as factory for
3550
* {@link org.springframework.data.mapping.model.SimpleTypeHolder}
3651
*
3752
* @author Mark Paluch
53+
* @author Christoph Strobl
3854
* @since 2.0
3955
* @see org.springframework.data.convert.CustomConversions
4056
* @see org.springframework.data.mapping.model.SimpleTypeHolder
@@ -71,7 +87,30 @@ public class MongoCustomConversions extends org.springframework.data.convert.Cus
7187
* @param converters must not be {@literal null}.
7288
*/
7389
public MongoCustomConversions(List<?> converters) {
74-
super(STORE_CONVERSIONS, converters);
90+
91+
this(converterConfigurationAdapter -> {
92+
93+
converterConfigurationAdapter.useSpringDataJavaTimeCodecs();
94+
converterConfigurationAdapter.registerConverters(converters);
95+
});
96+
}
97+
98+
/**
99+
* Functional style {@link org.springframework.data.convert.CustomConversions} creation giving users a convenient way
100+
* of configuring store specific capabilities by providing deferred hooks to what will be configured when creating the
101+
* {@link org.springframework.data.convert.CustomConversions#CustomConversions(ConverterConfiguration) instance}.
102+
*
103+
* @param conversionConfiguration must not be {@literal null}.
104+
* @since 2.3
105+
*/
106+
public MongoCustomConversions(Consumer<MongoConverterConfigurationAdapter> conversionConfiguration) {
107+
108+
super(() -> {
109+
110+
MongoConverterConfigurationAdapter adapter = new MongoConverterConfigurationAdapter();
111+
conversionConfiguration.accept(adapter);
112+
return adapter.createConverterConfiguration();
113+
});
75114
}
76115

77116
@WritingConverter
@@ -99,4 +138,158 @@ public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDe
99138
return source != null ? source.toString() : null;
100139
}
101140
}
141+
142+
/**
143+
* {@link MongoConverterConfigurationAdapter} encapsulates creation of
144+
* {@link org.springframework.data.convert.CustomConversions.ConverterConfiguration} with MongoDB specifics.
145+
*
146+
* @author Christoph Strobl
147+
* @since 2.3
148+
*/
149+
public static class MongoConverterConfigurationAdapter {
150+
151+
/**
152+
* List of {@literal java.time} types having different representation when rendered via the native
153+
* {@link org.bson.codecs.Codec} than the Spring Data {@link Converter}.
154+
*/
155+
private static final List<Class<?>> JAVA_DRIVER_TIME_SIMPLE_TYPES = Arrays.asList(LocalDate.class, LocalTime.class,
156+
LocalDateTime.class);
157+
158+
private boolean useNativeDriverJavaTimeCodecs = false;
159+
private List<Object> customConverters = new ArrayList<>();
160+
161+
/**
162+
* Set wether or not to use the native MongoDB Java Driver {@link org.bson.codecs.Codec codes} for
163+
* {@link org.bson.codecs.jsr310.LocalDateCodec LocalDate}, {@link org.bson.codecs.jsr310.LocalTimeCodec LocalTime}
164+
* and {@link org.bson.codecs.jsr310.LocalDateTimeCodec LocalDateTime} using a {@link ZoneOffset#UTC}.
165+
*
166+
* @param useNativeDriverJavaTimeCodecs
167+
* @return this.
168+
*/
169+
public MongoConverterConfigurationAdapter useNativeDriverJavaTimeCodecs(boolean useNativeDriverJavaTimeCodecs) {
170+
171+
this.useNativeDriverJavaTimeCodecs = useNativeDriverJavaTimeCodecs;
172+
return this;
173+
}
174+
175+
/**
176+
* Use the native MongoDB Java Driver {@link org.bson.codecs.Codec codes} for
177+
* {@link org.bson.codecs.jsr310.LocalDateCodec LocalDate}, {@link org.bson.codecs.jsr310.LocalTimeCodec LocalTime}
178+
* and {@link org.bson.codecs.jsr310.LocalDateTimeCodec LocalDateTime} using a {@link ZoneOffset#UTC}.
179+
*
180+
* @return this.
181+
* @see #useNativeDriverJavaTimeCodecs(boolean)
182+
*/
183+
public MongoConverterConfigurationAdapter useNativeDriverJavaTimeCodecs() {
184+
return useNativeDriverJavaTimeCodecs(true);
185+
}
186+
187+
/**
188+
* Use SpringData {@link Converter Jsr310 converters} for
189+
* {@link org.springframework.data.convert.Jsr310Converters.LocalDateToDateConverter LocalDate},
190+
* {@link org.springframework.data.convert.Jsr310Converters.LocalTimeToDateConverter LocalTime} and
191+
* {@link org.springframework.data.convert.Jsr310Converters.LocalDateTimeToDateConverter LocalDateTime} using the
192+
* {@link ZoneId#systemDefault()}.
193+
*
194+
* @return this.
195+
* @see #useNativeDriverJavaTimeCodecs(boolean)
196+
*/
197+
public MongoConverterConfigurationAdapter useSpringDataJavaTimeCodecs() {
198+
return useNativeDriverJavaTimeCodecs(false);
199+
}
200+
201+
/**
202+
* Add a custom {@link Converter} implementation.
203+
*
204+
* @param converter must not be {@literal null}.
205+
* @return this.
206+
*/
207+
public MongoConverterConfigurationAdapter registerConverter(Converter<?, ?> converter) {
208+
209+
Assert.notNull(converter, "Converter must not be null!");
210+
customConverters.add(converter);
211+
return this;
212+
}
213+
214+
/**
215+
* Add a custom {@link ConverterFactory} implementation.
216+
*
217+
* @param converterFactory must not be {@literal null}.
218+
* @return this.
219+
*/
220+
public MongoConverterConfigurationAdapter registerConverterFactory(ConverterFactory<?, ?> converterFactory) {
221+
222+
Assert.notNull(converterFactory, "ConverterFactory must not be null!");
223+
customConverters.add(converterFactory);
224+
return this;
225+
}
226+
227+
/**
228+
* Add {@link Converter converters}, {@link ConverterFactory factories}, ...
229+
*
230+
* @param converters must not be {@literal null} nor contain {@literal null} values.
231+
* @return this.
232+
*/
233+
public MongoConverterConfigurationAdapter registerConverters(Collection<?> converters) {
234+
235+
Assert.noNullElements(converters, "Converters must not be null nor contain null values!");
236+
customConverters.addAll(converters);
237+
return this;
238+
}
239+
240+
ConverterConfiguration createConverterConfiguration() {
241+
242+
if (!useNativeDriverJavaTimeCodecs) {
243+
return new ConverterConfiguration(STORE_CONVERSIONS, this.customConverters);
244+
}
245+
246+
/*
247+
* We need to have those converters using UTC as the default ones would go on with the systemDefault.
248+
*/
249+
List<Object> converters = new ArrayList<>();
250+
converters.add(DateToUtcLocalDateConverter.INSTANCE);
251+
converters.add(DateToUtcLocalTimeConverter.INSTANCE);
252+
converters.add(DateToUtcLocalDateTimeConverter.INSTANCE);
253+
converters.addAll(STORE_CONVERTERS);
254+
255+
/*
256+
* Right, good catch! We also need to make sure to remove default writing converters for java.time types.
257+
*/
258+
List<ConvertiblePair> skipConverterRegistrationFor = JAVA_DRIVER_TIME_SIMPLE_TYPES.stream() //
259+
.map(it -> new ConvertiblePair(it, Date.class)) //
260+
.collect(Collectors.toList()); //
261+
262+
StoreConversions storeConversions = StoreConversions
263+
.of(new SimpleTypeHolder(new HashSet<>(JAVA_DRIVER_TIME_SIMPLE_TYPES), MongoSimpleTypes.HOLDER), converters);
264+
265+
return new ConverterConfiguration(storeConversions, this.customConverters, skipConverterRegistrationFor);
266+
}
267+
268+
private enum DateToUtcLocalDateTimeConverter implements Converter<Date, LocalDateTime> {
269+
INSTANCE;
270+
271+
@Override
272+
public LocalDateTime convert(Date source) {
273+
return LocalDateTime.ofInstant(Instant.ofEpochMilli(source.getTime()), ZoneId.of("UTC"));
274+
}
275+
}
276+
277+
private enum DateToUtcLocalTimeConverter implements Converter<Date, LocalTime> {
278+
INSTANCE;
279+
280+
@Override
281+
public LocalTime convert(Date source) {
282+
return DateToUtcLocalDateTimeConverter.INSTANCE.convert(source).toLocalTime();
283+
}
284+
}
285+
286+
private enum DateToUtcLocalDateConverter implements Converter<Date, LocalDate> {
287+
INSTANCE;
288+
289+
@Override
290+
public LocalDate convert(Date source) {
291+
return DateToUtcLocalDateTimeConverter.INSTANCE.convert(source).toLocalDate();
292+
}
293+
}
294+
}
102295
}

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoSimpleTypes.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package org.springframework.data.mongodb.core.mapping;
1717

1818
import java.math.BigInteger;
19+
import java.time.Instant;
1920
import java.util.Collections;
2021
import java.util.HashSet;
2122
import java.util.Set;
@@ -71,6 +72,7 @@ public abstract class MongoSimpleTypes {
7172
simpleTypes.add(Pattern.class);
7273
simpleTypes.add(Symbol.class);
7374
simpleTypes.add(UUID.class);
75+
simpleTypes.add(Instant.class);
7476

7577
simpleTypes.add(BsonBinary.class);
7678
simpleTypes.add(BsonBoolean.class);

0 commit comments

Comments
 (0)