* To be called by translator subclasses when creating an instance of a generic * {@link org.springframework.dao.DataAccessException} class. @@ -110,6 +111,6 @@ public abstract class AbstractFallbackR2dbcExceptionTranslator implements R2dbcE * @return the message {@code String} to use. */ protected String buildMessage(String task, @Nullable String sql, R2dbcException ex) { - return task + "; " + (sql != null ? "SQL [" + sql : "]; " + "") + ex.getMessage(); + return task + "; " + (sql != null ? "SQL [" + sql + "]; " : "") + ex.getMessage(); } } diff --git a/src/main/java/org/springframework/data/r2dbc/support/R2dbcExceptionSubclassTranslator.java b/src/main/java/org/springframework/data/r2dbc/support/R2dbcExceptionSubclassTranslator.java new file mode 100644 index 0000000..16ed7a6 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/support/R2dbcExceptionSubclassTranslator.java @@ -0,0 +1,91 @@ +/* + * Copyright 2019 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.data.r2dbc.support; + +import io.r2dbc.spi.R2dbcBadGrammarException; +import io.r2dbc.spi.R2dbcDataIntegrityViolationException; +import io.r2dbc.spi.R2dbcException; +import io.r2dbc.spi.R2dbcNonTransientException; +import io.r2dbc.spi.R2dbcNonTransientResourceException; +import io.r2dbc.spi.R2dbcPermissionDeniedException; +import io.r2dbc.spi.R2dbcRollbackException; +import io.r2dbc.spi.R2dbcTimeoutException; +import io.r2dbc.spi.R2dbcTransientException; +import io.r2dbc.spi.R2dbcTransientResourceException; + +import org.springframework.dao.ConcurrencyFailureException; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.PermissionDeniedDataAccessException; +import org.springframework.dao.QueryTimeoutException; +import org.springframework.dao.TransientDataAccessResourceException; +import org.springframework.data.r2dbc.BadSqlGrammarException; +import org.springframework.lang.Nullable; + +/** + * {@link R2dbcExceptionTranslator} implementation which analyzes the specific {@link R2dbcException} subclass thrown by + * the R2DBC driver. + *
+ * Falls back to a standard {@link SqlStateR2dbcExceptionTranslator}. + * + * @author Mark Paluch + */ +public class R2dbcExceptionSubclassTranslator extends AbstractFallbackR2dbcExceptionTranslator { + + public R2dbcExceptionSubclassTranslator() { + setFallbackTranslator(new SqlStateR2dbcExceptionTranslator()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.support.AbstractFallbackR2dbcExceptionTranslator#doTranslate(java.lang.String, java.lang.String, io.r2dbc.spi.R2dbcException) + */ + @Override + @Nullable + protected DataAccessException doTranslate(String task, @Nullable String sql, R2dbcException ex) { + + if (ex instanceof R2dbcTransientException) { + if (ex instanceof R2dbcTransientResourceException) { + return new TransientDataAccessResourceException(buildMessage(task, sql, ex), ex); + } + if (ex instanceof R2dbcRollbackException) { + return new ConcurrencyFailureException(buildMessage(task, sql, ex), ex); + } + if (ex instanceof R2dbcTimeoutException) { + return new QueryTimeoutException(buildMessage(task, sql, ex), ex); + } + } + + if (ex instanceof R2dbcNonTransientException) { + if (ex instanceof R2dbcNonTransientResourceException) { + return new DataAccessResourceFailureException(buildMessage(task, sql, ex), ex); + } + if (ex instanceof R2dbcDataIntegrityViolationException) { + return new DataIntegrityViolationException(buildMessage(task, sql, ex), ex); + } + if (ex instanceof R2dbcPermissionDeniedException) { + return new PermissionDeniedDataAccessException(buildMessage(task, sql, ex), ex); + } + if (ex instanceof R2dbcBadGrammarException) { + return new BadSqlGrammarException(task, (sql != null ? sql : ""), ex); + } + } + + // Fallback to Spring's own R2DBC state translation... + return null; + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java index 36818df..fb72765 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java @@ -27,8 +27,9 @@ import javax.sql.DataSource; import org.junit.Before; import org.junit.Test; + import org.springframework.dao.DataAccessException; -import org.springframework.dao.DuplicateKeyException; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.data.r2dbc.testing.R2dbcIntegrationTestSupport; @@ -125,7 +126,7 @@ public abstract class AbstractDatabaseClientIntegrationTests extends R2dbcIntegr .fetch().rowsUpdated() // .as(StepVerifier::create) // .expectErrorSatisfies(exception -> assertThat(exception) // - .isInstanceOf(DuplicateKeyException.class) // + .isInstanceOf(DataIntegrityViolationException.class) // .hasMessageContaining("execute; SQL [INSERT INTO legoset")) // .verify(); } diff --git a/src/test/java/org/springframework/data/r2dbc/support/R2dbcExceptionSubclassTranslatorUnitTests.java b/src/test/java/org/springframework/data/r2dbc/support/R2dbcExceptionSubclassTranslatorUnitTests.java new file mode 100644 index 0000000..c65f090 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/support/R2dbcExceptionSubclassTranslatorUnitTests.java @@ -0,0 +1,114 @@ +/* + * Copyright 2019 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.data.r2dbc.support; + +import static org.assertj.core.api.Assertions.*; + +import io.r2dbc.spi.R2dbcBadGrammarException; +import io.r2dbc.spi.R2dbcDataIntegrityViolationException; +import io.r2dbc.spi.R2dbcException; +import io.r2dbc.spi.R2dbcNonTransientResourceException; +import io.r2dbc.spi.R2dbcPermissionDeniedException; +import io.r2dbc.spi.R2dbcRollbackException; +import io.r2dbc.spi.R2dbcTimeoutException; +import io.r2dbc.spi.R2dbcTransientResourceException; + +import org.junit.Test; + +import org.springframework.dao.ConcurrencyFailureException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.PermissionDeniedDataAccessException; +import org.springframework.dao.QueryTimeoutException; +import org.springframework.dao.TransientDataAccessResourceException; +import org.springframework.data.r2dbc.BadSqlGrammarException; +import org.springframework.data.r2dbc.UncategorizedR2dbcException; + +/** + * Unit tests for {@link R2dbcExceptionSubclassTranslator}. + * + * @author Mark Paluch + */ +public class R2dbcExceptionSubclassTranslatorUnitTests { + + R2dbcExceptionSubclassTranslator translator = new R2dbcExceptionSubclassTranslator(); + + @Test // gh-57 + public void shouldTranslateTransientResourceException() { + + Exception exception = translator.translate("", "", new R2dbcTransientResourceException()); + + assertThat(exception).isInstanceOf(TransientDataAccessResourceException.class); + } + + @Test // gh-57 + public void shouldTranslateRollbackException() { + + Exception exception = translator.translate("", "", new R2dbcRollbackException()); + + assertThat(exception).isInstanceOf(ConcurrencyFailureException.class); + } + + @Test // gh-57 + public void shouldTranslateTimeoutException() { + + Exception exception = translator.translate("", "", new R2dbcTimeoutException()); + + assertThat(exception).isInstanceOf(QueryTimeoutException.class); + } + + @Test // gh-57 + public void shouldNotTranslateUnknownExceptions() { + + Exception exception = translator.translate("", "", new MyTransientExceptions()); + + assertThat(exception).isInstanceOf(UncategorizedR2dbcException.class); + } + + @Test // gh-57 + public void shouldTranslateNonTransientResourceException() { + + Exception exception = translator.translate("", "", new R2dbcNonTransientResourceException()); + + assertThat(exception).isInstanceOf(DataAccessResourceFailureException.class); + } + + @Test // gh-57 + public void shouldTranslateIntegrityViolationException() { + + Exception exception = translator.translate("", "", new R2dbcDataIntegrityViolationException()); + + assertThat(exception).isInstanceOf(DataIntegrityViolationException.class); + } + + @Test // gh-57 + public void shouldTranslatePermissionDeniedException() { + + Exception exception = translator.translate("", "", new R2dbcPermissionDeniedException()); + + assertThat(exception).isInstanceOf(PermissionDeniedDataAccessException.class); + } + + @Test // gh-57 + public void shouldTranslateBadSqlGrammarException() { + + Exception exception = translator.translate("", "", new R2dbcBadGrammarException()); + + assertThat(exception).isInstanceOf(BadSqlGrammarException.class); + } + + private static class MyTransientExceptions extends R2dbcException {} +}