Project Spring (latest) [3/30/06 5:21 PM]
 
Coverage - org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslator.java
1  /*
2   * Copyright 2002-2006 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.Constructor;
20  import java.sql.SQLException;
21  import java.util.Arrays;
22  
23  import javax.sql.DataSource;
24  
25  import org.apache.commons.logging.Log;
26  import org.apache.commons.logging.LogFactory;
27  
28  import org.springframework.dao.CannotAcquireLockException;
29  import org.springframework.dao.CannotSerializeTransactionException;
30  import org.springframework.dao.DataAccessException;
31  import org.springframework.dao.DataAccessResourceFailureException;
32  import org.springframework.dao.DataIntegrityViolationException;
33  import org.springframework.dao.DeadlockLoserDataAccessException;
34  import org.springframework.jdbc.BadSqlGrammarException;
35  import org.springframework.jdbc.InvalidResultSetAccessException;
36  
37  /**
38   * Implementation of SQLExceptionTranslator that uses specific vendor codes.
39   * More precise than SQL state implementation, but vendor-specific.
40   *
41   * <p>This class applies the following matching rules:
42   * <ul>
43   * <li>Try custom translation implemented by any subclass. Note that this class is
44   * concrete and is typically used itself, in which case this rule doesn't apply.
45   * <li>Apply error code matching. Error codes are obtained from the SQLErrorCodesFactory
46   * by default. This factory loads a "sql-error-codes.xml" file from the class path,
47   * defining error code mappings for database names from database metadata.
48   * <li>Fallback to fallback translator. SQLStateSQLExceptionTranslator is the default
49   * fallback translator.
50   * </ul>
51   *
52   * @author Rod Johnson
53   * @author Thomas Risberg
54   * @author Juergen Hoeller
55   * @see SQLErrorCodesFactory
56   * @see SQLStateSQLExceptionTranslator
57   */
58  public class SQLErrorCodeSQLExceptionTranslator implements SQLExceptionTranslator {
59  
60      private static final int MESSAGE_ONLY_CONSTRUCTOR = 1;
61      private static final int MESSAGE_THROWABLE_CONSTRUCTOR = 2;
62      private static final int MESSAGE_SQLEX_CONSTRUCTOR = 3;
63      private static final int MESSAGE_SQL_THROWABLE_CONSTRUCTOR = 4;
64      private static final int MESSAGE_SQL_SQLEX_CONSTRUCTOR = 5;
65  
66  
67      /** Logger available to subclasses */
68 0x      protected final Log logger = LogFactory.getLog(getClass());
69  
70      /** Error codes used by this translator */
71      private SQLErrorCodes sqlErrorCodes;
72     
73      /** Fallback translator to use if SQL error code matching doesn't work */
74 0x      private SQLExceptionTranslator fallbackTranslator = new SQLStateSQLExceptionTranslator();
75  
76  
77      /**
78      * Constructor for use as a JavaBean.
79      * The SqlErrorCodes or DataSource property must be set.
80      */
81 0x      public SQLErrorCodeSQLExceptionTranslator() {
82 0x      }
83  
84      /**
85      * Create a SQL error code translator for the given DataSource.
86      * Invoking this constructor will cause a Connection to be obtained
87      * from the DataSource to get the metadata.
88      * @param dataSource DataSource to use to find metadata and establish
89      * which error codes are usable
90      * @see SQLErrorCodesFactory
91      */
92 0x      public SQLErrorCodeSQLExceptionTranslator(DataSource dataSource) {
93 0x          setDataSource(dataSource);
94 0x      }
95  
96      /**
97      * Create a SQL error code translator for the given database product name.
98      * Invoking this constructor will avoid obtaining a Connection from the
99      * DataSource to get the metadata.
100      * @param dbName the database product name that identifies the error codes entry
101      * @see SQLErrorCodesFactory
102      * @see java.sql.DatabaseMetaData#getDatabaseProductName()
103      */
104 0x      public SQLErrorCodeSQLExceptionTranslator(String dbName) {
105 0x          setDatabaseProductName(dbName);
106 0x      }
107  
108      /**
109      * Create a SQLErrorCode translator given these error codes.
110      * Does not require a database metadata lookup to be performed using a connection.
111      * @param sec error codes
112      */
113 0x      public SQLErrorCodeSQLExceptionTranslator(SQLErrorCodes sec) {
114 0x          this.sqlErrorCodes = sec;       
115 0x      }
116     
117      /**
118      * Set the DataSource for this translator.
119      * <p>Setting this property will cause a Connection to be obtained from
120      * the DataSource to get the metadata.
121      * @param dataSource DataSource to use to find metadata and establish
122      * which error codes are usable
123      * @see SQLErrorCodesFactory#getErrorCodes(javax.sql.DataSource)
124      * @see java.sql.DatabaseMetaData#getDatabaseProductName()
125      */
126      public void setDataSource(DataSource dataSource) {
127 0x          this.sqlErrorCodes = SQLErrorCodesFactory.getInstance().getErrorCodes(dataSource);
128 0x      }
129  
130      /**
131      * Set the database product name for this translator.
132      * <p>Setting this property will avoid obtaining a Connection from the DataSource
133      * to get the metadata.
134      * @param dbName the database product name that identifies the error codes entry
135      * @see SQLErrorCodesFactory#getErrorCodes(String)
136      * @see java.sql.DatabaseMetaData#getDatabaseProductName()
137      */
138      public void setDatabaseProductName(String dbName) {
139 0x          this.sqlErrorCodes = SQLErrorCodesFactory.getInstance().getErrorCodes(dbName);
140 0x      }
141  
142      /**
143      * Set custom error codes to be used for translation.
144      * @param sec custom error codes to use
145      */
146      public void setSqlErrorCodes(SQLErrorCodes sec) {
147 0x          this.sqlErrorCodes = sec;
148 0x      }
149  
150      /**
151      * Return the error codes used by this translator.
152      * Usually determined via a DataSource.
153      * @see #setDataSource
154      */
155      public SQLErrorCodes getSqlErrorCodes() {
156 0x          return sqlErrorCodes;
157      }
158  
159      /**
160      * Override the default SQL state fallback translator.
161      * @param fallback custom fallback exception translator to use if error code
162      * translation fails
163      * @see SQLStateSQLExceptionTranslator
164      */
165      public void setFallbackTranslator(SQLExceptionTranslator fallback) {
166 0x          this.fallbackTranslator = fallback;
167 0x      }
168  
169      /**
170      * Return the fallback exception translator.
171      */
172      public SQLExceptionTranslator getFallbackTranslator() {
173 0x          return fallbackTranslator;
174      }
175  
176  
177      public DataAccessException translate(String task, String sql, SQLException sqlEx) {
178 0/2 0x          if (task == null) {
179 0x              task = "";
180          }
181 0/2 0x          if (sql == null) {
182 0x              sql = "";
183          }
184         
185          // First, try custom translation from overridden method.
186 0x          DataAccessException dex = customTranslate(task, sql, sqlEx);
187 0/2 0x          if (dex != null) {
188 0x              return dex;
189          }
190  
191          // Check SQLErrorCodes with corresponding error code, if available.
192 0/2 0x          if (this.sqlErrorCodes != null) {
193 0x              String errorCode = null;
194 0/2 0x              if (this.sqlErrorCodes.isUseSqlStateForTranslation()) {
195 0x                  errorCode = sqlEx.getSQLState();
196 0x              }
197              else {
198 0x                  errorCode = Integer.toString(sqlEx.getErrorCode());
199              }
200  
201 0/2 0x              if (errorCode != null) {
202  
203                  // Look for defined custom translations first.
204 0x                  CustomSQLErrorCodesTranslation[] customTranslations = this.sqlErrorCodes.getCustomTranslations();
205 0/2 0x                  if (customTranslations != null) {
206 0/2 0x                      for (int i = 0; i < customTranslations.length; i++) {
207 0x                          CustomSQLErrorCodesTranslation customTranslation = customTranslations[i];
208 0/2 0x                          if (Arrays.binarySearch(customTranslation.getErrorCodes(), errorCode) >= 0) {
209 0/2 0x                              if (customTranslation.getExceptionClass() != null) {
210 0x                                  DataAccessException customException = createCustomException(
211                                          task, sql, sqlEx, customTranslation.getExceptionClass());
212 0/2 0x                                  if (customException != null) {
213 0x                                      logTranslation(task, sql, sqlEx, true);
214 0x                                      return customException;
215                                  }
216                              }
217                          }
218                      }
219                  }
220  
221                  // Next, look for grouped error codes.
222 0/2 0x                  if (Arrays.binarySearch(this.sqlErrorCodes.getBadSqlGrammarCodes(), errorCode) >= 0) {
223 0x                      logTranslation(task, sql, sqlEx, false);
224 0x                      return new BadSqlGrammarException(task, sql, sqlEx);
225                  }
226 0/2 0x                  else if (Arrays.binarySearch(this.sqlErrorCodes.getInvalidResultSetAccessCodes(), errorCode) >= 0) {
227 0x                      logTranslation(task, sql, sqlEx, false);
228 0x                      return new InvalidResultSetAccessException(task, sql, sqlEx);
229                  }
230 0/2 0x                  else if (Arrays.binarySearch(this.sqlErrorCodes.getDataAccessResourceFailureCodes(), errorCode) >= 0) {
231 0x                      logTranslation(task, sql, sqlEx, false);
232 0x                      return new DataAccessResourceFailureException(buildMessage(task, sql, sqlEx), sqlEx);
233                  }
234 0/2 0x                  else if (Arrays.binarySearch(this.sqlErrorCodes.getDataIntegrityViolationCodes(), errorCode) >= 0) {
235 0x                      logTranslation(task, sql, sqlEx, false);
236 0x                      return new DataIntegrityViolationException(buildMessage(task, sql, sqlEx), sqlEx);
237                  }
238 0/2 0x                  else if (Arrays.binarySearch(this.sqlErrorCodes.getCannotAcquireLockCodes(), errorCode) >= 0) {
239 0x                      logTranslation(task, sql, sqlEx, false);
240 0x                      return new CannotAcquireLockException(buildMessage(task, sql, sqlEx), sqlEx);
241                  }
242 0/2 0x                  else if (Arrays.binarySearch(this.sqlErrorCodes.getDeadlockLoserCodes(), errorCode) >= 0) {
243 0x                      logTranslation(task, sql, sqlEx, false);
244 0x                      return new DeadlockLoserDataAccessException(buildMessage(task, sql, sqlEx), sqlEx);
245                  }
246 0/2 0x                  else if (Arrays.binarySearch(this.sqlErrorCodes.getCannotSerializeTransactionCodes(), errorCode) >= 0) {
247 0x                      logTranslation(task, sql, sqlEx, false);
248 0x                      return new CannotSerializeTransactionException(buildMessage(task, sql, sqlEx), sqlEx);
249                  }
250              }
251          }
252  
253          // We couldn't identify it more precisely - let's hand it over to the SQLState fallback translator.
254 0/2 0x          if (logger.isDebugEnabled()) {
255 0x              logger.debug("Unable to translate SQLException with errorCode '" + sqlEx.getErrorCode() +
256                      "', will now try the fallback translator");
257          }
258 0x          return this.fallbackTranslator.translate(task, sql, sqlEx);
259      }
260  
261      /**
262      * Build a message String for the given SQLException.
263      * Called when creating an instance of a generic DataAccessException class.
264      * @param task readable text describing the task being attempted
265      * @param sql SQL query or update that caused the problem. May be <code>null</code>.
266      * @param sqlEx the offending SQLException
267      * @return the message String to use
268      */
269      protected String buildMessage(String task, String sql, SQLException sqlEx) {
270 0x          return task + "; SQL [" + sql + "]; " + sqlEx.getMessage();
271      }
272  
273      /**
274      * Subclasses can override this method to attempt a custom mapping from SQLException
275      * to DataAccessException.
276      * @param task readable text describing the task being attempted
277      * @param sql SQL query or update that caused the problem. May be <code>null</code>.
278      * @param sqlEx the offending SQLException
279      * @return null if no custom translation was possible, otherwise a DataAccessException
280      * resulting from custom translation. This exception should include the sqlEx parameter
281      * as a nested root cause. This implementation always returns null, meaning that
282      * the translator always falls back to the default error codes.
283      */
284      protected DataAccessException customTranslate(String task, String sql, SQLException sqlEx) {
285 0x          return null;
286      }
287  
288      /**
289      * Create a custom DataAccessException, based on a given exception
290      * class from a CustomSQLErrorCodesTranslation definition.
291      * @param task readable text describing the task being attempted
292      * @param sql SQL query or update that caused the problem. May be <code>null</code>.
293      * @param sqlEx the offending SQLException
294      * @param exceptionClass the exception class to use, as defined in the
295      * CustomSQLErrorCodesTranslation definition
296      * @return null if the custom exception could not be created, otherwise
297      * the resulting DataAccessException. This exception should include the
298      * sqlEx parameter as a nested root cause.
299      * @see CustomSQLErrorCodesTranslation#setExceptionClass
300      */
301      protected DataAccessException createCustomException(
302              String task, String sql, SQLException sqlEx, Class exceptionClass) {
303  
304          // find appropriate constructor
305          try {
306 0x              int constructorType = 0;
307 0x              Constructor[] constructors = exceptionClass.getConstructors();
308 0/2 0x              for (int i = 0; i < constructors.length; i++) {
309 0x                  Class[] parameterTypes = constructors[i].getParameterTypes();
310 0/4 0x                  if (parameterTypes.length == 1 && parameterTypes[0].equals(String.class)) {
311 0/2 0x                      if (constructorType < MESSAGE_ONLY_CONSTRUCTOR)
312 0x                          constructorType = MESSAGE_ONLY_CONSTRUCTOR;
313                  }
314 0/6 0x                  if (parameterTypes.length == 2 && parameterTypes[0].equals(String.class) &&
315                          parameterTypes[1].equals(Throwable.class)) {
316 0/2 0x                      if (constructorType < MESSAGE_THROWABLE_CONSTRUCTOR)
317 0x                          constructorType = MESSAGE_THROWABLE_CONSTRUCTOR;
318                  }
319 0/6 0x                  if (parameterTypes.length == 2 && parameterTypes[0].equals(String.class) &&
320                          parameterTypes[1].equals(SQLException.class)) {
321 0/2 0x                      if (constructorType < MESSAGE_SQLEX_CONSTRUCTOR)
322 0x                          constructorType = MESSAGE_SQLEX_CONSTRUCTOR;
323                  }
324 0/8 0x                  if (parameterTypes.length == 3 && parameterTypes[0].equals(String.class) &&
325                          parameterTypes[1].equals(String.class) && parameterTypes[2].equals(Throwable.class)) {
326 0/2 0x                      if (constructorType < MESSAGE_SQL_THROWABLE_CONSTRUCTOR)
327 0x                          constructorType = MESSAGE_SQL_THROWABLE_CONSTRUCTOR;
328                  }
329 0/8 0x                  if (parameterTypes.length == 3 && parameterTypes[0].equals(String.class) &&
330                          parameterTypes[1].equals(String.class) && parameterTypes[2].equals(SQLException.class)) {
331 0/2 0x                      if (constructorType < MESSAGE_SQL_SQLEX_CONSTRUCTOR)
332 0x                          constructorType = MESSAGE_SQL_SQLEX_CONSTRUCTOR;
333                  }
334              }
335  
336              // invoke constructor
337 0x              Constructor exceptionConstructor = null;
338 0x              switch (constructorType) {
339                  case MESSAGE_SQL_SQLEX_CONSTRUCTOR:
340 0x                      Class[] messageAndSqlAndSqlExArgsClass = new Class[] {String.class, String.class, SQLException.class};
341 0x                      Object[] messageAndSqlAndSqlExArgs = new Object[] {task, sql, sqlEx};
342 0x                      exceptionConstructor = exceptionClass.getConstructor(messageAndSqlAndSqlExArgsClass);
343 0x                      return (DataAccessException) exceptionConstructor.newInstance(messageAndSqlAndSqlExArgs);
344                  case MESSAGE_SQL_THROWABLE_CONSTRUCTOR:
345 0x                      Class[] messageAndSqlAndThrowableArgsClass = new Class[] {String.class, String.class, Throwable.class};
346 0x                      Object[] messageAndSqlAndThrowableArgs = new Object[] {task, sql, sqlEx};
347 0x                      exceptionConstructor = exceptionClass.getConstructor(messageAndSqlAndThrowableArgsClass);
348 0x                      return (DataAccessException) exceptionConstructor.newInstance(messageAndSqlAndThrowableArgs);
349                  case MESSAGE_SQLEX_CONSTRUCTOR:
350 0x                      Class[] messageAndSqlExArgsClass = new Class[] {String.class, SQLException.class};
351 0x                      Object[] messageAndSqlExArgs = new Object[] {task + ": " + sqlEx.getMessage(), sqlEx};
352 0x                      exceptionConstructor = exceptionClass.getConstructor(messageAndSqlExArgsClass);
353 0x                      return (DataAccessException) exceptionConstructor.newInstance(messageAndSqlExArgs);
354                  case MESSAGE_THROWABLE_CONSTRUCTOR:
355 0x                      Class[] messageAndThrowableArgsClass = new Class[] {String.class, Throwable.class};
356 0x                      Object[] messageAndThrowableArgs = new Object[] {task + ": " + sqlEx.getMessage(), sqlEx};
357 0x                      exceptionConstructor = exceptionClass.getConstructor(messageAndThrowableArgsClass);
358 0x                      return (DataAccessException)exceptionConstructor.newInstance(messageAndThrowableArgs);
359                  case MESSAGE_ONLY_CONSTRUCTOR:
360 0x                      Class[] messageOnlyArgsClass = new Class[] {String.class};
361 0x                      Object[] messageOnlyArgs = new Object[] {task + ": " + sqlEx.getMessage()};
362 0x                      exceptionConstructor = exceptionClass.getConstructor(messageOnlyArgsClass);
363 0x                      return (DataAccessException) exceptionConstructor.newInstance(messageOnlyArgs);
364                  default:
365 0x                      logger.warn("Unable to find appropriate constructor of custom exception class [" +
366                              exceptionClass.getName() + "]");
367 0x                      return null;
368                  }
369          }
370 0x          catch (Throwable ex) {
371 0/2 0x              if (logger.isWarnEnabled()) {
372 0x                  logger.warn("Unable to instantiate custom exception class [" + exceptionClass.getName() + "]", ex);
373              }
374 0x              return null;
375          }
376      }
377  
378      private void logTranslation(String task, String sql, SQLException sqlEx, boolean custom) {
379 0/2 0x          if (logger.isDebugEnabled()) {
380 0/2 0x              String intro = custom ? "Custom translation of" : "Translating";
381 0x              logger.debug(intro + " SQLException with SQLState '" + sqlEx.getSQLState() +
382                      "' and errorCode '" + sqlEx.getErrorCode() + "' and message [" + sqlEx.getMessage() +
383                      "]; SQL was [" + sql + "] for task [" + task + "]");
384          }
385 0x      }
386  
387  }