001/*
002 * Copyright 2002-2020 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.jdbc.core;
018
019import java.sql.ResultSet;
020import java.sql.ResultSetMetaData;
021import java.sql.SQLException;
022
023import org.springframework.core.convert.ConversionService;
024import org.springframework.core.convert.support.DefaultConversionService;
025import org.springframework.dao.TypeMismatchDataAccessException;
026import org.springframework.jdbc.IncorrectResultSetColumnCountException;
027import org.springframework.jdbc.support.JdbcUtils;
028import org.springframework.lang.Nullable;
029import org.springframework.util.ClassUtils;
030import org.springframework.util.NumberUtils;
031
032/**
033 * {@link RowMapper} implementation that converts a single column into a single
034 * result value per row. Expects to operate on a {@code java.sql.ResultSet}
035 * that just contains a single column.
036 *
037 * <p>The type of the result value for each row can be specified. The value
038 * for the single column will be extracted from the {@code ResultSet}
039 * and converted into the specified target type.
040 *
041 * @author Juergen Hoeller
042 * @author Kazuki Shimizu
043 * @since 1.2
044 * @param <T> the result type
045 * @see JdbcTemplate#queryForList(String, Class)
046 * @see JdbcTemplate#queryForObject(String, Class)
047 */
048public class SingleColumnRowMapper<T> implements RowMapper<T> {
049
050        @Nullable
051        private Class<?> requiredType;
052
053        @Nullable
054        private ConversionService conversionService = DefaultConversionService.getSharedInstance();
055
056        /**
057         * Create a new {@code SingleColumnRowMapper} for bean-style configuration.
058         * @see #setRequiredType
059         */
060        public SingleColumnRowMapper() {
061        }
062
063        /**
064         * Create a new {@code SingleColumnRowMapper}.
065         * @param requiredType the type that each result object is expected to match
066         */
067        public SingleColumnRowMapper(Class<T> requiredType) {
068                setRequiredType(requiredType);
069        }
070
071
072        /**
073         * Set the type that each result object is expected to match.
074         * <p>If not specified, the column value will be exposed as
075         * returned by the JDBC driver.
076         */
077        public void setRequiredType(Class<T> requiredType) {
078                this.requiredType = ClassUtils.resolvePrimitiveIfNecessary(requiredType);
079        }
080
081        /**
082         * Set a {@link ConversionService} for converting a fetched value.
083         * <p>Default is the {@link DefaultConversionService}.
084         * @since 5.0.4
085         * @see DefaultConversionService#getSharedInstance
086         */
087        public void setConversionService(@Nullable ConversionService conversionService) {
088                this.conversionService = conversionService;
089        }
090
091        /**
092         * Extract a value for the single column in the current row.
093         * <p>Validates that there is only one column selected,
094         * then delegates to {@code getColumnValue()} and also
095         * {@code convertValueToRequiredType}, if necessary.
096         * @see java.sql.ResultSetMetaData#getColumnCount()
097         * @see #getColumnValue(java.sql.ResultSet, int, Class)
098         * @see #convertValueToRequiredType(Object, Class)
099         */
100        @Override
101        @SuppressWarnings("unchecked")
102        @Nullable
103        public T mapRow(ResultSet rs, int rowNum) throws SQLException {
104                // Validate column count.
105                ResultSetMetaData rsmd = rs.getMetaData();
106                int nrOfColumns = rsmd.getColumnCount();
107                if (nrOfColumns != 1) {
108                        throw new IncorrectResultSetColumnCountException(1, nrOfColumns);
109                }
110
111                // Extract column value from JDBC ResultSet.
112                Object result = getColumnValue(rs, 1, this.requiredType);
113                if (result != null && this.requiredType != null && !this.requiredType.isInstance(result)) {
114                        // Extracted value does not match already: try to convert it.
115                        try {
116                                return (T) convertValueToRequiredType(result, this.requiredType);
117                        }
118                        catch (IllegalArgumentException ex) {
119                                throw new TypeMismatchDataAccessException(
120                                                "Type mismatch affecting row number " + rowNum + " and column type '" +
121                                                rsmd.getColumnTypeName(1) + "': " + ex.getMessage());
122                        }
123                }
124                return (T) result;
125        }
126
127        /**
128         * Retrieve a JDBC object value for the specified column.
129         * <p>The default implementation calls
130         * {@link JdbcUtils#getResultSetValue(java.sql.ResultSet, int, Class)}.
131         * If no required type has been specified, this method delegates to
132         * {@code getColumnValue(rs, index)}, which basically calls
133         * {@code ResultSet.getObject(index)} but applies some additional
134         * default conversion to appropriate value types.
135         * @param rs is the ResultSet holding the data
136         * @param index is the column index
137         * @param requiredType the type that each result object is expected to match
138         * (or {@code null} if none specified)
139         * @return the Object value
140         * @throws SQLException in case of extraction failure
141         * @see org.springframework.jdbc.support.JdbcUtils#getResultSetValue(java.sql.ResultSet, int, Class)
142         * @see #getColumnValue(java.sql.ResultSet, int)
143         */
144        @Nullable
145        protected Object getColumnValue(ResultSet rs, int index, @Nullable Class<?> requiredType) throws SQLException {
146                if (requiredType != null) {
147                        return JdbcUtils.getResultSetValue(rs, index, requiredType);
148                }
149                else {
150                        // No required type specified -> perform default extraction.
151                        return getColumnValue(rs, index);
152                }
153        }
154
155        /**
156         * Retrieve a JDBC object value for the specified column, using the most
157         * appropriate value type. Called if no required type has been specified.
158         * <p>The default implementation delegates to {@code JdbcUtils.getResultSetValue()},
159         * which uses the {@code ResultSet.getObject(index)} method. Additionally,
160         * it includes a "hack" to get around Oracle returning a non-standard object for
161         * their TIMESTAMP datatype. See the {@code JdbcUtils#getResultSetValue()}
162         * javadoc for details.
163         * @param rs is the ResultSet holding the data
164         * @param index is the column index
165         * @return the Object value
166         * @throws SQLException in case of extraction failure
167         * @see org.springframework.jdbc.support.JdbcUtils#getResultSetValue(java.sql.ResultSet, int)
168         */
169        @Nullable
170        protected Object getColumnValue(ResultSet rs, int index) throws SQLException {
171                return JdbcUtils.getResultSetValue(rs, index);
172        }
173
174        /**
175         * Convert the given column value to the specified required type.
176         * Only called if the extracted column value does not match already.
177         * <p>If the required type is String, the value will simply get stringified
178         * via {@code toString()}. In case of a Number, the value will be
179         * converted into a Number, either through number conversion or through
180         * String parsing (depending on the value type). Otherwise, the value will
181         * be converted to a required type using the {@link ConversionService}.
182         * @param value the column value as extracted from {@code getColumnValue()}
183         * (never {@code null})
184         * @param requiredType the type that each result object is expected to match
185         * (never {@code null})
186         * @return the converted value
187         * @see #getColumnValue(java.sql.ResultSet, int, Class)
188         */
189        @SuppressWarnings("unchecked")
190        @Nullable
191        protected Object convertValueToRequiredType(Object value, Class<?> requiredType) {
192                if (String.class == requiredType) {
193                        return value.toString();
194                }
195                else if (Number.class.isAssignableFrom(requiredType)) {
196                        if (value instanceof Number) {
197                                // Convert original Number to target Number class.
198                                return NumberUtils.convertNumberToTargetClass(((Number) value), (Class<Number>) requiredType);
199                        }
200                        else {
201                                // Convert stringified value to target Number class.
202                                return NumberUtils.parseNumber(value.toString(),(Class<Number>) requiredType);
203                        }
204                }
205                else if (this.conversionService != null && this.conversionService.canConvert(value.getClass(), requiredType)) {
206                        return this.conversionService.convert(value, requiredType);
207                }
208                else {
209                        throw new IllegalArgumentException(
210                                        "Value [" + value + "] is of type [" + value.getClass().getName() +
211                                        "] and cannot be converted to required type [" + requiredType.getName() + "]");
212                }
213        }
214
215
216        /**
217         * Static factory method to create a new {@code SingleColumnRowMapper}.
218         * @param requiredType the type that each result object is expected to match
219         * @since 4.1
220         * @see #newInstance(Class, ConversionService)
221         */
222        public static <T> SingleColumnRowMapper<T> newInstance(Class<T> requiredType) {
223                return new SingleColumnRowMapper<>(requiredType);
224        }
225
226        /**
227         * Static factory method to create a new {@code SingleColumnRowMapper}.
228         * @param requiredType the type that each result object is expected to match
229         * @param conversionService the {@link ConversionService} for converting a
230         * fetched value, or {@code null} for none
231         * @since 5.0.4
232         * @see #newInstance(Class)
233         * @see #setConversionService
234         */
235        public static <T> SingleColumnRowMapper<T> newInstance(
236                        Class<T> requiredType, @Nullable ConversionService conversionService) {
237
238                SingleColumnRowMapper<T> rowMapper = newInstance(requiredType);
239                rowMapper.setConversionService(conversionService);
240                return rowMapper;
241        }
242
243}