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}