001/*
002 * Copyright 2002-2015 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.dao.TypeMismatchDataAccessException;
024import org.springframework.jdbc.IncorrectResultSetColumnCountException;
025import org.springframework.jdbc.support.JdbcUtils;
026import org.springframework.util.ClassUtils;
027import org.springframework.util.NumberUtils;
028
029/**
030 * {@link RowMapper} implementation that converts a single column into a single
031 * result value per row. Expects to operate on a {@code java.sql.ResultSet}
032 * that just contains a single column.
033 *
034 * <p>The type of the result value for each row can be specified. The value
035 * for the single column will be extracted from the {@code ResultSet}
036 * and converted into the specified target type.
037 *
038 * @author Juergen Hoeller
039 * @since 1.2
040 * @see JdbcTemplate#queryForList(String, Class)
041 * @see JdbcTemplate#queryForObject(String, Class)
042 */
043public class SingleColumnRowMapper<T> implements RowMapper<T> {
044
045        private Class<?> requiredType;
046
047
048        /**
049         * Create a new {@code SingleColumnRowMapper} for bean-style configuration.
050         * @see #setRequiredType
051         */
052        public SingleColumnRowMapper() {
053        }
054
055        /**
056         * Create a new {@code SingleColumnRowMapper}.
057         * <p>Consider using the {@link #newInstance} factory method instead,
058         * which allows for specifying the required type once only.
059         * @param requiredType the type that each result object is expected to match
060         */
061        public SingleColumnRowMapper(Class<T> requiredType) {
062                setRequiredType(requiredType);
063        }
064
065
066        /**
067         * Set the type that each result object is expected to match.
068         * <p>If not specified, the column value will be exposed as
069         * returned by the JDBC driver.
070         */
071        public void setRequiredType(Class<T> requiredType) {
072                this.requiredType = ClassUtils.resolvePrimitiveIfNecessary(requiredType);
073        }
074
075
076        /**
077         * Extract a value for the single column in the current row.
078         * <p>Validates that there is only one column selected,
079         * then delegates to {@code getColumnValue()} and also
080         * {@code convertValueToRequiredType}, if necessary.
081         * @see java.sql.ResultSetMetaData#getColumnCount()
082         * @see #getColumnValue(java.sql.ResultSet, int, Class)
083         * @see #convertValueToRequiredType(Object, Class)
084         */
085        @Override
086        @SuppressWarnings("unchecked")
087        public T mapRow(ResultSet rs, int rowNum) throws SQLException {
088                // Validate column count.
089                ResultSetMetaData rsmd = rs.getMetaData();
090                int nrOfColumns = rsmd.getColumnCount();
091                if (nrOfColumns != 1) {
092                        throw new IncorrectResultSetColumnCountException(1, nrOfColumns);
093                }
094
095                // Extract column value from JDBC ResultSet.
096                Object result = getColumnValue(rs, 1, this.requiredType);
097                if (result != null && this.requiredType != null && !this.requiredType.isInstance(result)) {
098                        // Extracted value does not match already: try to convert it.
099                        try {
100                                return (T) convertValueToRequiredType(result, this.requiredType);
101                        }
102                        catch (IllegalArgumentException ex) {
103                                throw new TypeMismatchDataAccessException(
104                                                "Type mismatch affecting row number " + rowNum + " and column type '" +
105                                                rsmd.getColumnTypeName(1) + "': " + ex.getMessage());
106                        }
107                }
108                return (T) result;
109        }
110
111        /**
112         * Retrieve a JDBC object value for the specified column.
113         * <p>The default implementation calls
114         * {@link JdbcUtils#getResultSetValue(java.sql.ResultSet, int, Class)}.
115         * If no required type has been specified, this method delegates to
116         * {@code getColumnValue(rs, index)}, which basically calls
117         * {@code ResultSet.getObject(index)} but applies some additional
118         * default conversion to appropriate value types.
119         * @param rs is the ResultSet holding the data
120         * @param index is the column index
121         * @param requiredType the type that each result object is expected to match
122         * (or {@code null} if none specified)
123         * @return the Object value
124         * @throws SQLException in case of extraction failure
125         * @see org.springframework.jdbc.support.JdbcUtils#getResultSetValue(java.sql.ResultSet, int, Class)
126         * @see #getColumnValue(java.sql.ResultSet, int)
127         */
128        protected Object getColumnValue(ResultSet rs, int index, Class<?> requiredType) throws SQLException {
129                if (requiredType != null) {
130                        return JdbcUtils.getResultSetValue(rs, index, requiredType);
131                }
132                else {
133                        // No required type specified -> perform default extraction.
134                        return getColumnValue(rs, index);
135                }
136        }
137
138        /**
139         * Retrieve a JDBC object value for the specified column, using the most
140         * appropriate value type. Called if no required type has been specified.
141         * <p>The default implementation delegates to {@code JdbcUtils.getResultSetValue()},
142         * which uses the {@code ResultSet.getObject(index)} method. Additionally,
143         * it includes a "hack" to get around Oracle returning a non-standard object for
144         * their TIMESTAMP datatype. See the {@code JdbcUtils#getResultSetValue()}
145         * javadoc for details.
146         * @param rs is the ResultSet holding the data
147         * @param index is the column index
148         * @return the Object value
149         * @throws SQLException in case of extraction failure
150         * @see org.springframework.jdbc.support.JdbcUtils#getResultSetValue(java.sql.ResultSet, int)
151         */
152        protected Object getColumnValue(ResultSet rs, int index) throws SQLException {
153                return JdbcUtils.getResultSetValue(rs, index);
154        }
155
156        /**
157         * Convert the given column value to the specified required type.
158         * Only called if the extracted column value does not match already.
159         * <p>If the required type is String, the value will simply get stringified
160         * via {@code toString()}. In case of a Number, the value will be
161         * converted into a Number, either through number conversion or through
162         * String parsing (depending on the value type).
163         * @param value the column value as extracted from {@code getColumnValue()}
164         * (never {@code null})
165         * @param requiredType the type that each result object is expected to match
166         * (never {@code null})
167         * @return the converted value
168         * @see #getColumnValue(java.sql.ResultSet, int, Class)
169         */
170        @SuppressWarnings("unchecked")
171        protected Object convertValueToRequiredType(Object value, Class<?> requiredType) {
172                if (String.class == requiredType) {
173                        return value.toString();
174                }
175                else if (Number.class.isAssignableFrom(requiredType)) {
176                        if (value instanceof Number) {
177                                // Convert original Number to target Number class.
178                                return NumberUtils.convertNumberToTargetClass(((Number) value), (Class<Number>) requiredType);
179                        }
180                        else {
181                                // Convert stringified value to target Number class.
182                                return NumberUtils.parseNumber(value.toString(),(Class<Number>) requiredType);
183                        }
184                }
185                else {
186                        throw new IllegalArgumentException(
187                                        "Value [" + value + "] is of type [" + value.getClass().getName() +
188                                        "] and cannot be converted to required type [" + requiredType.getName() + "]");
189                }
190        }
191
192
193        /**
194         * Static factory method to create a new {@code SingleColumnRowMapper}
195         * (with the required type specified only once).
196         * @param requiredType the type that each result object is expected to match
197         * @since 4.1
198         */
199        public static <T> SingleColumnRowMapper<T> newInstance(Class<T> requiredType) {
200                return new SingleColumnRowMapper<T>(requiredType);
201        }
202
203}