From b7a7c2c0718cd6e39d67bda45a5a45859810db8f Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 15 Mar 2021 10:14:43 +0100 Subject: [PATCH] Add support for Oracle's R2DBC driver. We support Oracle's experimental R2DBC driver by providing a dialect including bind markers. Since the driver is not yet available from Maven Central and it requires module-path support for ServiceLoader discovery, we need to apply a few workarounds including absence check for our integration tests. See #230 --- pom.xml | 29 ++++ src/main/asciidoc/new-features.adoc | 4 +- src/main/asciidoc/reference/r2dbc-core.adoc | 8 +- .../data/r2dbc/dialect/DialectResolver.java | 1 + .../data/r2dbc/dialect/OracleDialect.java | 67 ++++++++ .../OracleDatabaseClientIntegrationTests.java | 57 +++++++ .../r2dbc/dialect/OracleDialectUnitTests.java | 44 +++++ ...OracleR2dbcRepositoryIntegrationTests.java | 98 +++++++++++ .../data/r2dbc/testing/EnabledOnClass.java | 51 ++++++ .../testing/EnabledOnClassCondition.java | 52 ++++++ ...racleConnectionFactoryProviderWrapper.java | 85 ++++++++++ .../data/r2dbc/testing/OracleTestSupport.java | 153 ++++++++++++++++++ .../io.r2dbc.spi.ConnectionFactoryProvider | 2 + 13 files changed, 646 insertions(+), 5 deletions(-) create mode 100644 src/main/java/org/springframework/data/r2dbc/dialect/OracleDialect.java create mode 100644 src/test/java/org/springframework/data/r2dbc/core/OracleDatabaseClientIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/r2dbc/dialect/OracleDialectUnitTests.java create mode 100644 src/test/java/org/springframework/data/r2dbc/repository/OracleR2dbcRepositoryIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/r2dbc/testing/EnabledOnClass.java create mode 100644 src/test/java/org/springframework/data/r2dbc/testing/EnabledOnClassCondition.java create mode 100644 src/test/java/org/springframework/data/r2dbc/testing/OracleConnectionFactoryProviderWrapper.java create mode 100644 src/test/java/org/springframework/data/r2dbc/testing/OracleTestSupport.java create mode 100644 src/test/resources/META-INF/services/io.r2dbc.spi.ConnectionFactoryProvider diff --git a/pom.xml b/pom.xml index 2d8394ad20..5a021d1260 100644 --- a/pom.xml +++ b/pom.xml @@ -216,6 +216,13 @@ test + + com.oracle.database.jdbc + ojdbc11 + 21.1.0.0 + test + + @@ -288,6 +295,12 @@ + + org.testcontainers + oracle-xe + test + + org.testcontainers postgresql @@ -448,6 +461,22 @@ + + + java11 + + + diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index 35a73281d0..5665a848c2 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -9,10 +9,12 @@ [[new-features.1-2-0]] == What's New in Spring Data R2DBC 1.2.0 -* Deprecate Spring Data R2DBC `DatabaseClient` and move off deprecated API in favor of Spring R2DBC. Consult the <> for further details. +* Deprecate Spring Data R2DBC `DatabaseClient` and move off deprecated API in favor of Spring R2DBC. +Consult the <> for further details. * Support for <>. * <> through `@EnableR2dbcAuditing`. * Support for `@Value` in persistence constructors. +* Support for Oracle's R2DBC driver. [[new-features.1-1-0]] == What's New in Spring Data R2DBC 1.1.0 diff --git a/src/main/asciidoc/reference/r2dbc-core.adoc b/src/main/asciidoc/reference/r2dbc-core.adoc index 33f80e78e3..f51ac42790 100644 --- a/src/main/asciidoc/reference/r2dbc-core.adoc +++ b/src/main/asciidoc/reference/r2dbc-core.adoc @@ -150,7 +150,7 @@ There is a https://github.com/spring-projects/spring-data-examples[GitHub reposi [[r2dbc.connecting]] == Connecting to a Relational Database with Spring -One of the first tasks when using relational databases and Spring is to create a `io.r2dbc.spi.ConnectionFactory` object by using the IoC container. Make sure to use a <>. +One of the first tasks when using relational databases and Spring is to create a `io.r2dbc.spi.ConnectionFactory` object by using the IoC container.Make sure to use a <>. [[r2dbc.connectionfactory]] === Registering a `ConnectionFactory` Instance using Java-based Metadata @@ -173,7 +173,7 @@ public class ApplicationConfiguration extends AbstractR2dbcConfiguration { ---- ==== -This approach lets you use the standard `io.r2dbc.spi.ConnectionFactory` instance, with the container using Spring's `AbstractR2dbcConfiguration`. As compared to registering a `ConnectionFactory` instance directly, the configuration support has the added advantage of also providing the container with an `ExceptionTranslator` implementation that translates R2DBC exceptions to exceptions in Spring's portable `DataAccessException` hierarchy for data access classes annotated with the `@Repository` annotation. This hierarchy and the use of `@Repository` is described in {spring-framework-ref}/data-access.html[Spring's DAO support features]. +This approach lets you use the standard `io.r2dbc.spi.ConnectionFactory` instance, with the container using Spring's `AbstractR2dbcConfiguration`.As compared to registering a `ConnectionFactory` instance directly, the configuration support has the added advantage of also providing the container with an `ExceptionTranslator` implementation that translates R2DBC exceptions to exceptions in Spring's portable `DataAccessException` hierarchy for data access classes annotated with the `@Repository` annotation.This hierarchy and the use of `@Repository` is described in {spring-framework-ref}/data-access.html[Spring's DAO support features]. `AbstractR2dbcConfiguration` also registers `DatabaseClient`, which is required for database interaction and for Repository implementation. @@ -191,11 +191,11 @@ Spring Data R2DBC ships with dialect impelemtations for the following drivers: * https://github.com/mirromutth/r2dbc-mysql[MySQL] (`dev.miku:r2dbc-mysql`) * https://github.com/jasync-sql/jasync-sql[jasync-sql MySQL] (`com.github.jasync-sql:jasync-r2dbc-mysql`) * https://github.com/r2dbc/r2dbc-postgresql[Postgres] (`io.r2dbc:r2dbc-postgresql`) +* https://github.com/oracle/oracle-r2dbc[Oracle] (`com.oracle.database.r2dbc:oracle-r2dbc`) Spring Data R2DBC reacts to database specifics by inspecting the `ConnectionFactory` and selects the appropriate database dialect accordingly. You need to configure your own {spring-data-r2dbc-javadoc}/api/org/springframework/data/r2dbc/dialect/R2dbcDialect.html[`R2dbcDialect`] if the driver you use is not yet known to Spring Data R2DBC. TIP: Dialects are resolved by {spring-data-r2dbc-javadoc}/org/springframework/data/r2dbc/dialect/DialectResolver.html[`DialectResolver`] from a `ConnectionFactory`, typically by inspecting `ConnectionFactoryMetadata`. - + -You can let Spring auto-discover your `R2dbcDialect` by registering a class that implements `org.springframework.data.r2dbc.dialect.DialectResolver$R2dbcDialectProvider` through `META-INF/spring.factories`. ++ You can let Spring auto-discover your `R2dbcDialect` by registering a class that implements `org.springframework.data.r2dbc.dialect.DialectResolver$R2dbcDialectProvider` through `META-INF/spring.factories`. `DialectResolver` discovers dialect provider implementations from the class path using Spring's `SpringFactoriesLoader`. diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/DialectResolver.java b/src/main/java/org/springframework/data/r2dbc/dialect/DialectResolver.java index 275c9a96ec..a4f8ad51cc 100644 --- a/src/main/java/org/springframework/data/r2dbc/dialect/DialectResolver.java +++ b/src/main/java/org/springframework/data/r2dbc/dialect/DialectResolver.java @@ -115,6 +115,7 @@ static class BuiltInDialectProvider implements R2dbcDialectProvider { BUILTIN.put("Microsoft SQL Server", SqlServerDialect.INSTANCE); BUILTIN.put("MySQL", MySqlDialect.INSTANCE); BUILTIN.put("MariaDB", MySqlDialect.INSTANCE); + BUILTIN.put("Oracle", OracleDialect.INSTANCE); BUILTIN.put("PostgreSQL", PostgresDialect.INSTANCE); } diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/OracleDialect.java b/src/main/java/org/springframework/data/r2dbc/dialect/OracleDialect.java new file mode 100644 index 0000000000..c9560c0446 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/dialect/OracleDialect.java @@ -0,0 +1,67 @@ +/* + * Copyright 2021 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.dialect; + +import org.springframework.r2dbc.core.binding.BindMarkersFactory; + +/** + * An SQL dialect for Oracle. + * + * @author Mark Paluch + * @since 1.2.6 + */ +public class OracleDialect extends org.springframework.data.relational.core.dialect.OracleDialect + implements R2dbcDialect { + + /** + * Singleton instance. + */ + public static final OracleDialect INSTANCE = new OracleDialect(); + + private static final BindMarkersFactory NAMED = BindMarkersFactory.named(":", "P", 32, + OracleDialect::filterBindMarker); + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.dialect.Dialect#getBindMarkersFactory() + */ + @Override + public BindMarkersFactory getBindMarkersFactory() { + return NAMED; + } + + private static String filterBindMarker(CharSequence input) { + + StringBuilder builder = new StringBuilder(); + + for (int i = 0; i < input.length(); i++) { + + char ch = input.charAt(i); + + // ascii letter or digit + if (Character.isLetterOrDigit(ch) && ch < 127) { + builder.append(ch); + } + } + + if (builder.length() == 0) { + return ""; + } + + return "_" + builder; + } + +} diff --git a/src/test/java/org/springframework/data/r2dbc/core/OracleDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/core/OracleDatabaseClientIntegrationTests.java new file mode 100644 index 0000000000..4f30a1c072 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/core/OracleDatabaseClientIntegrationTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2018-2021 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.core; + +import io.r2dbc.spi.ConnectionFactory; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.extension.RegisterExtension; + +import org.springframework.data.r2dbc.testing.EnabledOnClass; +import org.springframework.data.r2dbc.testing.ExternalDatabase; +import org.springframework.data.r2dbc.testing.OracleTestSupport; + +/** + * Integration tests for {@link DatabaseClient} against Oracle. + * + * @author Mark Paluch + */ +@EnabledOnClass("oracle.r2dbc.impl.OracleConnectionFactoryProviderImpl") +public class OracleDatabaseClientIntegrationTests extends AbstractDatabaseClientIntegrationTests { + + @RegisterExtension public static final ExternalDatabase database = OracleTestSupport.database(); + + @Override + protected DataSource createDataSource() { + return OracleTestSupport.createDataSource(database); + } + + @Override + protected ConnectionFactory createConnectionFactory() { + return OracleTestSupport.createConnectionFactory(database); + } + + @Override + protected String getCreateTableStatement() { + return OracleTestSupport.CREATE_TABLE_LEGOSET; + } + + @Override + @Disabled("https://github.com/oracle/oracle-r2dbc/issues/9") + public void executeSelectNamedParameters() {} +} diff --git a/src/test/java/org/springframework/data/r2dbc/dialect/OracleDialectUnitTests.java b/src/test/java/org/springframework/data/r2dbc/dialect/OracleDialectUnitTests.java new file mode 100644 index 0000000000..5109446dcc --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/dialect/OracleDialectUnitTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2021 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.dialect; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +import org.springframework.r2dbc.core.binding.BindMarker; +import org.springframework.r2dbc.core.binding.BindMarkers; + +/** + * Unit tests for {@link OracleDialect}. + * + * @author Mark Paluch + */ +class OracleDialectUnitTests { + + @Test // gh-230 + void shouldUseNamedPlaceholders() { + + BindMarkers bindMarkers = OracleDialect.INSTANCE.getBindMarkersFactory().create(); + + BindMarker first = bindMarkers.next(); + BindMarker second = bindMarkers.next("'foo!bar"); + + assertThat(first.getPlaceholder()).isEqualTo(":P0"); + assertThat(second.getPlaceholder()).isEqualTo(":P1_foobar"); + } + +} diff --git a/src/test/java/org/springframework/data/r2dbc/repository/OracleR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/OracleR2dbcRepositoryIntegrationTests.java new file mode 100644 index 0000000000..660ccf7eb4 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/repository/OracleR2dbcRepositoryIntegrationTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2019-2021 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.repository; + +import io.r2dbc.spi.ConnectionFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.FilterType; +import org.springframework.data.r2dbc.config.AbstractR2dbcConfiguration; +import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories; +import org.springframework.data.r2dbc.repository.support.R2dbcRepositoryFactory; +import org.springframework.data.r2dbc.testing.EnabledOnClass; +import org.springframework.data.r2dbc.testing.ExternalDatabase; +import org.springframework.data.r2dbc.testing.OracleTestSupport; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +/** + * Integration tests for {@link LegoSetRepository} using {@link R2dbcRepositoryFactory} against Oracle. + * + * @author Mark Paluch + */ +@ExtendWith(SpringExtension.class) +@ContextConfiguration +@EnabledOnClass("oracle.r2dbc.impl.OracleConnectionFactoryProviderImpl") +public class OracleR2dbcRepositoryIntegrationTests extends AbstractR2dbcRepositoryIntegrationTests { + + @RegisterExtension public static final ExternalDatabase database = OracleTestSupport.database(); + + @Configuration + @EnableR2dbcRepositories(considerNestedRepositories = true, + includeFilters = @Filter(classes = OracleLegoSetRepository.class, type = FilterType.ASSIGNABLE_TYPE)) + static class IntegrationTestConfiguration extends AbstractR2dbcConfiguration { + + @Bean + @Override + public ConnectionFactory connectionFactory() { + return OracleTestSupport.createConnectionFactory(database); + } + } + + @Override + protected DataSource createDataSource() { + return OracleTestSupport.createDataSource(database); + } + + @Override + protected ConnectionFactory createConnectionFactory() { + return OracleTestSupport.createConnectionFactory(database); + } + + @Override + protected String getCreateTableStatement() { + return OracleTestSupport.CREATE_TABLE_LEGOSET_WITH_ID_GENERATION; + } + + @Override + protected Class getRepositoryInterfaceType() { + return OracleLegoSetRepository.class; + } + + interface OracleLegoSetRepository extends LegoSetRepository { + + @Override + @Query("SELECT name FROM legoset") + Flux findAsProjection(); + + @Override + @Query("SELECT * FROM legoset WHERE manual = :manual") + Mono findByManual(int manual); + + @Override + @Query("SELECT id FROM legoset") + Flux findAllIds(); + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/testing/EnabledOnClass.java b/src/test/java/org/springframework/data/r2dbc/testing/EnabledOnClass.java new file mode 100644 index 0000000000..26282cf562 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/testing/EnabledOnClass.java @@ -0,0 +1,51 @@ +/* + * Copyright 2021 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.testing; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.condition.JRE; +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * {@code @EnabledOnClass} is used to signal that the annotated test class or test method is only enabled if + * the specified {@link #value() class} is present. + *

