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