8 changed files with 330 additions and 119 deletions
@ -0,0 +1,126 @@
@@ -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<ExecuteContext, SQLExceptionTranslator> translatorFactory; |
||||
|
||||
DefaultExceptionTranslatorExecuteListener() { |
||||
this(defaultLogger, new DefaultTranslatorFactory()); |
||||
} |
||||
|
||||
DefaultExceptionTranslatorExecuteListener(Function<ExecuteContext, SQLExceptionTranslator> translatorFactory) { |
||||
this(defaultLogger, translatorFactory); |
||||
} |
||||
|
||||
DefaultExceptionTranslatorExecuteListener(Log logger) { |
||||
this(logger, new DefaultTranslatorFactory()); |
||||
} |
||||
|
||||
private DefaultExceptionTranslatorExecuteListener(Log logger, |
||||
Function<ExecuteContext, SQLExceptionTranslator> 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<ExecuteContext, SQLExceptionTranslator> { |
||||
|
||||
@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; |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,59 @@
@@ -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<ExecuteContext, SQLExceptionTranslator> translatorFactory) { |
||||
return new DefaultExceptionTranslatorExecuteListener(translatorFactory); |
||||
} |
||||
|
||||
} |
||||
@ -1,38 +0,0 @@
@@ -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); |
||||
} |
||||
@ -0,0 +1,112 @@
@@ -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<ExecuteContext, SQLExceptionTranslator>) 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); |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue