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.support;
018:
019: import java.lang.reflect.Constructor;
020: import java.sql.SQLException;
021: import java.util.Arrays;
022:
023: import javax.sql.DataSource;
024:
025: import org.apache.commons.logging.Log;
026: import org.apache.commons.logging.LogFactory;
027:
028: import org.springframework.dao.CannotAcquireLockException;
029: import org.springframework.dao.CannotSerializeTransactionException;
030: import org.springframework.dao.DataAccessException;
031: import org.springframework.dao.DataAccessResourceFailureException;
032: import org.springframework.dao.DataIntegrityViolationException;
033: import org.springframework.dao.DeadlockLoserDataAccessException;
034: import org.springframework.dao.PermissionDeniedDataAccessException;
035: import org.springframework.jdbc.BadSqlGrammarException;
036: import org.springframework.jdbc.InvalidResultSetAccessException;
037:
038: /**
039: * Implementation of SQLExceptionTranslator that analyzes vendor-specific error codes.
040: * More precise than an implementation based on SQL state, but vendor-specific.
041: *
042: * <p>This class applies the following matching rules:
043: * <ul>
044: * <li>Try custom translation implemented by any subclass. Note that this class is
045: * concrete and is typically used itself, in which case this rule doesn't apply.
046: * <li>Apply error code matching. Error codes are obtained from the SQLErrorCodesFactory
047: * by default. This factory loads a "sql-error-codes.xml" file from the class path,
048: * defining error code mappings for database names from database metadata.
049: * <li>Fallback to a fallback translator. SQLStateSQLExceptionTranslator is the
050: * default fallback translator, analyzing the exception's SQL state only.
051: * </ul>
052: *
053: * <p>The configuration file named "sql-error-codes.xml" is by default read from
054: * this package. It can be overridden through a file of the same name in the root
055: * of the class path (e.g. in the "/WEB-INF/classes" directory).
056: *
057: * @author Rod Johnson
058: * @author Thomas Risberg
059: * @author Juergen Hoeller
060: * @see SQLErrorCodesFactory
061: * @see SQLStateSQLExceptionTranslator
062: */
063: public class SQLErrorCodeSQLExceptionTranslator implements
064: SQLExceptionTranslator {
065:
066: private static final int MESSAGE_ONLY_CONSTRUCTOR = 1;
067: private static final int MESSAGE_THROWABLE_CONSTRUCTOR = 2;
068: private static final int MESSAGE_SQLEX_CONSTRUCTOR = 3;
069: private static final int MESSAGE_SQL_THROWABLE_CONSTRUCTOR = 4;
070: private static final int MESSAGE_SQL_SQLEX_CONSTRUCTOR = 5;
071:
072: /** Logger available to subclasses */
073: protected final Log logger = LogFactory.getLog(getClass());
074:
075: /** Error codes used by this translator */
076: private SQLErrorCodes sqlErrorCodes;
077:
078: /** Fallback translator to use if SQL error code matching doesn't work */
079: private SQLExceptionTranslator fallbackTranslator = new SQLStateSQLExceptionTranslator();
080:
081: /**
082: * Constructor for use as a JavaBean.
083: * The SqlErrorCodes or DataSource property must be set.
084: */
085: public SQLErrorCodeSQLExceptionTranslator() {
086: }
087:
088: /**
089: * Create a SQL error code translator for the given DataSource.
090: * Invoking this constructor will cause a Connection to be obtained
091: * from the DataSource to get the metadata.
092: * @param dataSource DataSource to use to find metadata and establish
093: * which error codes are usable
094: * @see SQLErrorCodesFactory
095: */
096: public SQLErrorCodeSQLExceptionTranslator(DataSource dataSource) {
097: setDataSource(dataSource);
098: }
099:
100: /**
101: * Create a SQL error code translator for the given database product name.
102: * Invoking this constructor will avoid obtaining a Connection from the
103: * DataSource to get the metadata.
104: * @param dbName the database product name that identifies the error codes entry
105: * @see SQLErrorCodesFactory
106: * @see java.sql.DatabaseMetaData#getDatabaseProductName()
107: */
108: public SQLErrorCodeSQLExceptionTranslator(String dbName) {
109: setDatabaseProductName(dbName);
110: }
111:
112: /**
113: * Create a SQLErrorCode translator given these error codes.
114: * Does not require a database metadata lookup to be performed using a connection.
115: * @param sec error codes
116: */
117: public SQLErrorCodeSQLExceptionTranslator(SQLErrorCodes sec) {
118: this .sqlErrorCodes = sec;
119: }
120:
121: /**
122: * Set the DataSource for this translator.
123: * <p>Setting this property will cause a Connection to be obtained from
124: * the DataSource to get the metadata.
125: * @param dataSource DataSource to use to find metadata and establish
126: * which error codes are usable
127: * @see SQLErrorCodesFactory#getErrorCodes(javax.sql.DataSource)
128: * @see java.sql.DatabaseMetaData#getDatabaseProductName()
129: */
130: public void setDataSource(DataSource dataSource) {
131: this .sqlErrorCodes = SQLErrorCodesFactory.getInstance()
132: .getErrorCodes(dataSource);
133: }
134:
135: /**
136: * Set the database product name for this translator.
137: * <p>Setting this property will avoid obtaining a Connection from the DataSource
138: * to get the metadata.
139: * @param dbName the database product name that identifies the error codes entry
140: * @see SQLErrorCodesFactory#getErrorCodes(String)
141: * @see java.sql.DatabaseMetaData#getDatabaseProductName()
142: */
143: public void setDatabaseProductName(String dbName) {
144: this .sqlErrorCodes = SQLErrorCodesFactory.getInstance()
145: .getErrorCodes(dbName);
146: }
147:
148: /**
149: * Set custom error codes to be used for translation.
150: * @param sec custom error codes to use
151: */
152: public void setSqlErrorCodes(SQLErrorCodes sec) {
153: this .sqlErrorCodes = sec;
154: }
155:
156: /**
157: * Return the error codes used by this translator.
158: * Usually determined via a DataSource.
159: * @see #setDataSource
160: */
161: public SQLErrorCodes getSqlErrorCodes() {
162: return sqlErrorCodes;
163: }
164:
165: /**
166: * Override the default SQL state fallback translator.
167: * @param fallback custom fallback exception translator to use if error code
168: * translation fails
169: * @see SQLStateSQLExceptionTranslator
170: */
171: public void setFallbackTranslator(SQLExceptionTranslator fallback) {
172: this .fallbackTranslator = fallback;
173: }
174:
175: /**
176: * Return the fallback exception translator.
177: */
178: public SQLExceptionTranslator getFallbackTranslator() {
179: return fallbackTranslator;
180: }
181:
182: public DataAccessException translate(String task, String sql,
183: SQLException sqlEx) {
184: if (task == null) {
185: task = "";
186: }
187: if (sql == null) {
188: sql = "";
189: }
190:
191: // First, try custom translation from overridden method.
192: DataAccessException dex = customTranslate(task, sql, sqlEx);
193: if (dex != null) {
194: return dex;
195: }
196:
197: // Check SQLErrorCodes with corresponding error code, if available.
198: if (this .sqlErrorCodes != null) {
199: String errorCode = null;
200: if (this .sqlErrorCodes.isUseSqlStateForTranslation()) {
201: errorCode = sqlEx.getSQLState();
202: } else {
203: errorCode = Integer.toString(sqlEx.getErrorCode());
204: }
205:
206: if (errorCode != null) {
207:
208: // Look for defined custom translations first.
209: CustomSQLErrorCodesTranslation[] customTranslations = this .sqlErrorCodes
210: .getCustomTranslations();
211: if (customTranslations != null) {
212: for (int i = 0; i < customTranslations.length; i++) {
213: CustomSQLErrorCodesTranslation customTranslation = customTranslations[i];
214: if (Arrays.binarySearch(customTranslation
215: .getErrorCodes(), errorCode) >= 0) {
216: if (customTranslation.getExceptionClass() != null) {
217: DataAccessException customException = createCustomException(
218: task, sql, sqlEx,
219: customTranslation
220: .getExceptionClass());
221: if (customException != null) {
222: logTranslation(task, sql, sqlEx,
223: true);
224: return customException;
225: }
226: }
227: }
228: }
229: }
230:
231: // Next, look for grouped error codes.
232: if (Arrays.binarySearch(this .sqlErrorCodes
233: .getBadSqlGrammarCodes(), errorCode) >= 0) {
234: logTranslation(task, sql, sqlEx, false);
235: return new BadSqlGrammarException(task, sql, sqlEx);
236: } else if (Arrays.binarySearch(this .sqlErrorCodes
237: .getInvalidResultSetAccessCodes(), errorCode) >= 0) {
238: logTranslation(task, sql, sqlEx, false);
239: return new InvalidResultSetAccessException(task,
240: sql, sqlEx);
241: } else if (Arrays
242: .binarySearch(this .sqlErrorCodes
243: .getDataAccessResourceFailureCodes(),
244: errorCode) >= 0) {
245: logTranslation(task, sql, sqlEx, false);
246: return new DataAccessResourceFailureException(
247: buildMessage(task, sql, sqlEx), sqlEx);
248: } else if (Arrays.binarySearch(this .sqlErrorCodes
249: .getPermissionDeniedCodes(), errorCode) >= 0) {
250: logTranslation(task, sql, sqlEx, false);
251: return new PermissionDeniedDataAccessException(
252: buildMessage(task, sql, sqlEx), sqlEx);
253: } else if (Arrays.binarySearch(this .sqlErrorCodes
254: .getDataIntegrityViolationCodes(), errorCode) >= 0) {
255: logTranslation(task, sql, sqlEx, false);
256: return new DataIntegrityViolationException(
257: buildMessage(task, sql, sqlEx), sqlEx);
258: } else if (Arrays.binarySearch(this .sqlErrorCodes
259: .getCannotAcquireLockCodes(), errorCode) >= 0) {
260: logTranslation(task, sql, sqlEx, false);
261: return new CannotAcquireLockException(buildMessage(
262: task, sql, sqlEx), sqlEx);
263: } else if (Arrays.binarySearch(this .sqlErrorCodes
264: .getDeadlockLoserCodes(), errorCode) >= 0) {
265: logTranslation(task, sql, sqlEx, false);
266: return new DeadlockLoserDataAccessException(
267: buildMessage(task, sql, sqlEx), sqlEx);
268: } else if (Arrays.binarySearch(this .sqlErrorCodes
269: .getCannotSerializeTransactionCodes(),
270: errorCode) >= 0) {
271: logTranslation(task, sql, sqlEx, false);
272: return new CannotSerializeTransactionException(
273: buildMessage(task, sql, sqlEx), sqlEx);
274: }
275: }
276: }
277:
278: // We couldn't identify it more precisely - let's hand it over to the SQLState fallback translator.
279: if (logger.isDebugEnabled()) {
280: String codes = null;
281: if (this .sqlErrorCodes != null
282: && this .sqlErrorCodes.isUseSqlStateForTranslation()) {
283: codes = "SQL state '" + sqlEx.getSQLState()
284: + "', error code '" + sqlEx.getErrorCode();
285: } else {
286: codes = "Error code '" + sqlEx.getErrorCode() + "'";
287: }
288: logger.debug("Unable to translate SQLException with "
289: + codes + ", will now try the fallback translator");
290: }
291: return this .fallbackTranslator.translate(task, sql, sqlEx);
292: }
293:
294: /**
295: * Build a message String for the given SQLException.
296: * Called when creating an instance of a generic DataAccessException class.
297: * @param task readable text describing the task being attempted
298: * @param sql SQL query or update that caused the problem. May be <code>null</code>.
299: * @param sqlEx the offending SQLException
300: * @return the message String to use
301: */
302: protected String buildMessage(String task, String sql,
303: SQLException sqlEx) {
304: return task + "; SQL [" + sql + "]; " + sqlEx.getMessage();
305: }
306:
307: /**
308: * Subclasses can override this method to attempt a custom mapping from SQLException
309: * to DataAccessException.
310: * @param task readable text describing the task being attempted
311: * @param sql SQL query or update that caused the problem. May be <code>null</code>.
312: * @param sqlEx the offending SQLException
313: * @return null if no custom translation was possible, otherwise a DataAccessException
314: * resulting from custom translation. This exception should include the sqlEx parameter
315: * as a nested root cause. This implementation always returns null, meaning that
316: * the translator always falls back to the default error codes.
317: */
318: protected DataAccessException customTranslate(String task,
319: String sql, SQLException sqlEx) {
320: return null;
321: }
322:
323: /**
324: * Create a custom DataAccessException, based on a given exception
325: * class from a CustomSQLErrorCodesTranslation definition.
326: * @param task readable text describing the task being attempted
327: * @param sql SQL query or update that caused the problem. May be <code>null</code>.
328: * @param sqlEx the offending SQLException
329: * @param exceptionClass the exception class to use, as defined in the
330: * CustomSQLErrorCodesTranslation definition
331: * @return null if the custom exception could not be created, otherwise
332: * the resulting DataAccessException. This exception should include the
333: * sqlEx parameter as a nested root cause.
334: * @see CustomSQLErrorCodesTranslation#setExceptionClass
335: */
336: protected DataAccessException createCustomException(String task,
337: String sql, SQLException sqlEx, Class exceptionClass) {
338:
339: // find appropriate constructor
340: try {
341: int constructorType = 0;
342: Constructor[] constructors = exceptionClass
343: .getConstructors();
344: for (int i = 0; i < constructors.length; i++) {
345: Class[] parameterTypes = constructors[i]
346: .getParameterTypes();
347: if (parameterTypes.length == 1
348: && parameterTypes[0].equals(String.class)) {
349: if (constructorType < MESSAGE_ONLY_CONSTRUCTOR)
350: constructorType = MESSAGE_ONLY_CONSTRUCTOR;
351: }
352: if (parameterTypes.length == 2
353: && parameterTypes[0].equals(String.class)
354: && parameterTypes[1].equals(Throwable.class)) {
355: if (constructorType < MESSAGE_THROWABLE_CONSTRUCTOR)
356: constructorType = MESSAGE_THROWABLE_CONSTRUCTOR;
357: }
358: if (parameterTypes.length == 2
359: && parameterTypes[0].equals(String.class)
360: && parameterTypes[1].equals(SQLException.class)) {
361: if (constructorType < MESSAGE_SQLEX_CONSTRUCTOR)
362: constructorType = MESSAGE_SQLEX_CONSTRUCTOR;
363: }
364: if (parameterTypes.length == 3
365: && parameterTypes[0].equals(String.class)
366: && parameterTypes[1].equals(String.class)
367: && parameterTypes[2].equals(Throwable.class)) {
368: if (constructorType < MESSAGE_SQL_THROWABLE_CONSTRUCTOR)
369: constructorType = MESSAGE_SQL_THROWABLE_CONSTRUCTOR;
370: }
371: if (parameterTypes.length == 3
372: && parameterTypes[0].equals(String.class)
373: && parameterTypes[1].equals(String.class)
374: && parameterTypes[2].equals(SQLException.class)) {
375: if (constructorType < MESSAGE_SQL_SQLEX_CONSTRUCTOR)
376: constructorType = MESSAGE_SQL_SQLEX_CONSTRUCTOR;
377: }
378: }
379:
380: // invoke constructor
381: Constructor exceptionConstructor = null;
382: switch (constructorType) {
383: case MESSAGE_SQL_SQLEX_CONSTRUCTOR:
384: Class[] messageAndSqlAndSqlExArgsClass = new Class[] {
385: String.class, String.class, SQLException.class };
386: Object[] messageAndSqlAndSqlExArgs = new Object[] {
387: task, sql, sqlEx };
388: exceptionConstructor = exceptionClass
389: .getConstructor(messageAndSqlAndSqlExArgsClass);
390: return (DataAccessException) exceptionConstructor
391: .newInstance(messageAndSqlAndSqlExArgs);
392: case MESSAGE_SQL_THROWABLE_CONSTRUCTOR:
393: Class[] messageAndSqlAndThrowableArgsClass = new Class[] {
394: String.class, String.class, Throwable.class };
395: Object[] messageAndSqlAndThrowableArgs = new Object[] {
396: task, sql, sqlEx };
397: exceptionConstructor = exceptionClass
398: .getConstructor(messageAndSqlAndThrowableArgsClass);
399: return (DataAccessException) exceptionConstructor
400: .newInstance(messageAndSqlAndThrowableArgs);
401: case MESSAGE_SQLEX_CONSTRUCTOR:
402: Class[] messageAndSqlExArgsClass = new Class[] {
403: String.class, SQLException.class };
404: Object[] messageAndSqlExArgs = new Object[] {
405: task + ": " + sqlEx.getMessage(), sqlEx };
406: exceptionConstructor = exceptionClass
407: .getConstructor(messageAndSqlExArgsClass);
408: return (DataAccessException) exceptionConstructor
409: .newInstance(messageAndSqlExArgs);
410: case MESSAGE_THROWABLE_CONSTRUCTOR:
411: Class[] messageAndThrowableArgsClass = new Class[] {
412: String.class, Throwable.class };
413: Object[] messageAndThrowableArgs = new Object[] {
414: task + ": " + sqlEx.getMessage(), sqlEx };
415: exceptionConstructor = exceptionClass
416: .getConstructor(messageAndThrowableArgsClass);
417: return (DataAccessException) exceptionConstructor
418: .newInstance(messageAndThrowableArgs);
419: case MESSAGE_ONLY_CONSTRUCTOR:
420: Class[] messageOnlyArgsClass = new Class[] { String.class };
421: Object[] messageOnlyArgs = new Object[] { task + ": "
422: + sqlEx.getMessage() };
423: exceptionConstructor = exceptionClass
424: .getConstructor(messageOnlyArgsClass);
425: return (DataAccessException) exceptionConstructor
426: .newInstance(messageOnlyArgs);
427: default:
428: logger
429: .warn("Unable to find appropriate constructor of custom exception class ["
430: + exceptionClass.getName() + "]");
431: return null;
432: }
433: } catch (Throwable ex) {
434: if (logger.isWarnEnabled()) {
435: logger.warn(
436: "Unable to instantiate custom exception class ["
437: + exceptionClass.getName() + "]", ex);
438: }
439: return null;
440: }
441: }
442:
443: private void logTranslation(String task, String sql,
444: SQLException sqlEx, boolean custom) {
445: if (logger.isDebugEnabled()) {
446: String intro = custom ? "Custom translation of"
447: : "Translating";
448: logger.debug(intro + " SQLException with SQL state '"
449: + sqlEx.getSQLState() + "', error code '"
450: + sqlEx.getErrorCode() + "', message ["
451: + sqlEx.getMessage() + "]; SQL was [" + sql
452: + "] for task [" + task + "]");
453: }
454: }
455:
456: }
|