+ * When applied at the class level, all test methods within that class will be enabled on presence of the specified + * class. + *

+ * If a test method is disabled via this annotation, that does not prevent the test class from being instantiated. + * Rather, it prevents the execution of the test method and method-level lifecycle callbacks such as {@code @BeforeEach} + * methods, {@code @AfterEach} methods, and corresponding extension APIs. + *

+ * This annotation may be used as a meta-annotation in order to create a custom composed annotation that + * inherits the semantics of this annotation. + * + * @see JRE + * @see org.junit.jupiter.api.Disabled + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@ExtendWith(EnabledOnClassCondition.class) +public @interface EnabledOnClass { + + String value(); +} diff --git a/src/test/java/org/springframework/data/r2dbc/testing/EnabledOnClassCondition.java b/src/test/java/org/springframework/data/r2dbc/testing/EnabledOnClassCondition.java new file mode 100644 index 0000000000..7fe3a60246 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/testing/EnabledOnClassCondition.java @@ -0,0 +1,52 @@ +/* + * Copyright 2021 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.testing; + +import static org.junit.jupiter.api.extension.ConditionEvaluationResult.*; +import static org.junit.platform.commons.util.AnnotationUtils.*; + +import org.junit.jupiter.api.extension.ConditionEvaluationResult; +import org.junit.jupiter.api.extension.ExecutionCondition; +import org.junit.jupiter.api.extension.ExtensionContext; + +import org.springframework.util.ClassUtils; + +/** + * {@link ExecutionCondition} for {@link EnabledOnClass @EnabledOnClass}. + * + * @author Mark Paluch + * @see EnabledOnClass + */ +class EnabledOnClassCondition implements ExecutionCondition { + + @Override + public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { + return findAnnotation(context.getElement(), EnabledOnClass.class) // + .map(annotation -> isEnabled(annotation) + ? enabled(String.format("Class '%s' found on the class path.", annotation.value())) + : disabled(String.format("Class '%s' not found on the class path.", annotation.value()))) // + .orElseGet(this::enabledByDefault); + } + + private boolean isEnabled(EnabledOnClass annotation) { + return ClassUtils.isPresent(annotation.value(), EnabledOnClassCondition.class.getClassLoader()); + } + + private ConditionEvaluationResult enabledByDefault() { + return enabled("@EnabledOnClass is not present"); + } + +} diff --git a/src/test/java/org/springframework/data/r2dbc/testing/OracleConnectionFactoryProviderWrapper.java b/src/test/java/org/springframework/data/r2dbc/testing/OracleConnectionFactoryProviderWrapper.java new file mode 100644 index 0000000000..7813a3db66 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/testing/OracleConnectionFactoryProviderWrapper.java @@ -0,0 +1,85 @@ +/* + * Copyright 2021 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.testing; + +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.ConnectionFactoryOptions; +import io.r2dbc.spi.ConnectionFactoryProvider; + +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * {@link ConnectionFactoryProvider} for Oracle's R2DBC driver. Allows for absence of the driver which is required when + * using Java 8 as the ServiceLoader requires presence of classes listed in the service loader manifest. + * + * @author Mark Paluch + */ +public class OracleConnectionFactoryProviderWrapper implements ConnectionFactoryProvider { + + private final ConnectionFactoryProvider delegate; + + public OracleConnectionFactoryProviderWrapper() { + + if (ClassUtils.isPresent("oracle.r2dbc.impl.OracleConnectionFactoryProviderImpl", getClass().getClassLoader())) { + + delegate = createProvider(); + } else { + delegate = null; + } + + } + + private static ConnectionFactoryProvider createProvider() { + + try { + return (ConnectionFactoryProvider) Class.forName("oracle.r2dbc.impl.OracleConnectionFactoryProviderImpl") + .newInstance(); + } catch (ReflectiveOperationException e) { + ReflectionUtils.handleReflectionException(e); + } + return null; + } + + @Override + public ConnectionFactory create(ConnectionFactoryOptions connectionFactoryOptions) { + if (delegate != null) { + return delegate.create(connectionFactoryOptions); + } + throw new IllegalStateException( + "Oracle R2DBC (oracle.r2dbc.impl.OracleConnectionFactoryProviderImpl) is not on the class path"); + } + + @Override + public boolean supports(ConnectionFactoryOptions connectionFactoryOptions) { + + if (delegate != null) { + return delegate.supports(connectionFactoryOptions); + } + return false; + } + + @Override + public String getDriver() { + + if (delegate != null) { + return delegate.getDriver(); + } + + return "oracle-r2dbc (proxy)"; + } + +} diff --git a/src/test/java/org/springframework/data/r2dbc/testing/OracleTestSupport.java b/src/test/java/org/springframework/data/r2dbc/testing/OracleTestSupport.java new file mode 100644 index 0000000000..2997fb7b36 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/testing/OracleTestSupport.java @@ -0,0 +1,153 @@ +/* + * Copyright 2021 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.testing; + +import io.r2dbc.spi.ConnectionFactories; +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.ConnectionFactoryOptions; + +import java.util.function.Supplier; +import java.util.stream.Stream; + +import javax.sql.DataSource; + +import org.springframework.data.r2dbc.testing.ExternalDatabase.ProvidedDatabase; +import org.springframework.jdbc.datasource.DriverManagerDataSource; +import org.springframework.util.ClassUtils; + +import org.testcontainers.containers.OracleContainer; + +/** + * Utility class for testing against Oracle. + * + * @author Mark Paluch + */ +public class OracleTestSupport { + + private static ExternalDatabase testContainerDatabase; + + public static String CREATE_TABLE_LEGOSET = "CREATE TABLE legoset (\n" // + + " id INTEGER PRIMARY KEY,\n" // + + " version INTEGER NULL,\n" // + + " name VARCHAR2(255) NOT NULL,\n" // + + " manual INTEGER NULL,\n" // + + " cert RAW(255) NULL\n" // + + ")"; + + public static String CREATE_TABLE_LEGOSET_WITH_ID_GENERATION = "CREATE TABLE legoset (\n" // + + " id INTEGER GENERATED by default on null as IDENTITY PRIMARY KEY,\n" // + + " version INTEGER NULL,\n" // + + " name VARCHAR2(255) NOT NULL,\n" // + + " manual INTEGER NULL\n" // + + ")"; + + /** + * Returns a database either hosted locally or running inside Docker. + * + * @return information about the database. Guaranteed to be not {@literal null}. + */ + public static ExternalDatabase database() { + + if (!ClassUtils.isPresent("oracle.r2dbc.impl.OracleConnectionFactoryProviderImpl", + OracleTestSupport.class.getClassLoader())) { + return ExternalDatabase.unavailable(); + } + + if (Boolean.getBoolean("spring.data.r2dbc.test.preferLocalDatabase")) { + + return getFirstWorkingDatabase( // + OracleTestSupport::local, // + OracleTestSupport::testContainer // + ); + } else { + + return getFirstWorkingDatabase( // + OracleTestSupport::testContainer, // + OracleTestSupport::local // + ); + } + } + + @SafeVarargs + private static ExternalDatabase getFirstWorkingDatabase(Supplier... suppliers) { + + return Stream.of(suppliers).map(Supplier::get) // + .filter(ExternalDatabase::checkValidity) // + .findFirst() // + .orElse(ExternalDatabase.unavailable()); + } + + /** + * Returns a locally provided database. + */ + private static ExternalDatabase local() { + + return ProvidedDatabase.builder() // + .hostname("localhost") // + .port(1521) // + .database("XEPDB1") // + .username("system") // + .password("oracle") // + .jdbcUrl("jdbc:oracle:thin:system/oracle@localhost:1521:XEPDB1") // + .build(); + } + + /** + * Returns a database provided via Testcontainers. + */ + private static ExternalDatabase testContainer() { + + if (testContainerDatabase == null) { + + try { + OracleContainer container = new OracleContainer("springci/spring-data-oracle-xe-prebuild:18.4.0") + .withReuse(true); + container.start(); + + testContainerDatabase = ProvidedDatabase.builder(container) // + .database("XEPDB1").build(); + } catch (IllegalStateException ise) { + // docker not available. + testContainerDatabase = ExternalDatabase.unavailable(); + } + } + + return testContainerDatabase; + } + + /** + * Creates a new Oracle {@link ConnectionFactory} configured from the {@link ExternalDatabase}. + */ + public static ConnectionFactory createConnectionFactory(ExternalDatabase database) { + + ConnectionFactoryOptions options = ConnectionUtils.createOptions("oracle", database); + return ConnectionFactories.get(options); + } + + /** + * Creates a new {@link DataSource} configured from the {@link ExternalDatabase}. + */ + public static DataSource createDataSource(ExternalDatabase database) { + + DriverManagerDataSource dataSource = new DriverManagerDataSource(); + + dataSource.setUsername(database.getUsername()); + dataSource.setPassword(database.getPassword()); + dataSource.setUrl(database.getJdbcUrl().replace(":xe", "/XEPDB1")); + + return dataSource; + } +} diff --git a/src/test/resources/META-INF/services/io.r2dbc.spi.ConnectionFactoryProvider b/src/test/resources/META-INF/services/io.r2dbc.spi.ConnectionFactoryProvider new file mode 100644 index 0000000000..99f185a209 --- /dev/null +++ b/src/test/resources/META-INF/services/io.r2dbc.spi.ConnectionFactoryProvider @@ -0,0 +1,2 @@ +# https://github.com/oracle/oracle-r2dbc/issues/10, otherwise the driver needs to be operated in module-path mode. +org.springframework.data.r2dbc.testing.OracleConnectionFactoryProviderWrapper