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