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}