From 8c623c8a429afe83d3d6fed008eec0a9a45d29e2 Mon Sep 17 00:00:00 2001 From: Kazuki Shimizu Date: Mon, 12 Feb 2018 01:20:10 +0900 Subject: [PATCH] Supports ConversionService on SingleColumnRowMapper Issue: SPR-16483 --- .../jdbc/core/SingleColumnRowMapper.java | 36 +++++- .../jdbc/core/SingleColumnRowMapperTest.java | 107 ++++++++++++++++++ 2 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 spring-jdbc/src/test/java/org/springframework/jdbc/core/SingleColumnRowMapperTest.java diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/SingleColumnRowMapper.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SingleColumnRowMapper.java index 2b6b66a74b9..510fa9d71c3 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/SingleColumnRowMapper.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SingleColumnRowMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -20,6 +20,8 @@ import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.dao.TypeMismatchDataAccessException; import org.springframework.jdbc.IncorrectResultSetColumnCountException; import org.springframework.jdbc.support.JdbcUtils; @@ -37,6 +39,7 @@ import org.springframework.util.NumberUtils; * and converted into the specified target type. * * @author Juergen Hoeller + * @author Kazuki Shimizu * @since 1.2 * @see JdbcTemplate#queryForList(String, Class) * @see JdbcTemplate#queryForObject(String, Class) @@ -46,6 +49,8 @@ public class SingleColumnRowMapper implements RowMapper { @Nullable private Class requiredType; + @Nullable + private ConversionService conversionService = DefaultConversionService.getSharedInstance(); /** * Create a new {@code SingleColumnRowMapper} for bean-style configuration. @@ -74,6 +79,15 @@ public class SingleColumnRowMapper implements RowMapper { this.requiredType = ClassUtils.resolvePrimitiveIfNecessary(requiredType); } + /** + * Set a {@link ConversionService} for converting a fetched value. + *

Default is the {@link DefaultConversionService}. + * @since 5.0.4 + * @see DefaultConversionService#getSharedInstance + */ + public void setConversionService(@Nullable ConversionService conversionService) { + this.conversionService = conversionService; + } /** * Extract a value for the single column in the current row. @@ -164,7 +178,8 @@ public class SingleColumnRowMapper implements RowMapper { *

