Skip to content

Commit c6e50d3

Browse files
schauderodrotbohm
authored andcommitted
DATACMNS-1259 - Fixed support for Long values in Auditables.
Using Instant as internal data type since it's a point in time without time zone which LocalDateTime isn't. Added necessary converters. Fixed one JodaTime converter that used UTC to use SystemDefault like other similar converters. In case of a conversion failure the error message now contains the source type. Original pull request: #273.
1 parent 0b524e1 commit c6e50d3

File tree

5 files changed

+175
-8
lines changed

5 files changed

+175
-8
lines changed

src/main/java/org/springframework/data/auditing/DefaultAuditableBeanWrapperFactory.java

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import lombok.RequiredArgsConstructor;
2020

2121
import java.lang.reflect.Field;
22+
import java.time.Instant;
2223
import java.time.LocalDateTime;
2324
import java.time.temporal.TemporalAccessor;
2425
import java.util.Calendar;
@@ -42,6 +43,7 @@
4243
*
4344
* @author Oliver Gierke
4445
* @author Christoph Strobl
46+
* @author Jens Schauder
4547
* @since 1.5
4648
*/
4749
class DefaultAuditableBeanWrapperFactory implements AuditableBeanWrapperFactory {
@@ -207,8 +209,7 @@ protected Object getDateValueToSet(TemporalAccessor value, Class<?> targetType,
207209
return conversionService.convert(date, targetType);
208210
}
209211

210-
throw new IllegalArgumentException(String.format("Invalid date type for member %s! Supported types are %s.",
211-
source, AnnotationAuditingMetadata.SUPPORTED_DATE_TYPES));
212+
throw new IllegalArgumentException(createUnsupportedTypeErrorMessage(source));
212213
}
213214

214215
/**
@@ -228,19 +229,24 @@ protected <T extends TemporalAccessor> Optional<T> getAsTemporalAccessor(Optiona
228229
return (T) it;
229230
}
230231

231-
Class<?> typeToConvertTo = Stream.of(target, LocalDateTime.class)//
232+
Class<?> typeToConvertTo = Stream.of(target, Instant.class)//
232233
.filter(type -> target.isAssignableFrom(type))//
233234
.filter(type -> conversionService.canConvert(it.getClass(), type))//
234235
.findFirst()
235236
.orElseThrow(() -> new IllegalArgumentException(
236-
String.format("Invalid date type for member %s! Supported types are %s.", source,
237-
AnnotationAuditingMetadata.SUPPORTED_DATE_TYPES)));
237+
createUnsupportedTypeErrorMessage(((Optional<Object>)source).orElseGet(() -> source))));
238238

239239
return (T) conversionService.convert(it, typeToConvertTo);
240240
});
241241
}
242242
}
243243

244+
private static String createUnsupportedTypeErrorMessage(Object source) {
245+
246+
return String.format("Invalid date type %s for member %s! Supported types are %s.", source.getClass(), source,
247+
AnnotationAuditingMetadata.SUPPORTED_DATE_TYPES);
248+
}
249+
244250
/**
245251
* An {@link AuditableBeanWrapper} implementation that sets values on the target object using reflection.
246252
*

src/main/java/org/springframework/data/convert/JodaTimeConverters.java

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,19 @@
1515
*/
1616
package org.springframework.data.convert;
1717

18+
import java.time.Instant;
1819
import java.time.ZoneId;
1920
import java.util.ArrayList;
2021
import java.util.Collection;
2122
import java.util.Collections;
2223
import java.util.Date;
2324
import java.util.List;
25+
import java.util.TimeZone;
2426

2527
import javax.annotation.Nonnull;
2628

