001: /*
002: * Copyright 2002-2007 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: * http://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:
017: package org.springframework.jdbc.core;
018:
019: import java.math.BigDecimal;
020: import java.sql.Blob;
021: import java.sql.Clob;
022: import java.sql.ResultSet;
023: import java.sql.ResultSetMetaData;
024: import java.sql.SQLException;
025:
026: import org.springframework.dao.TypeMismatchDataAccessException;
027: import org.springframework.jdbc.IncorrectResultSetColumnCountException;
028: import org.springframework.jdbc.support.JdbcUtils;
029: import org.springframework.util.NumberUtils;
030:
031: /**
032: * {@link RowMapper} implementation that converts a single column into a single
033: * result value per row. Expects to operate on a <code>java.sql.ResultSet</code>
034: * that just contains a single column.
035: *
036: * <p>The type of the result value for each row can be specified. The value
037: * for the single column will be extracted from the <code>ResultSet</code>
038: * and converted into the specified target type.
039: *
040: * @author Juergen Hoeller
041: * @since 1.2
042: * @see JdbcTemplate#queryForList(String, Class)
043: * @see JdbcTemplate#queryForObject(String, Class)
044: */
045: public class SingleColumnRowMapper implements RowMapper {
046:
047: private Class requiredType;
048:
049: /**
050: * Create a new SingleColumnRowMapper.
051: * @see #setRequiredType
052: */
053: public SingleColumnRowMapper() {
054: }
055:
056: /**
057: * Create a new SingleColumnRowMapper.
058: * @param requiredType the type that each result object is expected to match
059: */
060: public SingleColumnRowMapper(Class requiredType) {
061: this .requiredType = requiredType;
062: }
063:
064: /**
065: * Set the type that each result object is expected to match.
066: * <p>If not specified, the column value will be exposed as
067: * returned by the JDBC driver.
068: */
069: public void setRequiredType(Class requiredType) {
070: this .requiredType = requiredType;
071: }
072:
073: /**
074: * Extract a value for the single column in the current row.
075: * <p>Validates that there is only one column selected,
076: * then delegates to <code>getColumnValue()</code> and also
077: * <code>convertValueToRequiredType</code>, if necessary.
078: * @see java.sql.ResultSetMetaData#getColumnCount()
079: * @see #getColumnValue(java.sql.ResultSet, int, Class)
080: * @see #convertValueToRequiredType(Object, Class)
081: */
082: public Object mapRow(ResultSet rs, int rowNum) throws SQLException {
083: // Validate column count.
084: ResultSetMetaData rsmd = rs.getMetaData();
085: int nrOfColumns = rsmd.getColumnCount();
086: if (nrOfColumns != 1) {
087: throw new IncorrectResultSetColumnCountException(1,
088: nrOfColumns);
089: }
090:
091: // Extract column value from JDBC ResultSet
092: Object result = getColumnValue(rs, 1, this .requiredType);
093: if (result != null && this .requiredType != null
094: && !this .requiredType.isInstance(result)) {
095: // Extracted value does not match already: try to convert it.
096: try {
097: return convertValueToRequiredType(result,
098: this .requiredType);
099: } catch (IllegalArgumentException ex) {
100: throw new TypeMismatchDataAccessException(
101: "Type mismatch affecting row number " + rowNum
102: + " and column type '"
103: + rsmd.getColumnTypeName(1) + "': "
104: + ex.getMessage());
105: }
106: }
107: return result;
108: }
109:
110: /**
111: * Retrieve a JDBC object value for the specified column.
112: * <p>The default implementation calls <code>ResultSet.getString(index)</code> etc
113: * for all standard value types (String, Boolean, number types, date types, etc).
114: * It calls <code>ResultSet.getObject(index)</code> else.
115: * <p>If no required type has been specified, this method delegates to
116: * <code>getColumnValue(rs, index)</code>, which basically calls
117: * <code>ResultSet.getObject(index)</code> but applies some additional
118: * default conversion to appropriate value types.
119: * <p>Explicit extraction of a String is necessary to properly extract an Oracle
120: * RAW value as a String, for example. For the other given types, it is also
121: * recommendable to extract the desired types explicitly, to let the JDBC driver
122: * perform appropriate (potentially database-specific) conversion.
123: * @param rs is the ResultSet holding the data
124: * @param index is the column index
125: * @param requiredType the type that each result object is expected to match
126: * (or <code>null</code> if none specified)
127: * @return the Object value
128: * @throws SQLException in case of extraction failure
129: * @see java.sql.ResultSet#getString(int)
130: * @see java.sql.ResultSet#getObject(int)
131: * @see #getColumnValue(java.sql.ResultSet, int)
132: */
133: protected Object getColumnValue(ResultSet rs, int index,
134: Class requiredType) throws SQLException {
135: if (requiredType != null) {
136: Object value = null;
137: boolean wasNullCheck = false;
138:
139: // Explicitly extract typed value, as far as possible.
140: if (String.class.equals(requiredType)) {
141: value = rs.getString(index);
142: } else if (Boolean.class.equals(requiredType)) {
143: value = (rs.getBoolean(index) ? Boolean.TRUE
144: : Boolean.FALSE);
145: wasNullCheck = true;
146: } else if (Byte.class.equals(requiredType)) {
147: value = new Byte(rs.getByte(index));
148: wasNullCheck = true;
149: } else if (Short.class.equals(requiredType)) {
150: value = new Short(rs.getShort(index));
151: wasNullCheck = true;
152: } else if (Integer.class.equals(requiredType)) {
153: value = new Integer(rs.getInt(index));
154: wasNullCheck = true;
155: } else if (Long.class.equals(requiredType)) {
156: value = new Long(rs.getLong(index));
157: wasNullCheck = true;
158: } else if (Float.class.equals(requiredType)) {
159: value = new Float(rs.getFloat(index));
160: wasNullCheck = true;
161: } else if (Double.class.equals(requiredType)
162: || Number.class.equals(requiredType)) {
163: value = new Double(rs.getDouble(index));
164: wasNullCheck = true;
165: } else if (byte[].class.equals(requiredType)) {
166: value = rs.getBytes(index);
167: } else if (java.sql.Date.class.equals(requiredType)) {
168: value = rs.getDate(index);
169: } else if (java.sql.Time.class.equals(requiredType)) {
170: value = rs.getTime(index);
171: } else if (java.sql.Timestamp.class.equals(requiredType)
172: || java.util.Date.class.equals(requiredType)) {
173: value = rs.getTimestamp(index);
174: } else if (BigDecimal.class.equals(requiredType)) {
175: value = rs.getBigDecimal(index);
176: } else if (Blob.class.equals(requiredType)) {
177: value = rs.getBlob(index);
178: } else if (Clob.class.equals(requiredType)) {
179: value = rs.getClob(index);
180: } else {
181: // Some unknown type desired -> rely on getObject.
182: value = rs.getObject(index);
183: }
184:
185: // Perform was-null check if demanded (for results that the
186: // JDBC driver returns as primitives).
187: if (wasNullCheck && value != null && rs.wasNull()) {
188: value = null;
189: }
190: return value;
191: }
192:
193: else {
194: // No required type specified -> perform default extraction.
195: return getColumnValue(rs, index);
196: }
197: }
198:
199: /**
200: * Retrieve a JDBC object value for the specified column, using the most
201: * appropriate value type. Called if no required type has been specified.
202: * <p>The default implementation delegates to <code>JdbcUtils.getResultSetValue()</code>,
203: * which uses the <code>ResultSet.getObject(index)</code> method. Additionally,
204: * it includes a "hack" to get around Oracle returning a non-standard object for
205: * their TIMESTAMP datatype. See the <code>JdbcUtils#getResultSetValue()</code>
206: * javadoc for details.
207: * @param rs is the ResultSet holding the data
208: * @param index is the column index
209: * @return the Object value
210: * @throws SQLException in case of extraction failure
211: * @see org.springframework.jdbc.support.JdbcUtils#getResultSetValue(java.sql.ResultSet, int)
212: */
213: protected Object getColumnValue(ResultSet rs, int index)
214: throws SQLException {
215: return JdbcUtils.getResultSetValue(rs, index);
216: }
217:
218: /**
219: * Convert the given column value to the specified required type.
220: * Only called if the extracted column value does not match already.
221: * <p>If the required type is String, the value will simply get stringified
222: * via <code>toString()</code>. In case of a Number, the value will be
223: * converted into a Number, either through number conversion or through
224: * String parsing (depending on the value type).
225: * @param value the column value as extracted from <code>getColumnValue()</code>
226: * (never <code>null</code>)
227: * @param requiredType the type that each result object is expected to match
228: * (never <code>null</code>)
229: * @return the converted value
230: * @see #getColumnValue(java.sql.ResultSet, int, Class)
231: */
232: protected Object convertValueToRequiredType(Object value,
233: Class requiredType) {
234: if (String.class.equals(requiredType)) {
235: return value.toString();
236: } else if (Number.class.isAssignableFrom(requiredType)) {
237: if (value instanceof Number) {
238: // Convert original Number to target Number class.
239: return NumberUtils.convertNumberToTargetClass(
240: ((Number) value), requiredType);
241: } else {
242: // Convert stringified value to target Number class.
243: return NumberUtils.parseNumber(value.toString(),
244: requiredType);
245: }
246: } else {
247: throw new IllegalArgumentException("Value [" + value
248: + "] is of type [" + value.getClass().getName()
249: + "] and cannot be converted to required type ["
250: + requiredType.getName() + "]");
251: }
252: }
253:
254: }
|