If the required type is String, the value will simply get stringified * via {@code toString()}. In case of a Number, the value will be * converted into a Number, either through number conversion or through - * String parsing (depending on the value type). + * String parsing (depending on the value type). Otherwise, the value will + * be converted to a required type using the {@link ConversionService}. * @param value the column value as extracted from {@code getColumnValue()} * (never {@code null}) * @param requiredType the type that each result object is expected to match @@ -173,6 +188,7 @@ public class SingleColumnRowMapper implements RowMapper { * @see #getColumnValue(java.sql.ResultSet, int, Class) */ @SuppressWarnings("unchecked") + @Nullable protected Object convertValueToRequiredType(Object value, Class requiredType) { if (String.class == requiredType) { return value.toString(); @@ -187,6 +203,9 @@ public class SingleColumnRowMapper implements RowMapper { return NumberUtils.parseNumber(value.toString(),(Class) requiredType); } } + else if (this.conversionService != null && this.conversionService.canConvert(value.getClass(), requiredType)) { + return this.conversionService.convert(value, requiredType); + } else { throw new IllegalArgumentException( "Value [" + value + "] is of type [" + value.getClass().getName() + @@ -205,4 +224,17 @@ public class SingleColumnRowMapper implements RowMapper { return new SingleColumnRowMapper<>(requiredType); } + /** + * Static factory method to create a new {@code SingleColumnRowMapper} + * (with the required type specified only once). + * @param requiredType the type that each result object is expected to match + * @param conversionService the {@link ConversionService} for converting a fetched value + * @since 5.0.4 + */ + public static SingleColumnRowMapper newInstance(Class requiredType, @Nullable ConversionService conversionService) { + SingleColumnRowMapper rowMapper = newInstance(requiredType); + rowMapper.setConversionService(conversionService); + return rowMapper; + } + } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/SingleColumnRowMapperTest.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/SingleColumnRowMapperTest.java new file mode 100644 index 00000000000..d885cf7b068 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/SingleColumnRowMapperTest.java @@ -0,0 +1,107 @@ +/* + * Copyright 2002-2018 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 + * + * http://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.jdbc.core; + +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.sql.Timestamp; +import java.time.LocalDateTime; + +import org.junit.Test; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.dao.TypeMismatchDataAccessException; + +import static org.mockito.BDDMockito.*; +import static org.junit.Assert.*; + +/** + * Tests for {@link SingleColumnRowMapper}. + * + * @author Kazuki Shimizu + * @since 5.0.4 + */ +public class SingleColumnRowMapperTest { + + @Test // SPR-16483 + public void useDefaultConversionService() throws SQLException { + Timestamp timestamp = new Timestamp(0); + + SingleColumnRowMapper rowMapper = SingleColumnRowMapper.newInstance(LocalDateTime.class); + + ResultSet resultSet = mock(ResultSet.class); + ResultSetMetaData metaData = mock(ResultSetMetaData.class); + given(metaData.getColumnCount()).willReturn(1); + given(resultSet.getMetaData()).willReturn(metaData); + given(resultSet.getObject(1, LocalDateTime.class)) + .willThrow(new SQLFeatureNotSupportedException()); + given(resultSet.getTimestamp(1)).willReturn(timestamp); + + LocalDateTime actualLocalDateTime = rowMapper.mapRow(resultSet, 1); + + assertEquals(timestamp.toLocalDateTime(), actualLocalDateTime); + } + + @Test // SPR-16483 + public void useCustomConversionService() throws SQLException { + Timestamp timestamp = new Timestamp(0); + + DefaultConversionService myConversionService = new DefaultConversionService(); + myConversionService.addConverter(Timestamp.class, MyLocalDateTime.class, + source -> new MyLocalDateTime(source.toLocalDateTime())); + SingleColumnRowMapper rowMapper = + SingleColumnRowMapper.newInstance(MyLocalDateTime.class, myConversionService); + + ResultSet resultSet = mock(ResultSet.class); + ResultSetMetaData metaData = mock(ResultSetMetaData.class); + given(metaData.getColumnCount()).willReturn(1); + given(resultSet.getMetaData()).willReturn(metaData); + given(resultSet.getObject(1, MyLocalDateTime.class)) + .willThrow(new SQLFeatureNotSupportedException()); + given(resultSet.getObject(1)).willReturn(timestamp); + + MyLocalDateTime actualMyLocalDateTime = rowMapper.mapRow(resultSet, 1); + + assertNotNull(actualMyLocalDateTime); + assertEquals(timestamp.toLocalDateTime(), actualMyLocalDateTime.value); + } + + @Test(expected = TypeMismatchDataAccessException.class) // SPR-16483 + public void doesNotUseConversionService() throws SQLException { + SingleColumnRowMapper rowMapper = + SingleColumnRowMapper.newInstance(LocalDateTime.class, null); + + ResultSet resultSet = mock(ResultSet.class); + ResultSetMetaData metaData = mock(ResultSetMetaData.class); + given(metaData.getColumnCount()).willReturn(1); + given(resultSet.getMetaData()).willReturn(metaData); + given(resultSet.getObject(1, LocalDateTime.class)) + .willThrow(new SQLFeatureNotSupportedException()); + given(resultSet.getTimestamp(1)).willReturn(new Timestamp(0)); + + rowMapper.mapRow(resultSet, 1); + } + + private static class MyLocalDateTime { + private final LocalDateTime value; + private MyLocalDateTime(LocalDateTime value) { + this.value = value; + } + } + +}