2729
import org.joda.time.DateTime;
30+
import org.joda.time.DateTimeZone;
2831
import org.joda.time.LocalDate;
2932
import org.joda.time.LocalDateTime;
3033
import org.springframework.core.convert.converter.Converter;
@@ -65,6 +68,9 @@ public abstract class JodaTimeConverters {
6568
converters.add(LocalDateTimeToJodaLocalDateTime.INSTANCE);
6669
converters.add(LocalDateTimeToJodaDateTime.INSTANCE);
6770

71+
converters.add(InstantToJodaLocalDateTime.INSTANCE);
72+
converters.add(JodaLocalDateTimeToInstant.INSTANCE);
73+
6874
converters.add(LocalDateTimeToJsr310Converter.INSTANCE);
6975

7076
return converters;
@@ -77,7 +83,7 @@ public enum LocalDateTimeToJsr310Converter implements Converter<LocalDateTime, j
7783
@Nonnull
7884
@Override
7985
public java.time.LocalDateTime convert(LocalDateTime source) {
80-
return java.time.LocalDateTime.ofInstant(source.toDate().toInstant(), ZoneId.of("UTC"));
86+
return java.time.LocalDateTime.ofInstant(source.toDate().toInstant(), ZoneId.systemDefault());
8187
}
8288
}
8389

@@ -158,6 +164,28 @@ public LocalDateTime convert(java.time.LocalDateTime source) {
158164
}
159165
}
160166

167+
public enum InstantToJodaLocalDateTime implements Converter<java.time.Instant, LocalDateTime> {
168+
169+
INSTANCE;
170+
171+
@Nonnull
172+
@Override
173+
public LocalDateTime convert(java.time.Instant source) {
174+
return LocalDateTime.fromDateFields(new Date(source.toEpochMilli()));
175+
}
176+
}
177+
178+
public enum JodaLocalDateTimeToInstant implements Converter<LocalDateTime, Instant> {
179+
180+
INSTANCE;
181+
182+
@Nonnull
183+
@Override
184+
public Instant convert(LocalDateTime source) {
185+
return Instant.ofEpochMilli(source.toDateTime().getMillis());
186+
}
187+
}
188+
161189
public enum LocalDateTimeToJodaDateTime implements Converter<java.time.LocalDateTime, DateTime> {
162190

163191
INSTANCE;

src/main/java/org/springframework/data/convert/ThreeTenBackPortConverters.java

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,15 @@
3636
import org.threeten.bp.LocalDateTime;
3737
import org.threeten.bp.LocalTime;
3838
import org.threeten.bp.ZoneId;
39+
import org.threeten.bp.ZoneOffset;
3940

4041
/**
4142
* Helper class to register {@link Converter} implementations for the ThreeTen Backport project in case it's present on
4243
* the classpath.
4344
*
4445
* @author Oliver Gierke
4546
* @author Christoph Strobl
47+
* @author Jens Schauder
4648
* @see <a href="http://www.threeten.org/threetenbp">http://www.threeten.org/threetenbp</a>
4749
* @since 1.10
4850
*/
@@ -74,6 +76,8 @@ public abstract class ThreeTenBackPortConverters {
7476
converters.add(ZoneIdToStringConverter.INSTANCE);
7577
converters.add(StringToZoneIdConverter.INSTANCE);
7678
converters.add(LocalDateTimeToJsr310LocalDateTimeConverter.INSTANCE);
79+
converters.add(LocalDateTimeToJavaTimeInstantConverter.INSTANCE);
80+
converters.add(JavaTimeInstantToLocalDateTimeConverter.INSTANCE);
7781

7882
return converters;
7983
}
@@ -84,7 +88,7 @@ public static boolean supports(Class<?> type) {
8488
return false;
8589
}
8690

87-
return Arrays.<Class<?>> asList(LocalDateTime.class, LocalDate.class, LocalTime.class, Instant.class)
91+
return Arrays.<Class<?>> asList(LocalDateTime.class, LocalDate.class, LocalTime.class, Instant.class, java.time.Instant.class)
8892
.contains(type);
8993
}
9094

@@ -195,6 +199,28 @@ public Date convert(Instant source) {
195199
}
196200
}
197201

