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}