001/* 002 * Copyright 2002-2018 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 * https://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 017package org.springframework.jdbc.support; 018 019import java.lang.reflect.Constructor; 020import java.sql.BatchUpdateException; 021import java.sql.SQLException; 022import java.util.Arrays; 023import javax.sql.DataSource; 024 025import org.springframework.dao.CannotAcquireLockException; 026import org.springframework.dao.CannotSerializeTransactionException; 027import org.springframework.dao.DataAccessException; 028import org.springframework.dao.DataAccessResourceFailureException; 029import org.springframework.dao.DataIntegrityViolationException; 030import org.springframework.dao.DeadlockLoserDataAccessException; 031import org.springframework.dao.DuplicateKeyException; 032import org.springframework.dao.PermissionDeniedDataAccessException; 033import org.springframework.dao.TransientDataAccessResourceException; 034import org.springframework.jdbc.BadSqlGrammarException; 035import org.springframework.jdbc.InvalidResultSetAccessException; 036 037/** 038 * Implementation of {@link SQLExceptionTranslator} that analyzes vendor-specific error codes. 039 * More precise than an implementation based on SQL state, but heavily vendor-specific. 040 * 041 * <p>This class applies the following matching rules: 042 * <ul> 043 * <li>Try custom translation implemented by any subclass. Note that this class is 044 * concrete and is typically used itself, in which case this rule doesn't apply. 045 * <li>Apply error code matching. Error codes are obtained from the SQLErrorCodesFactory 046 * by default. This factory loads a "sql-error-codes.xml" file from the class path, 047 * defining error code mappings for database names from database meta-data. 048 * <li>Fallback to a fallback translator. {@link SQLStateSQLExceptionTranslator} is the 049 * default fallback translator, analyzing the exception's SQL state only. On Java 6 050 * which introduces its own {@code SQLException} subclass hierarchy, we will 051 * use {@link SQLExceptionSubclassTranslator} by default, which in turns falls back 052 * to Spring's own SQL state translation when not encountering specific subclasses. 053 * </ul> 054 * 055 * <p>The configuration file named "sql-error-codes.xml" is by default read from 056 * this package. It can be overridden through a file of the same name in the root 057 * of the class path (e.g. in the "/WEB-INF/classes" directory), as long as the 058 * Spring JDBC package is loaded from the same ClassLoader. 059 * 060 * @author Rod Johnson 061 * @author Thomas Risberg 062 * @author Juergen Hoeller 063 * @see SQLErrorCodesFactory 064 * @see SQLStateSQLExceptionTranslator 065 */ 066public class SQLErrorCodeSQLExceptionTranslator extends AbstractFallbackSQLExceptionTranslator { 067 068 private static final int MESSAGE_ONLY_CONSTRUCTOR = 1; 069 private static final int MESSAGE_THROWABLE_CONSTRUCTOR = 2; 070 private static final int MESSAGE_SQLEX_CONSTRUCTOR = 3; 071 private static final int MESSAGE_SQL_THROWABLE_CONSTRUCTOR = 4; 072 private static final int MESSAGE_SQL_SQLEX_CONSTRUCTOR = 5; 073 074 075 /** Error codes used by this translator */ 076 private SQLErrorCodes sqlErrorCodes; 077 078 079 /** 080 * Constructor for use as a JavaBean. 081 * The SqlErrorCodes or DataSource property must be set. 082 */ 083 public SQLErrorCodeSQLExceptionTranslator() { 084 setFallbackTranslator(new SQLExceptionSubclassTranslator()); 085 } 086 087 /** 088 * Create a SQL error code translator for the given DataSource. 089 * Invoking this constructor will cause a Connection to be obtained 090 * from the DataSource to get the meta-data. 091 * @param dataSource the DataSource to use to find meta-data and establish 092 * which error codes are usable 093 * @see SQLErrorCodesFactory 094 */ 095 public SQLErrorCodeSQLExceptionTranslator(DataSource dataSource) { 096 this(); 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 meta-data. 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 meta-data 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 meta-data. 128 * @param dataSource the DataSource to use to find meta-data 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 meta-data. 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 dae = customTranslate(task, sql, sqlEx); 180 if (dae != null) { 181 return dae; 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 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 // Next, look for grouped error codes. 228 if (Arrays.binarySearch(this.sqlErrorCodes.getBadSqlGrammarCodes(), errorCode) >= 0) { 229 logTranslation(task, sql, sqlEx, false); 230 return new BadSqlGrammarException(task, sql, sqlEx); 231 } 232 else if (Arrays.binarySearch(this.sqlErrorCodes.getInvalidResultSetAccessCodes(), errorCode) >= 0) { 233 logTranslation(task, sql, sqlEx, false); 234 return new InvalidResultSetAccessException(task, sql, sqlEx); 235 } 236 else if (Arrays.binarySearch(this.sqlErrorCodes.getDuplicateKeyCodes(), errorCode) >= 0) { 237 logTranslation(task, sql, sqlEx, false); 238 return new DuplicateKeyException(buildMessage(task, sql, sqlEx), sqlEx); 239 } 240 else if (Arrays.binarySearch(this.sqlErrorCodes.getDataIntegrityViolationCodes(), errorCode) >= 0) { 241 logTranslation(task, sql, sqlEx, false); 242 return new DataIntegrityViolationException(buildMessage(task, sql, sqlEx), sqlEx); 243 } 244 else if (Arrays.binarySearch(this.sqlErrorCodes.getPermissionDeniedCodes(), errorCode) >= 0) { 245 logTranslation(task, sql, sqlEx, false); 246 return new PermissionDeniedDataAccessException(buildMessage(task, sql, sqlEx), sqlEx); 247 } 248 else if (Arrays.binarySearch(this.sqlErrorCodes.getDataAccessResourceFailureCodes(), errorCode) >= 0) { 249 logTranslation(task, sql, sqlEx, false); 250 return new DataAccessResourceFailureException(buildMessage(task, sql, sqlEx), sqlEx); 251 } 252 else if (Arrays.binarySearch(this.sqlErrorCodes.getTransientDataAccessResourceCodes(), errorCode) >= 0) { 253 logTranslation(task, sql, sqlEx, false); 254 return new TransientDataAccessResourceException(buildMessage(task, sql, sqlEx), sqlEx); 255 } 256 else if (Arrays.binarySearch(this.sqlErrorCodes.getCannotAcquireLockCodes(), errorCode) >= 0) { 257 logTranslation(task, sql, sqlEx, false); 258 return new CannotAcquireLockException(buildMessage(task, sql, sqlEx), sqlEx); 259 } 260 else if (Arrays.binarySearch(this.sqlErrorCodes.getDeadlockLoserCodes(), errorCode) >= 0) { 261 logTranslation(task, sql, sqlEx, false); 262 return new DeadlockLoserDataAccessException(buildMessage(task, sql, sqlEx), sqlEx); 263 } 264 else if (Arrays.binarySearch(this.sqlErrorCodes.getCannotSerializeTransactionCodes(), errorCode) >= 0) { 265 logTranslation(task, sql, sqlEx, false); 266 return new CannotSerializeTransactionException(buildMessage(task, sql, sqlEx), sqlEx); 267 } 268 } 269 } 270 271 // We couldn't identify it more precisely - let's hand it over to the SQLState fallback translator. 272 if (logger.isDebugEnabled()) { 273 String codes; 274 if (this.sqlErrorCodes != null && this.sqlErrorCodes.isUseSqlStateForTranslation()) { 275 codes = "SQL state '" + sqlEx.getSQLState() + "', error code '" + sqlEx.getErrorCode(); 276 } 277 else { 278 codes = "Error code '" + sqlEx.getErrorCode() + "'"; 279 } 280 logger.debug("Unable to translate SQLException with " + codes + ", will now try the fallback translator"); 281 } 282 283 return null; 284 } 285 286 /** 287 * Subclasses can override this method to attempt a custom mapping from 288 * {@link SQLException} to {@link DataAccessException}. 289 * @param task readable text describing the task being attempted 290 * @param sql the SQL query or update that caused the problem (may be {@code null}) 291 * @param sqlEx the offending SQLException 292 * @return {@code null} if no custom translation applies, otherwise a {@link DataAccessException} 293 * resulting from custom translation. This exception should include the {@code sqlEx} parameter 294 * as a nested root cause. This implementation always returns {@code null}, meaning that the 295 * translator always falls back to the default error codes. 296 */ 297 protected DataAccessException customTranslate(String task, String sql, SQLException sqlEx) { 298 return null; 299 } 300 301 /** 302 * Create a custom {@link DataAccessException}, based on a given exception 303 * class from a {@link CustomSQLErrorCodesTranslation} definition. 304 * @param task readable text describing the task being attempted 305 * @param sql the SQL query or update that caused the problem (may be {@code null}) 306 * @param sqlEx the offending SQLException 307 * @param exceptionClass the exception class to use, as defined in the 308 * {@link CustomSQLErrorCodesTranslation} definition 309 * @return {@code null} if the custom exception could not be created, otherwise 310 * the resulting {@link DataAccessException}. This exception should include the 311 * {@code sqlEx} parameter as a nested root cause. 312 * @see CustomSQLErrorCodesTranslation#setExceptionClass 313 */ 314 protected DataAccessException createCustomException( 315 String task, String sql, SQLException sqlEx, Class<?> exceptionClass) { 316 317 // Find appropriate constructor for the given exception class 318 try { 319 int constructorType = 0; 320 Constructor<?>[] constructors = exceptionClass.getConstructors(); 321 for (Constructor<?> constructor : constructors) { 322 Class<?>[] parameterTypes = constructor.getParameterTypes(); 323 if (parameterTypes.length == 1 && String.class == parameterTypes[0] && 324 constructorType < MESSAGE_ONLY_CONSTRUCTOR) { 325 constructorType = MESSAGE_ONLY_CONSTRUCTOR; 326 } 327 if (parameterTypes.length == 2 && String.class == parameterTypes[0] && 328 Throwable.class == parameterTypes[1] && 329 constructorType < MESSAGE_THROWABLE_CONSTRUCTOR) { 330 constructorType = MESSAGE_THROWABLE_CONSTRUCTOR; 331 } 332 if (parameterTypes.length == 2 && String.class == parameterTypes[0] && 333 SQLException.class == parameterTypes[1] && 334 constructorType < MESSAGE_SQLEX_CONSTRUCTOR) { 335 constructorType = MESSAGE_SQLEX_CONSTRUCTOR; 336 } 337 if (parameterTypes.length == 3 && String.class == parameterTypes[0] && 338 String.class == parameterTypes[1] && Throwable.class == parameterTypes[2] && 339 constructorType < MESSAGE_SQL_THROWABLE_CONSTRUCTOR) { 340 constructorType = MESSAGE_SQL_THROWABLE_CONSTRUCTOR; 341 } 342 if (parameterTypes.length == 3 && String.class == parameterTypes[0] && 343 String.class == parameterTypes[1] && SQLException.class == parameterTypes[2] && 344 constructorType < MESSAGE_SQL_SQLEX_CONSTRUCTOR) { 345 constructorType = MESSAGE_SQL_SQLEX_CONSTRUCTOR; 346 } 347 } 348 349 // invoke constructor 350 Constructor<?> exceptionConstructor; 351 switch (constructorType) { 352 case MESSAGE_SQL_SQLEX_CONSTRUCTOR: 353 Class<?>[] messageAndSqlAndSqlExArgsClass = new Class<?>[] {String.class, String.class, SQLException.class}; 354 Object[] messageAndSqlAndSqlExArgs = new Object[] {task, sql, sqlEx}; 355 exceptionConstructor = exceptionClass.getConstructor(messageAndSqlAndSqlExArgsClass); 356 return (DataAccessException) exceptionConstructor.newInstance(messageAndSqlAndSqlExArgs); 357 case MESSAGE_SQL_THROWABLE_CONSTRUCTOR: 358 Class<?>[] messageAndSqlAndThrowableArgsClass = new Class<?>[] {String.class, String.class, Throwable.class}; 359 Object[] messageAndSqlAndThrowableArgs = new Object[] {task, sql, sqlEx}; 360 exceptionConstructor = exceptionClass.getConstructor(messageAndSqlAndThrowableArgsClass); 361 return (DataAccessException) exceptionConstructor.newInstance(messageAndSqlAndThrowableArgs); 362 case MESSAGE_SQLEX_CONSTRUCTOR: 363 Class<?>[] messageAndSqlExArgsClass = new Class<?>[] {String.class, SQLException.class}; 364 Object[] messageAndSqlExArgs = new Object[] {task + ": " + sqlEx.getMessage(), sqlEx}; 365 exceptionConstructor = exceptionClass.getConstructor(messageAndSqlExArgsClass); 366 return (DataAccessException) exceptionConstructor.newInstance(messageAndSqlExArgs); 367 case MESSAGE_THROWABLE_CONSTRUCTOR: 368 Class<?>[] messageAndThrowableArgsClass = new Class<?>[] {String.class, Throwable.class}; 369 Object[] messageAndThrowableArgs = new Object[] {task + ": " + sqlEx.getMessage(), sqlEx}; 370 exceptionConstructor = exceptionClass.getConstructor(messageAndThrowableArgsClass); 371 return (DataAccessException)exceptionConstructor.newInstance(messageAndThrowableArgs); 372 case MESSAGE_ONLY_CONSTRUCTOR: 373 Class<?>[] messageOnlyArgsClass = new Class<?>[] {String.class}; 374 Object[] messageOnlyArgs = new Object[] {task + ": " + sqlEx.getMessage()}; 375 exceptionConstructor = exceptionClass.getConstructor(messageOnlyArgsClass); 376 return (DataAccessException) exceptionConstructor.newInstance(messageOnlyArgs); 377 default: 378 if (logger.isWarnEnabled()) { 379 logger.warn("Unable to find appropriate constructor of custom exception class [" + 380 exceptionClass.getName() + "]"); 381 } 382 return null; 383 } 384 } 385 catch (Throwable ex) { 386 if (logger.isWarnEnabled()) { 387 logger.warn("Unable to instantiate custom exception class [" + exceptionClass.getName() + "]", ex); 388 } 389 return null; 390 } 391 } 392 393 private void logTranslation(String task, String sql, SQLException sqlEx, boolean custom) { 394 if (logger.isDebugEnabled()) { 395 String intro = custom ? "Custom translation of" : "Translating"; 396 logger.debug(intro + " SQLException with SQL state '" + sqlEx.getSQLState() + 397 "', error code '" + sqlEx.getErrorCode() + "', message [" + sqlEx.getMessage() + 398 "]; SQL was [" + sql + "] for task [" + task + "]"); 399 } 400 } 401 402}