202+
public static enum LocalDateTimeToJavaTimeInstantConverter implements Converter<LocalDateTime, java.time.Instant> {
203+
204+
INSTANCE;
205+
206+
@Nonnull
207+
@Override
208+
public java.time.Instant convert(LocalDateTime source) {
209+
return java.time.Instant.ofEpochMilli(source.atZone(ZoneOffset.systemDefault()).toInstant().toEpochMilli());
210+
}
211+
}
212+
213+
public static enum JavaTimeInstantToLocalDateTimeConverter implements Converter<java.time.Instant, LocalDateTime> {
214+
215+
INSTANCE;
216+
217+
@Nonnull
218+
@Override
219+
public LocalDateTime convert(java.time.Instant source) {
220+
return LocalDateTime.ofInstant(Instant.ofEpochMilli(source.toEpochMilli()), ZoneOffset.systemDefault());
221+
}
222+
}
223+
198224
@WritingConverter
199225
public static enum ZoneIdToStringConverter implements Converter<ZoneId, String> {
200226

src/test/java/org/springframework/data/auditing/DefaultAuditableBeanWrapperFactoryUnitTests.java

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,27 @@
1818
import static org.assertj.core.api.Assertions.*;
1919

2020
import java.time.Instant;
21+
import java.time.LocalDateTime;
22+
import java.time.ZoneOffset;
2123
import java.time.ZonedDateTime;
24+
import java.time.temporal.ChronoField;
25+
import java.time.temporal.TemporalAccessor;
26+
import java.time.temporal.TemporalField;
2227
import java.util.Optional;
2328

2429
import org.junit.Test;
30+
import org.springframework.data.annotation.CreatedDate;
31+
import org.springframework.data.annotation.LastModifiedDate;
2532
import org.springframework.data.auditing.DefaultAuditableBeanWrapperFactory.AuditableInterfaceBeanWrapper;
2633
import org.springframework.data.auditing.DefaultAuditableBeanWrapperFactory.ReflectionAuditingBeanWrapper;
34+
import org.springframework.data.domain.Auditable;
2735

2836
/**
2937
* Unit tests for {@link DefaultAuditableBeanWrapperFactory}.
3038
*
3139
* @author Oliver Gierke
3240
* @author Christoph Strobl
41+
* @author Jens Schauder
3342
* @since 1.5
3443
*/
3544
public class DefaultAuditableBeanWrapperFactoryUnitTests {
@@ -88,4 +97,70 @@ public void errorsWhenUnableToConvertDateViaIntermediateJavaUtilDateConversion()
8897

8998
assertThat(wrapper).hasValueSatisfying(it -> it.setLastModifiedDate(zonedDateTime));
9099
}
100+
101+
@Test // DATACMNS-1259
102+
public void lastModifiedDateAsLongIsAvailableViaWrapper() {
103+
104+
LongBasedAuditable source = new LongBasedAuditable();
105+
source.dateModified = 42000L;
106+
107+
Optional<AuditableBeanWrapper> beanWrapper = factory.getBeanWrapperFor(source);
108+
assertThat(beanWrapper).isPresent();
109+
110+
assertThat(beanWrapper.flatMap(AuditableBeanWrapper::getLastModifiedDate).get()) //
111+
.extracting(ta -> ta.getLong(ChronoField.INSTANT_SECONDS)) //
112+
.containsExactly(42L);
113+
}
114+
115+
116+
@Test // DATACMNS-1259
117+
public void canSetLastModifiedDateAsInstantViaWrapperOnLongField() {
118+
119+
LongBasedAuditable source = new LongBasedAuditable();
120+
121+
Optional<AuditableBeanWrapper> beanWrapper = factory.getBeanWrapperFor(source);
122+
assertThat(beanWrapper).isPresent();
123+
124+
beanWrapper.get().setLastModifiedDate(Instant.ofEpochMilli(42L));
125+
126+
assertThat(source.dateModified).isEqualTo(42L);
127+
}
128+
129+
@Test // DATACMNS-1259
130+
public void canSetLastModifiedDateAsLocalDateTimeViaWrapperOnLongField() {
131+
132+
LongBasedAuditable source = new LongBasedAuditable();
133+
134+
Optional<AuditableBeanWrapper> beanWrapper = factory.getBeanWrapperFor(source);
135+
assertThat(beanWrapper).isPresent();
136+
137+
beanWrapper.get().setLastModifiedDate(LocalDateTime.ofInstant(Instant.ofEpochMilli(42L), ZoneOffset.systemDefault()));
138+
139+
assertThat(source.dateModified).isEqualTo(42L);
140+
}
141+
142+
143+
@Test // DATACMNS-1259
144+
public void lastModifiedAsLocalDateTimeDateIsAvailableViaWrapperAsLocalDateTime() {
145+
146+
LocalDateTime now = LocalDateTime.now();
147+
148+
AuditedUser source = new AuditedUser();
149+
source.setLastModifiedDate(now);
150+
151+
Optional<AuditableBeanWrapper> beanWrapper = factory.getBeanWrapperFor(source);
152+
assertThat(beanWrapper).isPresent();
153+
154+
assertThat(beanWrapper.flatMap(AuditableBeanWrapper::getLastModifiedDate).get()) //
155+
.isEqualTo(now);
156+
}
157+
158+
public static class LongBasedAuditable {
159+
160+
@CreatedDate
161+
public Long dateCreated;
162+
163+
@LastModifiedDate
164+
public Long dateModified;
165+
}
91166
}

