From c3aa95335a4401e0ef77d511df0cdd13b9a841bc Mon Sep 17 00:00:00 2001 From: Dennis Melzer Date: Wed, 13 Dec 2023 11:02:28 +0100 Subject: [PATCH 1/2] Add conditional bean for jOOQ translator Introduce an jOOQ `ExecuteListener` sub-interface specifically for exception translation with the auto-configured `DefaultExecuteListenerProvider` instance. Users can now define a bean that implements the interface or omit it and continue to use the existing exception translation logic. See gh-38762 --- .../jooq/JooqAutoConfiguration.java | 10 ++++- .../jooq/JooqExceptionTranslator.java | 5 +-- .../jooq/JooqExceptionTranslatorListener.java | 38 ++++++++++++++++++ .../jooq/JooqAutoConfigurationTests.java | 40 +++++++++++++++++++ 4 files changed, 88 insertions(+), 5 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslatorListener.java 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..26b882e811a 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 @@ -70,8 +70,14 @@ public class JooqAutoConfiguration { @Bean @Order(0) - public DefaultExecuteListenerProvider jooqExceptionTranslatorExecuteListenerProvider() { - return new DefaultExecuteListenerProvider(new JooqExceptionTranslator()); + public DefaultExecuteListenerProvider jooqExceptionTranslatorExecuteListenerProvider(JooqExceptionTranslatorListener jooqExceptionTranslator) { + return new DefaultExecuteListenerProvider(jooqExceptionTranslator); + } + + @Bean + @ConditionalOnMissingBean(JooqExceptionTranslatorListener.class) + public JooqExceptionTranslatorListener jooqExceptionTranslator() { + return new JooqExceptionTranslator(); } @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..b27d99213c8 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 @@ -21,7 +21,6 @@ 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; @@ -30,7 +29,7 @@ import org.springframework.jdbc.support.SQLExceptionTranslator; import org.springframework.jdbc.support.SQLStateSQLExceptionTranslator; /** - * Transforms {@link java.sql.SQLException} into a Spring-specific + * Transforms {@link SQLException} into a Spring-specific * {@link DataAccessException}. * * @author Lukas Eder @@ -39,7 +38,7 @@ import org.springframework.jdbc.support.SQLStateSQLExceptionTranslator; * @author Stephane Nicoll * @since 1.5.10 */ -public class JooqExceptionTranslator implements ExecuteListener { +public class JooqExceptionTranslator implements JooqExceptionTranslatorListener { // Based on the jOOQ-spring-example from https://github.com/jOOQ/jOOQ diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslatorListener.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslatorListener.java new file mode 100644 index 00000000000..a02b8508f21 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslatorListener.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2022 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 org.jooq.ExecuteContext; +import org.jooq.ExecuteListener; + +import org.springframework.dao.DataAccessException; + +/** + * A {@link ExecuteListener} which can transforms or translate {@link Exception} into a Spring-specific + * {@link DataAccessException}. + * + * @author Dennis Melzer + * @since 3.2.1 + */ +public interface JooqExceptionTranslatorListener extends ExecuteListener { + + /** + * Override the given {@link Exception} from {@link ExecuteContext} into a generic {@link DataAccessException}. + * @param context The context containing information about the execution. + */ + void exception(ExecuteContext context); +} 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..d2c41e3196c 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 @@ -22,6 +22,7 @@ import org.jooq.CharsetProvider; import org.jooq.ConnectionProvider; import org.jooq.ConverterProvider; import org.jooq.DSLContext; +import org.jooq.ExecuteContext; import org.jooq.ExecuteListener; import org.jooq.ExecuteListenerProvider; import org.jooq.SQLDialect; @@ -55,6 +56,7 @@ import static org.mockito.Mockito.mock; * @author Andy Wilkinson * @author Stephane Nicoll * @author Dmytro Nosan + * @author Dennis Melzer */ class JooqAutoConfigurationTests { @@ -180,6 +182,25 @@ class JooqAutoConfigurationTests { }); } + @Test + void jooqExceptionTranslatorProviderFromConfigurationCustomizerOverridesJooqExceptionTranslatorBean() { + this.contextRunner + .withUserConfiguration(JooqDataSourceConfiguration.class, CustomJooqExceptionTranslatorConfiguration.class) + .run((context) -> { + JooqExceptionTranslatorListener translator = context.getBean(JooqExceptionTranslatorListener.class); + assertThat(translator).isInstanceOf(CustomJooqExceptionTranslator.class); + }); + } + + @Test + void jooqWithDefaultJooqExceptionTranslator() { + this.contextRunner.withUserConfiguration(JooqDataSourceConfiguration.class).run((context) -> { + JooqExceptionTranslatorListener translator = context.getBean(JooqExceptionTranslatorListener.class); + assertThat(translator).isInstanceOf(JooqExceptionTranslator.class); + }); + } + + @Test void transactionProviderFromConfigurationCustomizerOverridesTransactionProviderBean() { this.contextRunner @@ -254,6 +275,16 @@ class JooqAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class CustomJooqExceptionTranslatorConfiguration { + + @Bean + JooqExceptionTranslatorListener jooqExceptionTranslator() { + return new CustomJooqExceptionTranslator(); + } + + } + @Configuration(proxyBeanMethods = false) static class CustomTransactionProviderFromCustomizerConfiguration { @@ -303,4 +334,13 @@ class JooqAutoConfigurationTests { } + static class CustomJooqExceptionTranslator implements JooqExceptionTranslatorListener { + + + @Override + public void exception(ExecuteContext context) { + + } + } + } From 4de91094cf18fe615feb96ac3467761533dcfcb3 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 22 Jan 2024 15:47:55 -0800 Subject: [PATCH 2/2] Polish 'Add conditional bean for jOOQ translator' See gh-38762 --- ...ultExceptionTranslatorExecuteListener.java | 126 ++++++++++++++++++ .../ExceptionTranslatorExecuteListener.java | 59 ++++++++ .../jooq/JooqAutoConfiguration.java | 13 +- .../jooq/JooqExceptionTranslator.java | 68 ++-------- .../jooq/JooqExceptionTranslatorListener.java | 38 ------ ...ceptionTranslatorExecuteListenerTests.java | 112 ++++++++++++++++ .../jooq/JooqAutoConfigurationTests.java | 29 ++-- .../jooq/JooqExceptionTranslatorTests.java | 4 +- 8 files changed, 330 insertions(+), 119 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/DefaultExceptionTranslatorExecuteListener.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/ExceptionTranslatorExecuteListener.java delete mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslatorListener.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/DefaultExceptionTranslatorExecuteListenerTests.java 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 26b882e811a..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,14 +70,15 @@ public class JooqAutoConfiguration { @Bean @Order(0) - public DefaultExecuteListenerProvider jooqExceptionTranslatorExecuteListenerProvider(JooqExceptionTranslatorListener jooqExceptionTranslator) { - return new DefaultExecuteListenerProvider(jooqExceptionTranslator); + public DefaultExecuteListenerProvider jooqExceptionTranslatorExecuteListenerProvider( + ExceptionTranslatorExecuteListener exceptionTranslatorExecuteListener) { + return new DefaultExecuteListenerProvider(exceptionTranslatorExecuteListener); } @Bean - @ConditionalOnMissingBean(JooqExceptionTranslatorListener.class) - public JooqExceptionTranslatorListener jooqExceptionTranslator() { - return new JooqExceptionTranslator(); + @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 b27d99213c8..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,79 +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.SQLDialect; +import org.jooq.ExecuteListener; 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 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} */ -public class JooqExceptionTranslator implements JooqExceptionTranslatorListener { +@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/main/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslatorListener.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslatorListener.java deleted file mode 100644 index a02b8508f21..00000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslatorListener.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2012-2022 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 org.jooq.ExecuteContext; -import org.jooq.ExecuteListener; - -import org.springframework.dao.DataAccessException; - -/** - * A {@link ExecuteListener} which can transforms or translate {@link Exception} into a Spring-specific - * {@link DataAccessException}. - * - * @author Dennis Melzer - * @since 3.2.1 - */ -public interface JooqExceptionTranslatorListener extends ExecuteListener { - - /** - * Override the given {@link Exception} from {@link ExecuteContext} into a generic {@link DataAccessException}. - * @param context The context containing information about the execution. - */ - void exception(ExecuteContext 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 d2c41e3196c..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. @@ -22,7 +22,6 @@ import org.jooq.CharsetProvider; import org.jooq.ConnectionProvider; import org.jooq.ConverterProvider; import org.jooq.DSLContext; -import org.jooq.ExecuteContext; import org.jooq.ExecuteListener; import org.jooq.ExecuteListenerProvider; import org.jooq.SQLDialect; @@ -185,22 +184,23 @@ class JooqAutoConfigurationTests { @Test void jooqExceptionTranslatorProviderFromConfigurationCustomizerOverridesJooqExceptionTranslatorBean() { this.contextRunner - .withUserConfiguration(JooqDataSourceConfiguration.class, CustomJooqExceptionTranslatorConfiguration.class) - .run((context) -> { - JooqExceptionTranslatorListener translator = context.getBean(JooqExceptionTranslatorListener.class); - assertThat(translator).isInstanceOf(CustomJooqExceptionTranslator.class); - }); + .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) -> { - JooqExceptionTranslatorListener translator = context.getBean(JooqExceptionTranslatorListener.class); - assertThat(translator).isInstanceOf(JooqExceptionTranslator.class); + ExceptionTranslatorExecuteListener translator = context.getBean(ExceptionTranslatorExecuteListener.class); + assertThat(translator).isInstanceOf(DefaultExceptionTranslatorExecuteListener.class); }); } - @Test void transactionProviderFromConfigurationCustomizerOverridesTransactionProviderBean() { this.contextRunner @@ -279,7 +279,7 @@ class JooqAutoConfigurationTests { static class CustomJooqExceptionTranslatorConfiguration { @Bean - JooqExceptionTranslatorListener jooqExceptionTranslator() { + ExceptionTranslatorExecuteListener jooqExceptionTranslator() { return new CustomJooqExceptionTranslator(); } @@ -334,13 +334,8 @@ class JooqAutoConfigurationTests { } - static class CustomJooqExceptionTranslator implements JooqExceptionTranslatorListener { + static class CustomJooqExceptionTranslator implements ExceptionTranslatorExecuteListener { - - @Override - public void exception(ExecuteContext context) { - - } } } 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();