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}