src/test/java/org/springframework/data/auditing/MappingAuditableBeanWrapperFactoryUnitTests.java

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,16 @@
2020

2121
import java.time.Instant;
2222
import java.time.LocalDateTime;
23+
import java.time.ZoneOffset;
24+
import java.time.temporal.ChronoField;
2325
import java.time.temporal.TemporalAccessor;
2426
import java.util.Calendar;
2527
import java.util.Collections;
2628
import java.util.Date;
2729
import java.util.GregorianCalendar;
2830
import java.util.Optional;
2931

32+
import org.assertj.core.api.AbstractLongAssert;
3033
import org.junit.Before;
3134
import org.junit.Test;
3235
import org.springframework.data.annotation.CreatedBy;
@@ -45,6 +48,7 @@
4548
* Unit tests for {@link MappingAuditableBeanWrapperFactory}.
4649
*
4750
* @author Oliver Gierke
51+
* @author Jens Schauder
4852
* @since 1.8
4953
*/
5054
public class MappingAuditableBeanWrapperFactoryUnitTests {
@@ -169,14 +173,42 @@ public void exposesInstantAsModificationDate() {
169173
assertThat(wrapper.flatMap(it -> it.getLastModifiedDate())).hasValue(sample.modified);
170174
}
171175

176+
@Test // DATACMNS-1259
177+
public void exposesLongAsModificationDate() {
178+
179+
Long reference = new Date().getTime();
180+
181+
assertLastModificationDate(reference, Instant.ofEpochMilli(reference));
182+
}
183+
172184
private void assertLastModificationDate(Object source, TemporalAccessor expected) {
173185

174186
Sample sample = new Sample();
175187
sample.lastModifiedDate = source;
176188

177189
Optional<AuditableBeanWrapper> wrapper = factory.getBeanWrapperFor(sample);
178190

179-
assertThat(wrapper.flatMap(it -> it.getLastModifiedDate())).hasValue(expected);
191+
assertThat(wrapper.flatMap(it -> it.getLastModifiedDate())).hasValueSatisfying(ta -> {
192+
compareTemporalAccessors(expected, ta);
193+
});
194+
}
195+
196+
private AbstractLongAssert<?> compareTemporalAccessors(TemporalAccessor expected, TemporalAccessor actual) {
197+
198+
long actualSeconds = getInstantSeconds(actual);
199+
long expectedSeconds = getInstantSeconds(expected);
200+
201+
return assertThat(actualSeconds).describedAs("Difference is %s", actualSeconds - expectedSeconds)
202+
.isEqualTo(expectedSeconds);
203+
}
204+
205+
private long getInstantSeconds(TemporalAccessor actual) {
206+
207+
if (actual instanceof LocalDateTime) {
208+
return getInstantSeconds(((LocalDateTime) actual).atZone(ZoneOffset.systemDefault()));
209+
}
210+
211+
return actual.getLong(ChronoField.INSTANT_SECONDS);
180212
}
181213

182214
static class Sample {

0 commit comments

Comments
 (0)