Skip to content

Commit d0ba125

Browse files
mp911deschauder
authored andcommitted
Introduce support to pass-thru TemporalAccessor auditing values.
We now allow passing-thru TemporalAccessor auditing values, bypassing conversion if the target value type matches the value provided from e.g. DateTimeProvider. Refined the error messages and listing all commonly supported types for which we provide converters. Closes #2719 Original pull request #2874
1 parent f2387f6 commit d0ba125

File tree

6 files changed

+113
-19
lines changed

6 files changed

+113
-19
lines changed

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

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

1818
import java.lang.reflect.Field;
19+
import java.time.temporal.TemporalAccessor;
1920
import java.util.ArrayList;
2021
import java.util.Collections;
2122
import java.util.Date;
@@ -58,7 +59,12 @@ final class AnnotationAuditingMetadata {
5859

5960
static {
6061

61-
List<String> types = new ArrayList<>(3);
62+
List<String> types = new ArrayList<>(Jsr310Converters.getSupportedClasses() //
63+
.stream() //
64+
.filter(TemporalAccessor.class::isAssignableFrom) //
65+
.map(Class::getName) //
66+
.toList());
67+
6268
types.add(Date.class.getName());
6369
types.add(Long.class.getName());
6470
types.add(long.class.getName());
@@ -104,7 +110,7 @@ private void assertValidDateFieldType(Optional<Field> field) {
104110

105111
Class<?> type = it.getType();
106112

107-
if (Jsr310Converters.supports(type)) {
113+
if (TemporalAccessor.class.isAssignableFrom(type)) {
108114
return;
109115
}
110116

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

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ public <T> Optional<AuditableBeanWrapper<T>> getBeanWrapperFor(T source) {
7373
return Optional.of(source).map(it -> {
7474

7575
if (it instanceof Auditable) {
76-
return (AuditableBeanWrapper<T>) new AuditableInterfaceBeanWrapper(conversionService, (Auditable<Object, ?, TemporalAccessor>) it);
76+
return (AuditableBeanWrapper<T>) new AuditableInterfaceBeanWrapper(conversionService,
77+
(Auditable<Object, ?, TemporalAccessor>) it);
7778
}
7879

7980
AnnotationAuditingMetadata metadata = AnnotationAuditingMetadata.getMetadata(it.getClass());
@@ -98,7 +99,8 @@ static class AuditableInterfaceBeanWrapper
9899
private final Class<? extends TemporalAccessor> type;
99100

100101
@SuppressWarnings("unchecked")
101-
public AuditableInterfaceBeanWrapper(ConversionService conversionService, Auditable<Object, ?, TemporalAccessor> auditable) {
102+
public AuditableInterfaceBeanWrapper(ConversionService conversionService,
103+
Auditable<Object, ?, TemporalAccessor> auditable) {
102104

103105
super(conversionService);
104106

@@ -151,8 +153,8 @@ public TemporalAccessor setLastModifiedDate(TemporalAccessor value) {
151153
}
152154

153155
/**
154-
* Base class for {@link AuditableBeanWrapper} implementations that might need to convert {@link TemporalAccessor} values into
155-
* compatible types when setting date/time information.
156+
* Base class for {@link AuditableBeanWrapper} implementations that might need to convert {@link TemporalAccessor}
157+
* values into compatible types when setting date/time information.
156158
*
157159
* @author Oliver Gierke
158160
* @since 1.8
@@ -168,15 +170,15 @@ abstract static class DateConvertingAuditableBeanWrapper<T> implements Auditable
168170
/**
169171
* Returns the {@link TemporalAccessor} in a type, compatible to the given field.
170172
*
171-
* @param value can be {@literal null}.
173+
* @param value must not be {@literal null}.
172174
* @param targetType must not be {@literal null}.
173175
* @param source must not be {@literal null}.
174176
* @return
175177
*/
176178
@Nullable
177179
protected Object getDateValueToSet(TemporalAccessor value, Class<?> targetType, Object source) {
178180

179-
if (TemporalAccessor.class.equals(targetType)) {
181+
if (targetType.isInstance(value)) {
180182
return value;
181183
}
182184

@@ -188,15 +190,15 @@ protected Object getDateValueToSet(TemporalAccessor value, Class<?> targetType,
188190

189191
if (!conversionService.canConvert(value.getClass(), Date.class)) {
190192
throw new IllegalArgumentException(
191-
String.format("Cannot convert date type for member %s; From %s to java.util.Date to %s", source,
193+
String.format("Cannot convert date type for %s; From %s to java.util.Date to %s", source,
192194
value.getClass(), targetType));
193195
}
194196

195197
Date date = conversionService.convert(value, Date.class);
196198
return conversionService.convert(date, targetType);
197199
}
198200

199-
throw rejectUnsupportedType(source);
201+
throw rejectUnsupportedType(value.getClass(), targetType);
200202
}
201203

202204
/**
@@ -217,19 +219,20 @@ protected <S extends TemporalAccessor> Optional<S> getAsTemporalAccessor(Optiona
217219
}
218220

219221
Class<?> typeToConvertTo = Stream.of(target, Instant.class)//
220-
.filter(type -> target.isAssignableFrom(type))//
222+
.filter(target::isAssignableFrom)//
221223
.filter(type -> conversionService.canConvert(it.getClass(), type))//
222224
.findFirst() //
223-
.orElseThrow(() -> rejectUnsupportedType(source.map(Object.class::cast).orElseGet(() -> source)));
225+
.orElseThrow(() -> rejectUnsupportedType(it.getClass(), target));
224226

225227
return (S) conversionService.convert(it, typeToConvertTo);
226228
});
227229
}
228230
}
229231

230-
private static IllegalArgumentException rejectUnsupportedType(Object source) {
231-
return new IllegalArgumentException(String.format("Invalid date type %s for member %s; Supported types are %s",
232-
source.getClass(), source, AnnotationAuditingMetadata.SUPPORTED_DATE_TYPES));
232+
private static IllegalArgumentException rejectUnsupportedType(Class<?> sourceType, Class<?> targetType) {
233+
return new IllegalArgumentException(
234+
String.format("Cannot convert unsupported date type %s to %s; Supported types are %s", sourceType.getName(),
235+
targetType.getName(), AnnotationAuditingMetadata.SUPPORTED_DATE_TYPES));
233236
}
234237

235238
/**
@@ -264,7 +267,6 @@ public Object setCreatedBy(Object value) {
264267

265268
@Override
266269
public TemporalAccessor setCreatedDate(TemporalAccessor value) {
267-
268270
return setDateField(metadata.getCreatedDateField(), value);
269271
}
270272

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,8 @@ static class MappingAuditingMetadata {
114114
/**
115115
* Creates a new {@link MappingAuditingMetadata} instance from the given {@link PersistentEntity}.
116116
*
117-
* @param entity must not be {@literal null}.
117+
* @param context must not be {@literal null}.
118+
* @param type must not be {@literal null}.
118119
*/
119120
public <P> MappingAuditingMetadata(MappingContext<?, ? extends PersistentProperty<?>> context, Class<?> type) {
120121

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import java.util.ArrayList;
3131
import java.util.Arrays;
3232
import java.util.Collection;
33+
import java.util.Collections;
3334
import java.util.Date;
3435
import java.util.List;
3536

@@ -82,10 +83,17 @@ public abstract class Jsr310Converters {
8283
}
8384

8485
public static boolean supports(Class<?> type) {
85-
8686
return CLASSES.contains(type);
8787
}
8888

89+
/**
90+
* @return the collection of supported temporal classes.
91+
* @since 3.2
92+
*/
93+
public static Collection<Class<?>> getSupportedClasses() {
94+
return Collections.unmodifiableList(CLASSES);
95+
}
96+
8997
@ReadingConverter
9098
public enum DateToLocalDateTimeConverter implements Converter<Date, LocalDateTime> {
9199

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

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919

2020
import java.time.Instant;
2121
import java.time.LocalDateTime;
22+
import java.time.OffsetDateTime;
2223
import java.time.ZoneOffset;
24+
import java.time.ZonedDateTime;
2325
import java.time.temporal.ChronoField;
2426

2527
import org.junit.jupiter.api.Test;
@@ -34,7 +36,7 @@
3436
* @author Oliver Gierke
3537
* @author Christoph Strobl
3638
* @author Jens Schauder
37-
* @since 1.5
39+
* @author Mark Paluch
3840
*/
3941
class DefaultAuditableBeanWrapperFactoryUnitTests {
4042

@@ -135,10 +137,56 @@ void lastModifiedAsLocalDateTimeDateIsAvailableViaWrapperAsLocalDateTime() {
135137
assertThat(result).hasValue(now);
136138
}
137139

140+
@Test
141+
void shouldRejectUnsupportedTemporalConversion() {
142+
143+
var source = new WithZonedDateTime();
144+
AuditableBeanWrapper<WithZonedDateTime> wrapper = factory.getBeanWrapperFor(source).get();
145+
146+
assertThatIllegalArgumentException().isThrownBy(() -> wrapper.setCreatedDate(LocalDateTime.now()))
147+
.withMessageContaining(
148+
"Cannot convert unsupported date type java.time.LocalDateTime to java.time.ZonedDateTime");
149+
}
150+
151+
@Test // GH-2719
152+
void shouldPassthruZonedDateTimeValue() {
153+
154+
var source = new WithZonedDateTime();
155+
var now = ZonedDateTime.now();
156+
AuditableBeanWrapper<WithZonedDateTime> wrapper = factory.getBeanWrapperFor(source).get();
157+
158+
wrapper.setCreatedDate(now);
159+
160+
assertThat(source.created).isEqualTo(now);
161+
}
162+
163+
@Test // GH-2719
164+
void shouldPassthruOffsetDatetimeValue() {
165+
166+
var source = new WithOffsetDateTime();
167+
var now = OffsetDateTime.now();
168+
AuditableBeanWrapper<WithOffsetDateTime> wrapper = factory.getBeanWrapperFor(source).get();
169+
170+
wrapper.setCreatedDate(now);
171+
172+
assertThat(source.created).isEqualTo(now);
173+
}
174+
138175
public static class LongBasedAuditable {
139176

140177
@CreatedDate public Long dateCreated;
141178

142179
@LastModifiedDate public Long dateModified;
143180
}
181+
182+
static class WithZonedDateTime {
183+
184+
@CreatedDate ZonedDateTime created;
185+
}
186+
187+
static class WithOffsetDateTime {
188+
189+
@CreatedDate OffsetDateTime created;
190+
}
191+
144192
}

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.time.Instant;
2222
import java.time.LocalDateTime;
2323
import java.time.ZoneOffset;
24+
import java.time.ZonedDateTime;
2425
import java.time.temporal.ChronoField;
2526
import java.time.temporal.TemporalAccessor;
2627
import java.util.Arrays;
@@ -242,6 +243,29 @@ void skipsCollectionPropertiesWhenSettingProperties() {
242243
});
243244
}
244245

246+
@Test // GH-2719
247+
void shouldRejectUnsupportedTemporalConversion() {
248+
249+
var source = new WithZonedDateTime();
250+
AuditableBeanWrapper<WithZonedDateTime> wrapper = factory.getBeanWrapperFor(source).get();
251+
252+
assertThatIllegalArgumentException().isThrownBy(() -> wrapper.setCreatedDate(LocalDateTime.now()))
253+
.withMessageContaining(
254+
"Cannot convert unsupported date type java.time.LocalDateTime to java.time.ZonedDateTime");
255+
}
256+
257+
@Test // GH-2719
258+
void shouldPassthruTemporalValue() {
259+
260+
var source = new WithZonedDateTime();
261+
var now = ZonedDateTime.now();
262+
AuditableBeanWrapper<WithZonedDateTime> wrapper = factory.getBeanWrapperFor(source).get();
263+
264+
wrapper.setCreatedDate(now);
265+
266+
assertThat(source.created).isEqualTo(now);
267+
}
268+
245269
private void assertLastModificationDate(Object source, TemporalAccessor expected) {
246270

247271
var sample = new Sample();
@@ -302,6 +326,11 @@ static class Embedded {
302326
@LastModifiedBy String modifier;
303327
}
304328

329+
static class WithZonedDateTime {
330+
331+
@CreatedDate ZonedDateTime created;
332+
}
333+
305334
static class WithEmbedded {
306335
Embedded embedded;
307336
Collection<Embedded> embeddeds;

0 commit comments

Comments
 (0)