diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/DateToInstantConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/DateToInstantConverter.java new file mode 100644 index 00000000000..74d1b9c70fd --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/DateToInstantConverter.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core.convert.support; + +import java.time.Instant; +import java.util.Date; + +import org.springframework.core.convert.converter.Converter; + +/** + * Convert a {@link java.util.Date} to a {@link java.time.Instant}. + * + *

This includes conversion support for {@link java.sql.Timestamp} and other + * subtypes of {@code java.util.Date}. Note, however, that an attempt to convert + * a {@link java.sql.Date} or {@link java.sql.Time} to a {@code java.time.Instant} + * results in an {@link UnsupportedOperationException} since those types do not + * have time or date components, respectively. + * + * @author Sam Brannen + * @since 6.2.9 + * @see Date#toInstant() + * @see InstantToDateConverter + */ +final class DateToInstantConverter implements Converter { + + @Override + public Instant convert(Date date) { + return date.toInstant(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.java b/spring-core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.java index b597b912466..7ba1285348d 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.java @@ -91,6 +91,8 @@ public class DefaultConversionService extends GenericConversionService { addCollectionConverters(converterRegistry); converterRegistry.addConverter(new ByteBufferConverter((ConversionService) converterRegistry)); + converterRegistry.addConverter(new DateToInstantConverter()); + converterRegistry.addConverter(new InstantToDateConverter()); converterRegistry.addConverter(new StringToTimeZoneConverter()); converterRegistry.addConverter(new ZoneIdToTimeZoneConverter()); converterRegistry.addConverter(new ZonedDateTimeToCalendarConverter()); diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/InstantToDateConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/InstantToDateConverter.java new file mode 100644 index 00000000000..56185c16790 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/InstantToDateConverter.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core.convert.support; + +import java.time.Instant; +import java.util.Date; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalConverter; +import org.springframework.core.convert.converter.Converter; + +/** + * Convert a {@link java.time.Instant} to a {@link java.util.Date}. + * + *

This does not include conversion support for target types which are subtypes + * of {@code java.util.Date}. + * + * @author Sam Brannen + * @since 6.2.9 + * @see Date#from(Instant) + * @see DateToInstantConverter + */ +final class InstantToDateConverter implements ConditionalConverter, Converter { + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return targetType.getType().equals(Date.class); + } + + @Override + public Date convert(Instant instant) { + return Date.from(instant); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.java b/spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.java index 903b1857ede..88113d9f8a6 100644 --- a/spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.java +++ b/spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.java @@ -22,12 +22,16 @@ import java.math.BigDecimal; import java.math.BigInteger; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.sql.Time; +import java.sql.Timestamp; +import java.time.Instant; import java.time.ZoneId; import java.util.AbstractList; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Currency; +import java.util.Date; import java.util.EnumSet; import java.util.HashMap; import java.util.LinkedHashMap; @@ -973,6 +977,88 @@ class DefaultConversionServiceTests { assertThat((Object) conversionService.convert(Optional.empty(), Optional.class)).isSameAs(Optional.empty()); } + @Test // gh-35175 + void convertDateToInstant() { + TypeDescriptor dateDescriptor = TypeDescriptor.valueOf(Date.class); + TypeDescriptor instantDescriptor = TypeDescriptor.valueOf(Instant.class); + Date date = new Date(); + + // Conversion performed by DateToInstantConverter. + assertThat(conversionService.convert(date, dateDescriptor, instantDescriptor)) + .isEqualTo(date.toInstant()); + } + + @Test // gh-35175 + void convertSqlDateToInstant() { + TypeDescriptor sqlDateDescriptor = TypeDescriptor.valueOf(java.sql.Date.class); + TypeDescriptor instantDescriptor = TypeDescriptor.valueOf(Instant.class); + java.sql.Date sqlDate = new java.sql.Date(System.currentTimeMillis()); + + // DateToInstantConverter blindly invokes toInstant() on any java.util.Date + // subtype, which results in an UnsupportedOperationException since + // java.sql.Date does not have a time component. However, even if + // DateToInstantConverter were not registered, ObjectToObjectConverter + // would still attempt to invoke toInstant() on a java.sql.Date by convention, + // which results in the same UnsupportedOperationException. + assertThatExceptionOfType(ConversionFailedException.class) + .isThrownBy(() -> conversionService.convert(sqlDate, sqlDateDescriptor, instantDescriptor)) + .withCauseExactlyInstanceOf(UnsupportedOperationException.class); + } + + @Test // gh-35175 + void convertSqlTimeToInstant() { + TypeDescriptor timeDescriptor = TypeDescriptor.valueOf(Time.class); + TypeDescriptor instantDescriptor = TypeDescriptor.valueOf(Instant.class); + Time time = new Time(System.currentTimeMillis()); + + // DateToInstantConverter blindly invokes toInstant() on any java.util.Date + // subtype, which results in an UnsupportedOperationException since + // java.sql.Date does not have a time component. However, even if + // DateToInstantConverter were not registered, ObjectToObjectConverter + // would still attempt to invoke toInstant() on a java.sql.Date by convention, + // which results in the same UnsupportedOperationException. + assertThatExceptionOfType(ConversionFailedException.class) + .isThrownBy(() -> conversionService.convert(time, timeDescriptor, instantDescriptor)) + .withCauseExactlyInstanceOf(UnsupportedOperationException.class); + } + + @Test // gh-35175 + void convertSqlTimestampToInstant() { + TypeDescriptor timestampDescriptor = TypeDescriptor.valueOf(Timestamp.class); + TypeDescriptor instantDescriptor = TypeDescriptor.valueOf(Instant.class); + Timestamp timestamp = new Timestamp(System.currentTimeMillis()); + + // Conversion performed by DateToInstantConverter. + assertThat(conversionService.convert(timestamp, timestampDescriptor, instantDescriptor)) + .isEqualTo(timestamp.toInstant()); + } + + @Test // gh-35175 + void convertInstantToDate() { + TypeDescriptor instantDescriptor = TypeDescriptor.valueOf(Instant.class); + TypeDescriptor dateDescriptor = TypeDescriptor.valueOf(Date.class); + Date date = new Date(); + Instant instant = date.toInstant(); + + // Conversion performed by InstantToDateConverter. + assertThat(conversionService.convert(instant, instantDescriptor, dateDescriptor)) + .isExactlyInstanceOf(Date.class) + .isEqualTo(date); + } + + @Test + void convertInstantToSqlTimestamp() { + TypeDescriptor instantDescriptor = TypeDescriptor.valueOf(Instant.class); + TypeDescriptor timestampDescriptor = TypeDescriptor.valueOf(Timestamp.class); + Timestamp timestamp = new Timestamp(System.currentTimeMillis()); + Instant instant = timestamp.toInstant(); + + // Conversion performed by ObjectToObjectConverter. + assertThat(conversionService.convert(instant, instantDescriptor, timestampDescriptor)) + .isExactlyInstanceOf(Timestamp.class) + .isEqualTo(timestamp); + } + // test fields and helpers