diff --git a/spring-core/src/main/java/org/springframework/aot/hint/support/ObjectToObjectConverterRuntimeHints.java b/spring-core/src/main/java/org/springframework/aot/hint/support/ObjectToObjectConverterRuntimeHints.java index 0b77a49eb36..95fdb49a52c 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/support/ObjectToObjectConverterRuntimeHints.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/support/ObjectToObjectConverterRuntimeHints.java @@ -16,6 +16,7 @@ package org.springframework.aot.hint.support; +import java.time.Instant; import java.time.LocalDate; import java.util.Collections; import java.util.List; @@ -34,6 +35,7 @@ import org.springframework.aot.hint.TypeReference; * {@code org.springframework.core.convert.support.ObjectToObjectConverter}. * * @author Sebastien Deleuze + * @author Sam Brannen * @since 6.0 */ class ObjectToObjectConverterRuntimeHints implements RuntimeHintsRegistrar { @@ -41,6 +43,7 @@ class ObjectToObjectConverterRuntimeHints implements RuntimeHintsRegistrar { @Override public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { ReflectionHints reflectionHints = hints.reflection(); + TypeReference sqlDateTypeReference = TypeReference.of("java.sql.Date"); reflectionHints.registerTypeIfPresent(classLoader, sqlDateTypeReference.getName(), hint -> hint .withMethod("toLocalDate", Collections.emptyList(), ExecutableMode.INVOKE) @@ -48,8 +51,14 @@ class ObjectToObjectConverterRuntimeHints implements RuntimeHintsRegistrar { .withMethod("valueOf", List.of(TypeReference.of(LocalDate.class)), ExecutableMode.INVOKE) .onReachableType(sqlDateTypeReference)); + TypeReference sqlTimestampTypeReference = TypeReference.of("java.sql.Timestamp"); + reflectionHints.registerTypeIfPresent(classLoader, sqlTimestampTypeReference.getName(), hint -> hint + .withMethod("from", List.of(TypeReference.of(Instant.class)), ExecutableMode.INVOKE) + .onReachableType(sqlTimestampTypeReference)); + reflectionHints.registerTypeIfPresent(classLoader, "org.springframework.http.HttpMethod", builder -> builder.withMethod("valueOf", List.of(TypeReference.of(String.class)), ExecutableMode.INVOKE)); + reflectionHints.registerTypeIfPresent(classLoader, "java.net.URI", MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); } 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 e448a4e363e..05017f5a227 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/aot/hint/support/ObjectToObjectConverterRuntimeHintsTests.java b/spring-core/src/test/java/org/springframework/aot/hint/support/ObjectToObjectConverterRuntimeHintsTests.java index 5bd0f03c578..b69faa2a05b 100644 --- a/spring-core/src/test/java/org/springframework/aot/hint/support/ObjectToObjectConverterRuntimeHintsTests.java +++ b/spring-core/src/test/java/org/springframework/aot/hint/support/ObjectToObjectConverterRuntimeHintsTests.java @@ -17,6 +17,7 @@ package org.springframework.aot.hint.support; import java.net.URI; +import java.time.Instant; import java.time.LocalDate; import org.junit.jupiter.api.BeforeEach; @@ -24,38 +25,45 @@ import org.junit.jupiter.api.Test; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; -import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.util.ClassUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.aot.hint.predicate.RuntimeHintsPredicates.reflection; /** * Tests for {@link ObjectToObjectConverterRuntimeHints}. * * @author Sebastien Deleuze + * @author Sam Brannen */ class ObjectToObjectConverterRuntimeHintsTests { - private RuntimeHints hints; + private final RuntimeHints hints = new RuntimeHints(); + @BeforeEach void setup() { - this.hints = new RuntimeHints(); + ClassLoader classLoader = ClassUtils.getDefaultClassLoader(); SpringFactoriesLoader.forResourceLocation("META-INF/spring/aot.factories") - .load(RuntimeHintsRegistrar.class).forEach(registrar -> registrar - .registerHints(this.hints, ClassUtils.getDefaultClassLoader())); + .load(RuntimeHintsRegistrar.class) + .forEach(registrar -> registrar.registerHints(this.hints, classLoader)); } @Test void javaSqlDateHasHints() throws NoSuchMethodException { - assertThat(RuntimeHintsPredicates.reflection().onMethodInvocation(java.sql.Date.class, "toLocalDate")).accepts(this.hints); - assertThat(RuntimeHintsPredicates.reflection().onMethodInvocation(java.sql.Date.class.getMethod("valueOf", LocalDate.class))).accepts(this.hints); + assertThat(reflection().onMethodInvocation(java.sql.Date.class, "toLocalDate")).accepts(this.hints); + assertThat(reflection().onMethodInvocation(java.sql.Date.class.getMethod("valueOf", LocalDate.class))).accepts(this.hints); + } + + @Test // gh-35156 + void javaSqlTimestampHasHints() throws NoSuchMethodException { + assertThat(reflection().onMethodInvocation(java.sql.Timestamp.class.getMethod("from", Instant.class))).accepts(this.hints); } @Test - void uriHasHints() throws NoSuchMethodException { - assertThat(RuntimeHintsPredicates.reflection().onType(URI.class)).accepts(this.hints); + void uriHasHints() { + assertThat(reflection().onType(URI.class)).accepts(this.hints); } } 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 c670e10ba1a..09deb980d77 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; @@ -1066,6 +1070,88 @@ class DefaultConversionServiceTests { } } + @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