View Javadoc
1   /*
2    * Copyright 2002-2014 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.springframework.jdbc.support;
18  
19  import java.lang.reflect.InvocationTargetException;
20  import java.lang.reflect.Method;
21  import java.math.BigDecimal;
22  import java.sql.Blob;
23  import java.sql.Clob;
24  import java.sql.Connection;
25  import java.sql.DatabaseMetaData;
26  import java.sql.ResultSet;
27  import java.sql.ResultSetMetaData;
28  import java.sql.SQLException;
29  import java.sql.SQLFeatureNotSupportedException;
30  import java.sql.Statement;
31  import java.sql.Types;
32  import javax.sql.DataSource;
33  
34  import org.apache.commons.logging.Log;
35  import org.apache.commons.logging.LogFactory;
36  
37  import org.springframework.jdbc.CannotGetJdbcConnectionException;
38  import org.springframework.jdbc.datasource.DataSourceUtils;
39  import org.springframework.lang.UsesJava7;
40  import org.springframework.util.ClassUtils;
41  
42  /**
43   * Generic utility methods for working with JDBC. Mainly for internal use
44   * within the framework, but also useful for custom JDBC access code.
45   *
46   * @author Thomas Risberg
47   * @author Juergen Hoeller
48   */
49  public abstract class JdbcUtils {
50  
51  	/**
52  	 * Constant that indicates an unknown (or unspecified) SQL type.
53  	 * @see java.sql.Types
54  	 */
55  	public static final int TYPE_UNKNOWN = Integer.MIN_VALUE;
56  
57  
58  	// Check for JDBC 4.1 getObject(int, Class) method - available on JDK 7 and higher
59  	private static final boolean getObjectWithTypeAvailable =
60  			ClassUtils.hasMethod(ResultSet.class, "getObject", int.class, Class.class);
61  
62  	private static final Log logger = LogFactory.getLog(JdbcUtils.class);
63  
64  
65  	/**
66  	 * Close the given JDBC Connection and ignore any thrown exception.
67  	 * This is useful for typical finally blocks in manual JDBC code.
68  	 * @param con the JDBC Connection to close (may be {@code null})
69  	 */
70  	public static void closeConnection(Connection con) {
71  		if (con != null) {
72  			try {
73  				con.close();
74  			}
75  			catch (SQLException ex) {
76  				logger.debug("Could not close JDBC Connection", ex);
77  			}
78  			catch (Throwable ex) {
79  				// We don't trust the JDBC driver: It might throw RuntimeException or Error.
80  				logger.debug("Unexpected exception on closing JDBC Connection", ex);
81  			}
82  		}
83  	}
84  
85  	/**
86  	 * Close the given JDBC Statement and ignore any thrown exception.
87  	 * This is useful for typical finally blocks in manual JDBC code.
88  	 * @param stmt the JDBC Statement to close (may be {@code null})
89  	 */
90  	public static void closeStatement(Statement stmt) {
91  		if (stmt != null) {
92  			try {
93  				stmt.close();
94  			}
95  			catch (SQLException ex) {
96  				logger.trace("Could not close JDBC Statement", ex);
97  			}
98  			catch (Throwable ex) {
99  				// We don't trust the JDBC driver: It might throw RuntimeException or Error.
100 				logger.trace("Unexpected exception on closing JDBC Statement", ex);
101 			}
102 		}
103 	}
104 
105 	/**
106 	 * Close the given JDBC ResultSet and ignore any thrown exception.
107 	 * This is useful for typical finally blocks in manual JDBC code.
108 	 * @param rs the JDBC ResultSet to close (may be {@code null})
109 	 */
110 	public static void closeResultSet(ResultSet rs) {
111 		if (rs != null) {
112 			try {
113 				rs.close();
114 			}
115 			catch (SQLException ex) {
116 				logger.trace("Could not close JDBC ResultSet", ex);
117 			}
118 			catch (Throwable ex) {
119 				// We don't trust the JDBC driver: It might throw RuntimeException or Error.
120 				logger.trace("Unexpected exception on closing JDBC ResultSet", ex);
121 			}
122 		}
123 	}
124 
125 	/**
126 	 * Retrieve a JDBC column value from a ResultSet, using the specified value type.
127 	 * <p>Uses the specifically typed ResultSet accessor methods, falling back to
128 	 * {@link #getResultSetValue(java.sql.ResultSet, int)} for unknown types.
129 	 * <p>Note that the returned value may not be assignable to the specified
130 	 * required type, in case of an unknown type. Calling code needs to deal
131 	 * with this case appropriately, e.g. throwing a corresponding exception.
132 	 * @param rs is the ResultSet holding the data
133 	 * @param index is the column index
134 	 * @param requiredType the required value type (may be {@code null})
135 	 * @return the value object
136 	 * @throws SQLException if thrown by the JDBC API
137 	 */
138 	@UsesJava7  // guard optional use of JDBC 4.1 (safe with 1.6 due to getObjectWithTypeAvailable check)
139 	public static Object getResultSetValue(ResultSet rs, int index, Class<?> requiredType) throws SQLException {
140 		if (requiredType == null) {
141 			return getResultSetValue(rs, index);
142 		}
143 
144 		Object value;
145 
146 		// Explicitly extract typed value, as far as possible.
147 		if (String.class.equals(requiredType)) {
148 			return rs.getString(index);
149 		}
150 		else if (boolean.class.equals(requiredType) || Boolean.class.equals(requiredType)) {
151 			value = rs.getBoolean(index);
152 		}
153 		else if (byte.class.equals(requiredType) || Byte.class.equals(requiredType)) {
154 			value = rs.getByte(index);
155 		}
156 		else if (short.class.equals(requiredType) || Short.class.equals(requiredType)) {
157 			value = rs.getShort(index);
158 		}
159 		else if (int.class.equals(requiredType) || Integer.class.equals(requiredType)) {
160 			value = rs.getInt(index);
161 		}
162 		else if (long.class.equals(requiredType) || Long.class.equals(requiredType)) {
163 			value = rs.getLong(index);
164 		}
165 		else if (float.class.equals(requiredType) || Float.class.equals(requiredType)) {
166 			value = rs.getFloat(index);
167 		}
168 		else if (double.class.equals(requiredType) || Double.class.equals(requiredType) ||
169 				Number.class.equals(requiredType)) {
170 			value = rs.getDouble(index);
171 		}
172 		else if (BigDecimal.class.equals(requiredType)) {
173 			return rs.getBigDecimal(index);
174 		}
175 		else if (java.sql.Date.class.equals(requiredType)) {
176 			return rs.getDate(index);
177 		}
178 		else if (java.sql.Time.class.equals(requiredType)) {
179 			return rs.getTime(index);
180 		}
181 		else if (java.sql.Timestamp.class.equals(requiredType) || java.util.Date.class.equals(requiredType)) {
182 			return rs.getTimestamp(index);
183 		}
184 		else if (byte[].class.equals(requiredType)) {
185 			return rs.getBytes(index);
186 		}
187 		else if (Blob.class.equals(requiredType)) {
188 			return rs.getBlob(index);
189 		}
190 		else if (Clob.class.equals(requiredType)) {
191 			return rs.getClob(index);
192 		}
193 		else {
194 			// Some unknown type desired -> rely on getObject.
195 			if (getObjectWithTypeAvailable) {
196 				try {
197 					return rs.getObject(index, requiredType);
198 				}
199 				catch (AbstractMethodError err) {
200 					logger.debug("JDBC driver does not implement JDBC 4.1 'getObject(int, Class)' method", err);
201 				}
202 				catch (SQLFeatureNotSupportedException ex) {
203 					logger.debug("JDBC driver does not support JDBC 4.1 'getObject(int, Class)' method", ex);
204 				}
205 				catch (SQLException ex) {
206 					logger.debug("JDBC driver has limited support for JDBC 4.1 'getObject(int, Class)' method", ex);
207 				}
208 			}
209 			// Fall back to getObject without type specification...
210 			return getResultSetValue(rs, index);
211 		}
212 
213 		// Perform was-null check if necessary (for results that the JDBC driver returns as primitives).
214 		return (rs.wasNull() ? null : value);
215 	}
216 
217 	/**
218 	 * Retrieve a JDBC column value from a ResultSet, using the most appropriate
219 	 * value type. The returned value should be a detached value object, not having
220 	 * any ties to the active ResultSet: in particular, it should not be a Blob or
221 	 * Clob object but rather a byte array or String representation, respectively.
222 	 * <p>Uses the {@code getObject(index)} method, but includes additional "hacks"
223 	 * to get around Oracle 10g returning a non-standard object for its TIMESTAMP
224 	 * datatype and a {@code java.sql.Date} for DATE columns leaving out the
225 	 * time portion: These columns will explicitly be extracted as standard
226 	 * {@code java.sql.Timestamp} object.
227 	 * @param rs is the ResultSet holding the data
228 	 * @param index is the column index
229 	 * @return the value object
230 	 * @throws SQLException if thrown by the JDBC API
231 	 * @see java.sql.Blob
232 	 * @see java.sql.Clob
233 	 * @see java.sql.Timestamp
234 	 */
235 	public static Object getResultSetValue(ResultSet rs, int index) throws SQLException {
236 		Object obj = rs.getObject(index);
237 		String className = null;
238 		if (obj != null) {
239 			className = obj.getClass().getName();
240 		}
241 		if (obj instanceof Blob) {
242 			Blob blob = (Blob) obj;
243 			obj = blob.getBytes(1, (int) blob.length());
244 		}
245 		else if (obj instanceof Clob) {
246 			Clob clob = (Clob) obj;
247 			obj = clob.getSubString(1, (int) clob.length());
248 		}
249 		else if ("oracle.sql.TIMESTAMP".equals(className) || "oracle.sql.TIMESTAMPTZ".equals(className)) {
250 			obj = rs.getTimestamp(index);
251 		}
252 		else if (className != null && className.startsWith("oracle.sql.DATE")) {
253 			String metaDataClassName = rs.getMetaData().getColumnClassName(index);
254 			if ("java.sql.Timestamp".equals(metaDataClassName) || "oracle.sql.TIMESTAMP".equals(metaDataClassName)) {
255 				obj = rs.getTimestamp(index);
256 			}
257 			else {
258 				obj = rs.getDate(index);
259 			}
260 		}
261 		else if (obj != null && obj instanceof java.sql.Date) {
262 			if ("java.sql.Timestamp".equals(rs.getMetaData().getColumnClassName(index))) {
263 				obj = rs.getTimestamp(index);
264 			}
265 		}
266 		return obj;
267 	}
268 
269 	/**
270 	 * Extract database meta data via the given DatabaseMetaDataCallback.
271 	 * <p>This method will open a connection to the database and retrieve the database metadata.
272 	 * Since this method is called before the exception translation feature is configured for
273 	 * a datasource, this method can not rely on the SQLException translation functionality.
274 	 * <p>Any exceptions will be wrapped in a MetaDataAccessException. This is a checked exception
275 	 * and any calling code should catch and handle this exception. You can just log the
276 	 * error and hope for the best, but there is probably a more serious error that will
277 	 * reappear when you try to access the database again.
278 	 * @param dataSource the DataSource to extract metadata for
279 	 * @param action callback that will do the actual work
280 	 * @return object containing the extracted information, as returned by
281 	 * the DatabaseMetaDataCallback's {@code processMetaData} method
282 	 * @throws MetaDataAccessException if meta data access failed
283 	 */
284 	public static Object extractDatabaseMetaData(DataSource dataSource, DatabaseMetaDataCallback action)
285 			throws MetaDataAccessException {
286 
287 		Connection con = null;
288 		try {
289 			con = DataSourceUtils.getConnection(dataSource);
290 			if (con == null) {
291 				// should only happen in test environments
292 				throw new MetaDataAccessException("Connection returned by DataSource [" + dataSource + "] was null");
293 			}
294 			DatabaseMetaData metaData = con.getMetaData();
295 			if (metaData == null) {
296 				// should only happen in test environments
297 				throw new MetaDataAccessException("DatabaseMetaData returned by Connection [" + con + "] was null");
298 			}
299 			return action.processMetaData(metaData);
300 		}
301 		catch (CannotGetJdbcConnectionException ex) {
302 			throw new MetaDataAccessException("Could not get Connection for extracting meta data", ex);
303 		}
304 		catch (SQLException ex) {
305 			throw new MetaDataAccessException("Error while extracting DatabaseMetaData", ex);
306 		}
307 		catch (AbstractMethodError err) {
308 			throw new MetaDataAccessException(
309 					"JDBC DatabaseMetaData method not implemented by JDBC driver - upgrade your driver", err);
310 		}
311 		finally {
312 			DataSourceUtils.releaseConnection(con, dataSource);
313 		}
314 	}
315 
316 	/**
317 	 * Call the specified method on DatabaseMetaData for the given DataSource,
318 	 * and extract the invocation result.
319 	 * @param dataSource the DataSource to extract meta data for
320 	 * @param metaDataMethodName the name of the DatabaseMetaData method to call
321 	 * @return the object returned by the specified DatabaseMetaData method
322 	 * @throws MetaDataAccessException if we couldn't access the DatabaseMetaData
323 	 * or failed to invoke the specified method
324 	 * @see java.sql.DatabaseMetaData
325 	 */
326 	public static Object extractDatabaseMetaData(DataSource dataSource, final String metaDataMethodName)
327 			throws MetaDataAccessException {
328 
329 		return extractDatabaseMetaData(dataSource,
330 				new DatabaseMetaDataCallback() {
331 					@Override
332 					public Object processMetaData(DatabaseMetaData dbmd) throws SQLException, MetaDataAccessException {
333 						try {
334 							Method method = DatabaseMetaData.class.getMethod(metaDataMethodName, (Class[]) null);
335 							return method.invoke(dbmd, (Object[]) null);
336 						}
337 						catch (NoSuchMethodException ex) {
338 							throw new MetaDataAccessException("No method named '" + metaDataMethodName +
339 									"' found on DatabaseMetaData instance [" + dbmd + "]", ex);
340 						}
341 						catch (IllegalAccessException ex) {
342 							throw new MetaDataAccessException(
343 									"Could not access DatabaseMetaData method '" + metaDataMethodName + "'", ex);
344 						}
345 						catch (InvocationTargetException ex) {
346 							if (ex.getTargetException() instanceof SQLException) {
347 								throw (SQLException) ex.getTargetException();
348 							}
349 							throw new MetaDataAccessException(
350 									"Invocation of DatabaseMetaData method '" + metaDataMethodName + "' failed", ex);
351 						}
352 					}
353 				});
354 	}
355 
356 	/**
357 	 * Return whether the given JDBC driver supports JDBC 2.0 batch updates.
358 	 * <p>Typically invoked right before execution of a given set of statements:
359 	 * to decide whether the set of SQL statements should be executed through
360 	 * the JDBC 2.0 batch mechanism or simply in a traditional one-by-one fashion.
361 	 * <p>Logs a warning if the "supportsBatchUpdates" methods throws an exception
362 	 * and simply returns {@code false} in that case.
363 	 * @param con the Connection to check
364 	 * @return whether JDBC 2.0 batch updates are supported
365 	 * @see java.sql.DatabaseMetaData#supportsBatchUpdates()
366 	 */
367 	public static boolean supportsBatchUpdates(Connection con) {
368 		try {
369 			DatabaseMetaData dbmd = con.getMetaData();
370 			if (dbmd != null) {
371 				if (dbmd.supportsBatchUpdates()) {
372 					logger.debug("JDBC driver supports batch updates");
373 					return true;
374 				}
375 				else {
376 					logger.debug("JDBC driver does not support batch updates");
377 				}
378 			}
379 		}
380 		catch (SQLException ex) {
381 			logger.debug("JDBC driver 'supportsBatchUpdates' method threw exception", ex);
382 		}
383 		return false;
384 	}
385 
386 	/**
387 	 * Extract a common name for the database in use even if various drivers/platforms provide varying names.
388 	 * @param source the name as provided in database metedata
389 	 * @return the common name to be used
390 	 */
391 	public static String commonDatabaseName(String source) {
392 		String name = source;
393 		if (source != null && source.startsWith("DB2")) {
394 			name = "DB2";
395 		}
396 		else if ("Sybase SQL Server".equals(source) ||
397 				"Adaptive Server Enterprise".equals(source) ||
398 				"ASE".equals(source) ||
399 				"sql server".equalsIgnoreCase(source) ) {
400 			name = "Sybase";
401 		}
402 		return name;
403 	}
404 
405 	/**
406 	 * Check whether the given SQL type is numeric.
407 	 * @param sqlType the SQL type to be checked
408 	 * @return whether the type is numeric
409 	 */
410 	public static boolean isNumeric(int sqlType) {
411 		return Types.BIT == sqlType || Types.BIGINT == sqlType || Types.DECIMAL == sqlType ||
412 				Types.DOUBLE == sqlType || Types.FLOAT == sqlType || Types.INTEGER == sqlType ||
413 				Types.NUMERIC == sqlType || Types.REAL == sqlType || Types.SMALLINT == sqlType ||
414 				Types.TINYINT == sqlType;
415 	}
416 
417 	/**
418 	 * Determine the column name to use. The column name is determined based on a
419 	 * lookup using ResultSetMetaData.
420 	 * <p>This method implementation takes into account recent clarifications
421 	 * expressed in the JDBC 4.0 specification:
422 	 * <p><i>columnLabel - the label for the column specified with the SQL AS clause.
423 	 * If the SQL AS clause was not specified, then the label is the name of the column</i>.
424 	 * @return the column name to use
425 	 * @param resultSetMetaData the current meta data to use
426 	 * @param columnIndex the index of the column for the look up
427 	 * @throws SQLException in case of lookup failure
428 	 */
429 	public static String lookupColumnName(ResultSetMetaData resultSetMetaData, int columnIndex) throws SQLException {
430 		String name = resultSetMetaData.getColumnLabel(columnIndex);
431 		if (name == null || name.length() < 1) {
432 			name = resultSetMetaData.getColumnName(columnIndex);
433 		}
434 		return name;
435 	}
436 
437 	/**
438 	 * Convert a column name with underscores to the corresponding property name using "camel case".  A name
439 	 * like "customer_number" would match a "customerNumber" property name.
440 	 * @param name the column name to be converted
441 	 * @return the name using "camel case"
442 	 */
443 	public static String convertUnderscoreNameToPropertyName(String name) {
444 		StringBuilder result = new StringBuilder();
445 		boolean nextIsUpper = false;
446 		if (name != null && name.length() > 0) {
447 			if (name.length() > 1 && name.substring(1,2).equals("_")) {
448 				result.append(name.substring(0, 1).toUpperCase());
449 			}
450 			else {
451 				result.append(name.substring(0, 1).toLowerCase());
452 			}
453 			for (int i = 1; i < name.length(); i++) {
454 				String s = name.substring(i, i + 1);
455 				if (s.equals("_")) {
456 					nextIsUpper = true;
457 				}
458 				else {
459 					if (nextIsUpper) {
460 						result.append(s.toUpperCase());
461 						nextIsUpper = false;
462 					}
463 					else {
464 						result.append(s.toLowerCase());
465 					}
466 				}
467 			}
468 		}
469 		return result.toString();
470 	}
471 
472 }