diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/CustomSQLExceptionTranslatorRegistrar.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/CustomSQLExceptionTranslatorRegistrar.java new file mode 100644 index 0000000000..e9806b8c0b --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/CustomSQLExceptionTranslatorRegistrar.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.jdbc.support; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.InitializingBean; + +/** + * Registry for registering custom {@link org.springframework.jdbc.support.SQLExceptionTranslator}. + * + * @author Thomas Risberg + * @since 3.1 + */ +public class CustomSQLExceptionTranslatorRegistrar implements InitializingBean { + + private static final Log logger = LogFactory.getLog(CustomSQLExceptionTranslatorRegistrar.class); + + /** + * Map registry to hold custom translators specific databases. + * Key is the database product name as defined in the + * {@link org.springframework.jdbc.support.SQLErrorCodesFactory}. + */ + private final Map sqlExceptionTranslators = + new HashMap(); + + /** + * Setter for a Map of translators where the key must be the database name as defined in the + * sql-error-codes.xml file. This method is used when this registry is used in an application context. + *

Note that any existing translators will remain unless there is a match in the database name at which + * point the new translator will replace the existing one. + * + * @param sqlExceptionTranslators + */ + public void setSqlExceptionTranslators(Map sqlExceptionTranslators) { + this.sqlExceptionTranslators.putAll(sqlExceptionTranslators); + } + + public void afterPropertiesSet() throws Exception { + if (logger.isDebugEnabled()) { + logger.debug("Registering custom SQL exception translators for database(s): " + + sqlExceptionTranslators.keySet()); + } + for (String dbName : sqlExceptionTranslators.keySet()) { + CustomSQLExceptionTranslatorRegistry.getInstance() + .registerSqlExceptionTranslator(dbName, sqlExceptionTranslators.get(dbName)); + } + } +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/CustomSQLExceptionTranslatorRegistry.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/CustomSQLExceptionTranslatorRegistry.java new file mode 100644 index 0000000000..89db6b8902 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/CustomSQLExceptionTranslatorRegistry.java @@ -0,0 +1,102 @@ +/* + * Copyright 2002-2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.jdbc.support; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.WeakHashMap; +import javax.sql.DataSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.util.Assert; +import org.springframework.util.PatternMatchUtils; + +/** + * Registry for custom {@link org.springframework.jdbc.support.SQLExceptionTranslator} instances associated with + * specific databases allowing for overriding translation based on values contained in the configuration file + * named "sql-error-codes.xml". + * + * @author Thomas Risberg + * @since 3.1 + * @see SQLErrorCodesFactory + */ +public class CustomSQLExceptionTranslatorRegistry { + + private static final Log logger = LogFactory.getLog(CustomSQLExceptionTranslatorRegistry.class); + + /** + * Map registry to hold custom translators specific databases. + * Key is the database product name as defined in the + * {@link org.springframework.jdbc.support.SQLErrorCodesFactory}. + */ + private final Map sqlExceptionTranslatorRegistry = + new HashMap(); + + + /** + * Keep track of a single instance so we can return it to classes that request it. + */ + private static final CustomSQLExceptionTranslatorRegistry instance = new CustomSQLExceptionTranslatorRegistry(); + + + /** + * Return the singleton instance. + */ + public static CustomSQLExceptionTranslatorRegistry getInstance() { + return instance; + } + + + /** + * Create a new instance of the {@link org.springframework.jdbc.support.CustomSQLExceptionTranslatorRegistry} class. + *

Not public to enforce Singleton design pattern. + */ + private CustomSQLExceptionTranslatorRegistry() { + } + + /** + * Register a new custom translator for the specified database name. + * + * @param dbName the database name + * @param sqlExceptionTranslator the custom translator + */ + public void registerSqlExceptionTranslator(String dbName, SQLExceptionTranslator sqlExceptionTranslator) { + SQLExceptionTranslator replaced = sqlExceptionTranslatorRegistry.put(dbName, sqlExceptionTranslator); + if (replaced != null) { + logger.warn("Replacing custom translator '" + replaced + + "' for database " + dbName + + " with '" + sqlExceptionTranslator + "'"); + } + else { + logger.info("Adding custom translator '" + sqlExceptionTranslator.getClass().getSimpleName() + + "' for database " + dbName); + } + } + + public SQLExceptionTranslator findSqlExceptionTranslatorForDatabase(String dbName) { + return sqlExceptionTranslatorRegistry.get(dbName); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodes.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodes.java index c7d49126ab..8dde372bec 100644 --- a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodes.java +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodes.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2008 the original author or authors. + * Copyright 2002-2012 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -104,6 +104,10 @@ public class SQLErrorCodes { return customSqlExceptionTranslator; } + public void setCustomSqlExceptionTranslator(SQLExceptionTranslator customSqlExceptionTranslator) { + this.customSqlExceptionTranslator = customSqlExceptionTranslator; + } + public void setCustomSqlExceptionTranslatorClass(Class customSqlExceptionTranslatorClass) { if (customSqlExceptionTranslatorClass != null) { try { diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodesFactory.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodesFactory.java index 4b984e7f57..bdaf16c0e1 100644 --- a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodesFactory.java +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodesFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * Copyright 2002-2012 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.jdbc.support; import java.util.Collections; +import java.util.HashMap; import java.util.Map; import java.util.WeakHashMap; import javax.sql.DataSource; @@ -130,7 +131,7 @@ public class SQLErrorCodesFactory { logger.warn("Error loading SQL error codes from config file", ex); errorCodes = Collections.emptyMap(); } - + this.errorCodesMap = errorCodes; } @@ -159,7 +160,7 @@ public class SQLErrorCodesFactory { */ public SQLErrorCodes getErrorCodes(String dbName) { Assert.notNull(dbName, "Database product name must not be null"); - + SQLErrorCodes sec = this.errorCodesMap.get(dbName); if (sec == null) { for (SQLErrorCodes candidate : this.errorCodesMap.values()) { @@ -170,6 +171,7 @@ public class SQLErrorCodesFactory { } } if (sec != null) { + checkSqlExceptionTranslatorRegistry(dbName, sec); if (logger.isDebugEnabled()) { logger.debug("SQL error codes for '" + dbName + "' found"); } @@ -246,4 +248,24 @@ public class SQLErrorCodesFactory { } } + private void checkSqlExceptionTranslatorRegistry(String dbName, SQLErrorCodes dbCodes) { + // Check the custom sql exception translator registry for any entries + SQLExceptionTranslator customTranslator = + CustomSQLExceptionTranslatorRegistry.getInstance().findSqlExceptionTranslatorForDatabase(dbName); + if (customTranslator != null) { + if (dbCodes.getCustomSqlExceptionTranslator() != null) { + logger.warn("Overriding already defined custom translator '" + + dbCodes.getCustomSqlExceptionTranslator().getClass().getSimpleName() + + " with '" + customTranslator.getClass().getSimpleName() + + "' found in the CustomSQLExceptionTranslatorRegistry for database " + dbName); + } + else { + logger.info("Using custom translator '" + customTranslator.getClass().getSimpleName() + + "' found in the CustomSQLExceptionTranslatorRegistry for database " + dbName); + } + dbCodes.setCustomSqlExceptionTranslator(customTranslator); + } + + } + } diff --git a/org.springframework.jdbc/src/test/java/org/springframework/jdbc/support/CustomSQLExceptionTranslatorRegistrarTests.java b/org.springframework.jdbc/src/test/java/org/springframework/jdbc/support/CustomSQLExceptionTranslatorRegistrarTests.java new file mode 100644 index 0000000000..21ed920479 --- /dev/null +++ b/org.springframework.jdbc/src/test/java/org/springframework/jdbc/support/CustomSQLExceptionTranslatorRegistrarTests.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.jdbc.support; + +import java.sql.SQLException; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.TransientDataAccessResourceException; +import org.springframework.jdbc.BadSqlGrammarException; + +import static org.junit.Assert.*; + +/** + * Tests for custom translator. + * + * @author Thomas Risberg + */ +public class CustomSQLExceptionTranslatorRegistrarTests { + + @Before + public void setUp() { + new ClassPathXmlApplicationContext("test-custom-translators-context.xml", + CustomSQLExceptionTranslatorRegistrarTests.class); + } + + @Test + public void testCustomErrorCodeTranslation() { + + SQLErrorCodes codes = SQLErrorCodesFactory.getInstance().getErrorCodes("H2"); + SQLErrorCodeSQLExceptionTranslator sext = new SQLErrorCodeSQLExceptionTranslator(); + sext.setSqlErrorCodes(codes); + + DataAccessException exFor4200 = sext.doTranslate("", "", new SQLException("Ouch", "42000", 42000)); + assertNotNull("Should have been translated", exFor4200); + assertTrue("Should have been instance of BadSqlGrammarException", + BadSqlGrammarException.class.isAssignableFrom(exFor4200.getClass())); + + DataAccessException exFor2 = sext.doTranslate("", "", new SQLException("Ouch", "42000", 2)); + assertNotNull("Should have been translated", exFor2); + assertTrue("Should have been instance of TransientDataAccessResourceException", + TransientDataAccessResourceException.class.isAssignableFrom(exFor2.getClass())); + + DataAccessException exFor3 = sext.doTranslate("", "", new SQLException("Ouch", "42000", 3)); + assertNull("Should not have been translated", exFor3); + } + +} diff --git a/org.springframework.jdbc/src/test/resources/org/springframework/jdbc/support/test-custom-translators-context.xml b/org.springframework.jdbc/src/test/resources/org/springframework/jdbc/support/test-custom-translators-context.xml new file mode 100644 index 0000000000..df3a6258da --- /dev/null +++ b/org.springframework.jdbc/src/test/resources/org/springframework/jdbc/support/test-custom-translators-context.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + diff --git a/org.springframework.transaction/src/main/java/org/springframework/dao/QueryTimeoutException.java b/org.springframework.transaction/src/main/java/org/springframework/dao/QueryTimeoutException.java new file mode 100644 index 0000000000..5df9cb3c3f --- /dev/null +++ b/org.springframework.transaction/src/main/java/org/springframework/dao/QueryTimeoutException.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.dao; + +/** + * Exception to be thrown on a query timeout. This could have different causes depending on + * the database API in use but most likely thrown after the database interrupts or stops + * the processing of a query before it has completed. + * + *

This exception can be thrown by user code trapping the native database exception or + * by exception translation. + * + * @author Thomas Risberg + * @since 3.1 + */ +public class QueryTimeoutException extends TransientDataAccessException { + + /** + * Constructor for QueryTimeoutException. + * @param msg the detail message + */ + public QueryTimeoutException(String msg) { + super(msg); + } + + /** + * Constructor for QueryTimeoutException. + * @param msg the detail message + * @param cause the root cause from the data access API in use + */ + public QueryTimeoutException(String msg, Throwable cause) { + super(msg, cause); + } + +}