diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/DefaultExceptionTranslatorExecuteListener.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/DefaultExceptionTranslatorExecuteListener.java new file mode 100644 index 00000000000..199a62dda0e --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/DefaultExceptionTranslatorExecuteListener.java @@ -0,0 +1,126 @@ +/* + * Copyright 2012-2024 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 + * + * https://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.boot.autoconfigure.jooq; + +import java.sql.SQLException; +import java.util.function.Function; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.jooq.ExecuteContext; +import org.jooq.SQLDialect; + +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator; +import org.springframework.jdbc.support.SQLExceptionTranslator; +import org.springframework.jdbc.support.SQLStateSQLExceptionTranslator; +import org.springframework.util.Assert; + +/** + * Default implementation of {@link ExceptionTranslatorExecuteListener} that delegates to + * an {@link SQLExceptionTranslator}. + * + * @author Lukas Eder + * @author Andreas Ahlenstorf + * @author Phillip Webb + * @author Stephane Nicoll + */ +final class DefaultExceptionTranslatorExecuteListener implements ExceptionTranslatorExecuteListener { + + // Based on the jOOQ-spring-example from https://github.com/jOOQ/jOOQ + + private static final Log defaultLogger = LogFactory.getLog(ExceptionTranslatorExecuteListener.class); + + private final Log logger; + + private Function translatorFactory; + + DefaultExceptionTranslatorExecuteListener() { + this(defaultLogger, new DefaultTranslatorFactory()); + } + + DefaultExceptionTranslatorExecuteListener(Function translatorFactory) { + this(defaultLogger, translatorFactory); + } + + DefaultExceptionTranslatorExecuteListener(Log logger) { + this(logger, new DefaultTranslatorFactory()); + } + + private DefaultExceptionTranslatorExecuteListener(Log logger, + Function translatorFactory) { + Assert.notNull(translatorFactory, "TranslatorFactory must not be null"); + this.logger = logger; + this.translatorFactory = translatorFactory; + } + + @Override + public void exception(ExecuteContext context) { + SQLExceptionTranslator translator = this.translatorFactory.apply(context); + // The exception() callback is not only triggered for SQL exceptions but also for + // "normal" exceptions. In those cases sqlException() returns null. + SQLException exception = context.sqlException(); + while (exception != null) { + handle(context, translator, exception); + exception = exception.getNextException(); + } + } + + /** + * Handle a single exception in the chain. SQLExceptions might be nested multiple + * levels deep. The outermost exception is usually the least interesting one ("Call + * getNextException to see the cause."). Therefore the innermost exception is + * propagated and all other exceptions are logged. + * @param context the execute context + * @param translator the exception translator + * @param exception the exception + */ + private void handle(ExecuteContext context, SQLExceptionTranslator translator, SQLException exception) { + DataAccessException translated = translator.translate("jOOQ", context.sql(), exception); + if (exception.getNextException() != null) { + this.logger.error("Execution of SQL statement failed.", (translated != null) ? translated : exception); + return; + } + if (translated != null) { + context.exception(translated); + } + } + + /** + * Default {@link SQLExceptionTranslator} factory that creates the translator based on + * the Spring DB name. + */ + private static final class DefaultTranslatorFactory implements Function { + + @Override + public SQLExceptionTranslator apply(ExecuteContext context) { + return apply(context.configuration().dialect()); + } + + private SQLExceptionTranslator apply(SQLDialect dialect) { + String dbName = getSpringDbName(dialect); + return (dbName != null) ? new SQLErrorCodeSQLExceptionTranslator(dbName) + : new SQLStateSQLExceptionTranslator(); + } + + private String getSpringDbName(SQLDialect dialect) { + return (dialect != null && dialect.thirdParty() != null) ? dialect.thirdParty().springDbName() : null; + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/ExceptionTranslatorExecuteListener.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/ExceptionTranslatorExecuteListener.java new file mode 100644 index 00000000000..eca548dfc33 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/ExceptionTranslatorExecuteListener.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012-2024 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 + * + * https://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.boot.autoconfigure.jooq; + +import java.sql.SQLException; +import java.util.function.Function; + +import org.jooq.ExecuteContext; +import org.jooq.ExecuteListener; +import org.jooq.impl.DefaultExecuteListenerProvider; + +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.support.SQLExceptionTranslator; + +/** + * An {@link ExecuteListener} used by the auto-configured + * {@link DefaultExecuteListenerProvider} to translate exceptions in the + * {@link ExecuteContext}. Most commonly used to translate {@link SQLException + * SQLExceptions} to Spring-specific {@link DataAccessException DataAccessExceptions} by + * adapting an existing {@link SQLExceptionTranslator}. + * + * @author Dennis Melzer + * @since 3.3.0 + * @see #DEFAULT + * @see #of(Function) + */ +public interface ExceptionTranslatorExecuteListener extends ExecuteListener { + + /** + * Default {@link ExceptionTranslatorExecuteListener} suitable for most applications. + */ + ExceptionTranslatorExecuteListener DEFAULT = new DefaultExceptionTranslatorExecuteListener(); + + /** + * Creates a new {@link ExceptionTranslatorExecuteListener} backed by an + * {@link SQLExceptionTranslator}. + * @param translatorFactory factory function used to create the + * {@link SQLExceptionTranslator} + * @return a new {@link ExceptionTranslatorExecuteListener} instance + */ + static ExceptionTranslatorExecuteListener of(Function translatorFactory) { + return new DefaultExceptionTranslatorExecuteListener(translatorFactory); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqAutoConfiguration.java index 72944ddf3f6..4580ed021cc 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 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. @@ -70,8 +70,15 @@ public class JooqAutoConfiguration { @Bean @Order(0) - public DefaultExecuteListenerProvider jooqExceptionTranslatorExecuteListenerProvider() { - return new DefaultExecuteListenerProvider(new JooqExceptionTranslator()); + public DefaultExecuteListenerProvider jooqExceptionTranslatorExecuteListenerProvider( + ExceptionTranslatorExecuteListener exceptionTranslatorExecuteListener) { + return new DefaultExecuteListenerProvider(exceptionTranslatorExecuteListener); + } + + @Bean + @ConditionalOnMissingBean(ExceptionTranslatorExecuteListener.class) + public ExceptionTranslatorExecuteListener jooqExceptionTranslator() { + return ExceptionTranslatorExecuteListener.DEFAULT; } @Configuration(proxyBeanMethods = false) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslator.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslator.java index 383da48c975..102bd59b056 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslator.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslator.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2024 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. @@ -18,80 +18,33 @@ package org.springframework.boot.autoconfigure.jooq; import java.sql.SQLException; -import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jooq.ExecuteContext; import org.jooq.ExecuteListener; -import org.jooq.SQLDialect; import org.springframework.dao.DataAccessException; -import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator; -import org.springframework.jdbc.support.SQLExceptionTranslator; -import org.springframework.jdbc.support.SQLStateSQLExceptionTranslator; /** - * Transforms {@link java.sql.SQLException} into a Spring-specific - * {@link DataAccessException}. + * Transforms {@link SQLException} into a Spring-specific {@link DataAccessException}. * * @author Lukas Eder * @author Andreas Ahlenstorf * @author Phillip Webb * @author Stephane Nicoll * @since 1.5.10 + * @deprecated since 3.3.0 for removal in 3.5.0 in favor of + * {@link ExceptionTranslatorExecuteListener#DEFAULT} or + * {@link ExceptionTranslatorExecuteListener#of} */ +@Deprecated(since = "3.3.0", forRemoval = true) public class JooqExceptionTranslator implements ExecuteListener { - // Based on the jOOQ-spring-example from https://github.com/jOOQ/jOOQ - - private static final Log logger = LogFactory.getLog(JooqExceptionTranslator.class); + private final DefaultExceptionTranslatorExecuteListener delegate = new DefaultExceptionTranslatorExecuteListener( + LogFactory.getLog(JooqExceptionTranslator.class)); @Override public void exception(ExecuteContext context) { - SQLExceptionTranslator translator = getTranslator(context); - // The exception() callback is not only triggered for SQL exceptions but also for - // "normal" exceptions. In those cases sqlException() returns null. - SQLException exception = context.sqlException(); - while (exception != null) { - handle(context, translator, exception); - exception = exception.getNextException(); - } - } - - private SQLExceptionTranslator getTranslator(ExecuteContext context) { - SQLDialect dialect = context.configuration().dialect(); - if (dialect != null && dialect.thirdParty() != null) { - String dbName = dialect.thirdParty().springDbName(); - if (dbName != null) { - return new SQLErrorCodeSQLExceptionTranslator(dbName); - } - } - return new SQLStateSQLExceptionTranslator(); - } - - /** - * Handle a single exception in the chain. SQLExceptions might be nested multiple - * levels deep. The outermost exception is usually the least interesting one ("Call - * getNextException to see the cause."). Therefore the innermost exception is - * propagated and all other exceptions are logged. - * @param context the execute context - * @param translator the exception translator - * @param exception the exception - */ - private void handle(ExecuteContext context, SQLExceptionTranslator translator, SQLException exception) { - DataAccessException translated = translate(context, translator, exception); - if (exception.getNextException() == null) { - if (translated != null) { - context.exception(translated); - } - } - else { - logger.error("Execution of SQL statement failed.", (translated != null) ? translated : exception); - } - } - - private DataAccessException translate(ExecuteContext context, SQLExceptionTranslator translator, - SQLException exception) { - return translator.translate("jOOQ", context.sql(), exception); + this.delegate.exception(context); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/DefaultExceptionTranslatorExecuteListenerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/DefaultExceptionTranslatorExecuteListenerTests.java new file mode 100644 index 00000000000..ae1278889d1 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/DefaultExceptionTranslatorExecuteListenerTests.java @@ -0,0 +1,112 @@ +/* + * Copyright 2012-2024 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 + * + * https://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.boot.autoconfigure.jooq; + +import java.sql.SQLException; +import java.util.function.Function; + +import org.jooq.Configuration; +import org.jooq.ExecuteContext; +import org.jooq.SQLDialect; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import org.springframework.jdbc.BadSqlGrammarException; +import org.springframework.jdbc.support.SQLExceptionTranslator; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.assertArg; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; + +/** + * Tests for {@link DefaultExceptionTranslatorExecuteListener}. + * + * @author Andy Wilkinson + * @author Phillip Webb + */ +class DefaultExceptionTranslatorExecuteListenerTests { + + private final ExceptionTranslatorExecuteListener listener = new DefaultExceptionTranslatorExecuteListener(); + + @Test + void createWhenTranslatorFactoryIsNullThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new DefaultExceptionTranslatorExecuteListener( + (Function) null)) + .withMessage("TranslatorFactory must not be null"); + } + + @ParameterizedTest(name = "{0}") + @MethodSource + void exceptionTranslatesSqlExceptions(SQLDialect dialect, SQLException sqlException) { + ExecuteContext context = mockContext(dialect, sqlException); + this.listener.exception(context); + then(context).should().exception(assertArg((ex) -> assertThat(ex).isInstanceOf(BadSqlGrammarException.class))); + } + + @Test + void exceptionWhenExceptionCannotBeTranslatedDoesNotCallExecuteContextException() { + ExecuteContext context = mockContext(SQLDialect.POSTGRES, new SQLException(null, null, 123456789)); + this.listener.exception(context); + then(context).should(never()).exception(any()); + } + + @Test + void exceptionWhenHasCustomTranslatorFactory() { + SQLExceptionTranslator translator = BadSqlGrammarException::new; + ExceptionTranslatorExecuteListener listener = new DefaultExceptionTranslatorExecuteListener( + (context) -> translator); + SQLException sqlException = sqlException(123); + ExecuteContext context = mockContext(SQLDialect.DUCKDB, sqlException); + listener.exception(context); + then(context).should().exception(assertArg((ex) -> assertThat(ex).isInstanceOf(BadSqlGrammarException.class))); + } + + private ExecuteContext mockContext(SQLDialect dialect, SQLException sqlException) { + ExecuteContext context = mock(ExecuteContext.class); + Configuration configuration = mock(Configuration.class); + given(context.configuration()).willReturn(configuration); + given(configuration.dialect()).willReturn(dialect); + given(context.sqlException()).willReturn(sqlException); + return context; + } + + static Object[] exceptionTranslatesSqlExceptions() { + return new Object[] { new Object[] { SQLDialect.DERBY, sqlException("42802") }, + new Object[] { SQLDialect.H2, sqlException(42000) }, + new Object[] { SQLDialect.HSQLDB, sqlException(-22) }, + new Object[] { SQLDialect.MARIADB, sqlException(1054) }, + new Object[] { SQLDialect.MYSQL, sqlException(1054) }, + new Object[] { SQLDialect.POSTGRES, sqlException("03000") }, + new Object[] { SQLDialect.SQLITE, sqlException("21000") } }; + } + + private static SQLException sqlException(String sqlState) { + return new SQLException(null, sqlState); + } + + private static SQLException sqlException(int vendorCode) { + return new SQLException(null, null, vendorCode); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/JooqAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/JooqAutoConfigurationTests.java index 1e78fee1d9a..a26b6334915 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/JooqAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/JooqAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 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. @@ -55,6 +55,7 @@ import static org.mockito.Mockito.mock; * @author Andy Wilkinson * @author Stephane Nicoll * @author Dmytro Nosan + * @author Dennis Melzer */ class JooqAutoConfigurationTests { @@ -180,6 +181,26 @@ class JooqAutoConfigurationTests { }); } + @Test + void jooqExceptionTranslatorProviderFromConfigurationCustomizerOverridesJooqExceptionTranslatorBean() { + this.contextRunner + .withUserConfiguration(JooqDataSourceConfiguration.class, CustomJooqExceptionTranslatorConfiguration.class) + .run((context) -> { + assertThat(context.getBean(ExceptionTranslatorExecuteListener.class)) + .isInstanceOf(CustomJooqExceptionTranslator.class); + assertThat(context.getBean(DefaultExecuteListenerProvider.class).provide()) + .isInstanceOf(CustomJooqExceptionTranslator.class); + }); + } + + @Test + void jooqWithDefaultJooqExceptionTranslator() { + this.contextRunner.withUserConfiguration(JooqDataSourceConfiguration.class).run((context) -> { + ExceptionTranslatorExecuteListener translator = context.getBean(ExceptionTranslatorExecuteListener.class); + assertThat(translator).isInstanceOf(DefaultExceptionTranslatorExecuteListener.class); + }); + } + @Test void transactionProviderFromConfigurationCustomizerOverridesTransactionProviderBean() { this.contextRunner @@ -254,6 +275,16 @@ class JooqAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class CustomJooqExceptionTranslatorConfiguration { + + @Bean + ExceptionTranslatorExecuteListener jooqExceptionTranslator() { + return new CustomJooqExceptionTranslator(); + } + + } + @Configuration(proxyBeanMethods = false) static class CustomTransactionProviderFromCustomizerConfiguration { @@ -303,4 +334,8 @@ class JooqAutoConfigurationTests { } + static class CustomJooqExceptionTranslator implements ExceptionTranslatorExecuteListener { + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslatorTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslatorTests.java index 5a9f6685f14..f53b34880b3 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslatorTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 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. @@ -40,6 +40,8 @@ import static org.mockito.Mockito.never; * * @author Andy Wilkinson */ +@Deprecated(since = "3.3.0") +@SuppressWarnings("removal") class JooqExceptionTranslatorTests { private final JooqExceptionTranslator exceptionTranslator = new JooqExceptionTranslator();