From 1e785e81f81599b5d15cabed18e60bf43933092b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 25 Apr 2023 13:06:40 +0100 Subject: [PATCH] Add Docker Compose support for Oracle Database using R2DBC Closes gh-35143 --- .../spring-boot-docker-compose/build.gradle | 1 + .../connection/oracle/OracleEnvironment.java | 68 +++++++++++ ...DockerComposeConnectionDetailsFactory.java | 73 ++++++++++++ .../connection/oracle/package-info.java | 20 ++++ .../main/resources/META-INF/spring.factories | 1 + .../oracle/OracleEnvironmentTests.java | 110 ++++++++++++++++++ ...nectionDetailsFactoryIntegrationTests.java | 56 +++++++++ .../connection/oracle/oracle-compose.yaml | 13 +++ .../asciidoc/features/docker-compose.adoc | 2 +- 9 files changed, 343 insertions(+), 1 deletion(-) create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleEnvironment.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleR2dbcDockerComposeConnectionDetailsFactory.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/package-info.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleEnvironmentTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/oracle/oracle-compose.yaml diff --git a/spring-boot-project/spring-boot-docker-compose/build.gradle b/spring-boot-project/spring-boot-docker-compose/build.gradle index 588e65cfe63..fa33f0f8062 100644 --- a/spring-boot-project/spring-boot-docker-compose/build.gradle +++ b/spring-boot-project/spring-boot-docker-compose/build.gradle @@ -30,5 +30,6 @@ dependencies { testImplementation("org.junit.jupiter:junit-jupiter") testRuntimeOnly("com.microsoft.sqlserver:mssql-jdbc") + testRuntimeOnly("com.oracle.database.r2dbc:oracle-r2dbc") testRuntimeOnly("io.r2dbc:r2dbc-mssql") } diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleEnvironment.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleEnvironment.java new file mode 100644 index 00000000000..b06fc5aa8cb --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleEnvironment.java @@ -0,0 +1,68 @@ +/* + * Copyright 2012-2023 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.docker.compose.service.connection.oracle; + +import java.util.Map; + +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Oracle Database environment details. + * + * @author Andy Wilkinson + */ +class OracleEnvironment { + + private final String username; + + private final String password; + + private final String database; + + OracleEnvironment(Map env) { + this.username = env.getOrDefault("APP_USER", "system"); + this.password = extractPassword(env); + this.database = env.getOrDefault("ORACLE_DATABASE", "xepdb1"); + } + + private String extractPassword(Map env) { + if (env.containsKey("APP_USER")) { + String password = env.get("APP_PASSWORD"); + Assert.state(StringUtils.hasLength(password), "No Oracle app password found"); + return password; + } + Assert.state(!env.containsKey("ORACLE_RANDOM_PASSWORD"), + "ORACLE_RANDOM_PASSWORD is not supported without APP_USER and APP_PASSWORD"); + String password = env.get("ORACLE_PASSWORD"); + Assert.state(StringUtils.hasLength(password), "No Oracle password found"); + return password; + } + + String getUsername() { + return this.username; + } + + String getPassword() { + return this.password; + } + + String getDatabase() { + return this.database; + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleR2dbcDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleR2dbcDockerComposeConnectionDetailsFactory.java new file mode 100644 index 00000000000..5ebf98956e9 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleR2dbcDockerComposeConnectionDetailsFactory.java @@ -0,0 +1,73 @@ +/* + * Copyright 2012-2023 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.docker.compose.service.connection.oracle; + +import io.r2dbc.spi.ConnectionFactoryOptions; + +import org.springframework.boot.autoconfigure.r2dbc.R2dbcConnectionDetails; +import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource; +import org.springframework.boot.docker.compose.service.connection.r2dbc.ConnectionFactoryOptionsBuilder; + +/** + * {@link DockerComposeConnectionDetailsFactory} to create {@link R2dbcConnectionDetails} + * for an {@code oracle-xe} service. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class OracleR2dbcDockerComposeConnectionDetailsFactory + extends DockerComposeConnectionDetailsFactory { + + OracleR2dbcDockerComposeConnectionDetailsFactory() { + super("gvenzl/oracle-xe", "io.r2dbc.spi.ConnectionFactoryOptions"); + } + + @Override + protected R2dbcConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) { + return new OracleDbR2dbcDockerComposeConnectionDetails(source.getRunningService()); + } + + /** + * {@link R2dbcConnectionDetails} backed by a {@code gvenzl/oracle-xe} + * {@link RunningService}. + */ + static class OracleDbR2dbcDockerComposeConnectionDetails extends DockerComposeConnectionDetails + implements R2dbcConnectionDetails { + + private static final ConnectionFactoryOptionsBuilder connectionFactoryOptionsBuilder = new ConnectionFactoryOptionsBuilder( + "oracle", 1521); + + private final ConnectionFactoryOptions connectionFactoryOptions; + + OracleDbR2dbcDockerComposeConnectionDetails(RunningService service) { + super(service); + OracleEnvironment environment = new OracleEnvironment(service.env()); + this.connectionFactoryOptions = connectionFactoryOptionsBuilder.build(service, environment.getDatabase(), + environment.getUsername(), environment.getPassword()); + } + + @Override + public ConnectionFactoryOptions getConnectionFactoryOptions() { + return this.connectionFactoryOptions; + } + + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/package-info.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/package-info.java new file mode 100644 index 00000000000..e12c74290f4 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2023 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. + */ + +/** + * Auto-configuration for docker compose MySQL service connections. + */ +package org.springframework.boot.docker.compose.service.connection.oracle; diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories index 6b7f5142e2b..3182f984514 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories @@ -12,6 +12,7 @@ org.springframework.boot.docker.compose.service.connection.mariadb.MariaDbR2dbcD org.springframework.boot.docker.compose.service.connection.mongo.MongoDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.mysql.MySqlJdbcDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.mysql.MySqlR2dbcDockerComposeConnectionDetailsFactory,\ +org.springframework.boot.docker.compose.service.connection.oracle.OracleR2dbcDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.postgres.PostgresJdbcDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.postgres.PostgresR2dbcDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.rabbit.RabbitDockerComposeConnectionDetailsFactory,\ diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleEnvironmentTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleEnvironmentTests.java new file mode 100644 index 00000000000..334ee0812d4 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleEnvironmentTests.java @@ -0,0 +1,110 @@ +/* + * Copyright 2012-2023 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.docker.compose.service.connection.oracle; + +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatNoException; + +/** + * Tests for {@link OracleEnvironment}. + * + * @author Andy Wilkinson + */ +class OracleEnvironmentTests { + + @Test + void getUsernameWhenHasAppUser() { + OracleEnvironment environment = new OracleEnvironment(Map.of("APP_USER", "alice", "APP_PASSWORD", "secret")); + assertThat(environment.getUsername()).isEqualTo("alice"); + } + + @Test + void getUsernameWhenHasNoAppUser() { + OracleEnvironment environment = new OracleEnvironment(Map.of("ORACLE_PASSWORD", "secret")); + assertThat(environment.getUsername()).isEqualTo("system"); + } + + @Test + void getPasswordWhenHasAppPassword() { + OracleEnvironment environment = new OracleEnvironment(Map.of("APP_USER", "alice", "APP_PASSWORD", "secret")); + assertThat(environment.getPassword()).isEqualTo("secret"); + } + + @Test + void getPasswordWhenHasOraclePassword() { + OracleEnvironment environment = new OracleEnvironment(Map.of("ORACLE_PASSWORD", "secret")); + assertThat(environment.getPassword()).isEqualTo("secret"); + } + + @Test + void createWhenRandomPasswordAndAppPasswordDoesNotThrow() { + assertThatNoException().isThrownBy(() -> new OracleEnvironment( + Map.of("APP_USER", "alice", "APP_PASSWORD", "secret", "ORACLE_RANDOM_PASSWORD", "true"))); + } + + @Test + void createWhenRandomPasswordThrowsException() { + assertThatIllegalStateException() + .isThrownBy(() -> new OracleEnvironment(Map.of("ORACLE_RANDOM_PASSWORD", "true"))) + .withMessage("ORACLE_RANDOM_PASSWORD is not supported without APP_USER and APP_PASSWORD"); + } + + @Test + void createWhenAppUserAndNoAppPasswordThrowsException() { + assertThatIllegalStateException().isThrownBy(() -> new OracleEnvironment(Map.of("APP_USER", "alice"))) + .withMessage("No Oracle app password found"); + } + + @Test + void createWhenAppUserAndEmptyAppPasswordThrowsException() { + assertThatIllegalStateException() + .isThrownBy(() -> new OracleEnvironment(Map.of("APP_USER", "alice", "APP_PASSWORD", ""))) + .withMessage("No Oracle app password found"); + } + + @Test + void createWhenHasNoPasswordThrowsException() { + assertThatIllegalStateException().isThrownBy(() -> new OracleEnvironment(Collections.emptyMap())) + .withMessage("No Oracle password found"); + } + + @Test + void createWhenHasEmptyPasswordThrowsException() { + assertThatIllegalStateException().isThrownBy(() -> new OracleEnvironment(Map.of("ORACLE_PASSWORD", ""))) + .withMessage("No Oracle password found"); + } + + @Test + void getDatabaseWhenHasNoOracleDatabase() { + OracleEnvironment environment = new OracleEnvironment(Map.of("ORACLE_PASSWORD", "secret")); + assertThat(environment.getDatabase()).isEqualTo("xepdb1"); + } + + @Test + void getDatabaseWhenHasOracleDatabase() { + OracleEnvironment environment = new OracleEnvironment( + Map.of("ORACLE_PASSWORD", "secret", "ORACLE_DATABASE", "db")); + assertThat(environment.getDatabase()).isEqualTo("db"); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 00000000000..eeec9f61e19 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 2012-2023 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.docker.compose.service.connection.oracle; + +import io.r2dbc.spi.ConnectionFactories; +import io.r2dbc.spi.ConnectionFactoryOptions; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.r2dbc.R2dbcConnectionDetails; +import org.springframework.boot.docker.compose.service.connection.test.AbstractDockerComposeIntegrationTests; +import org.springframework.boot.jdbc.DatabaseDriver; +import org.springframework.r2dbc.core.DatabaseClient; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link OracleR2dbcDockerComposeConnectionDetailsFactory} + * + * @author Andy Wilkinson + */ +class OracleR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests extends AbstractDockerComposeIntegrationTests { + + OracleR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests() { + super("oracle-compose.yaml"); + } + + @Test + void runCreatesConnectionDetailsThatCanBeUsedToAccessDatabase() { + R2dbcConnectionDetails connectionDetails = run(R2dbcConnectionDetails.class); + ConnectionFactoryOptions connectionFactoryOptions = connectionDetails.getConnectionFactoryOptions(); + assertThat(connectionFactoryOptions.toString()).contains("database=xepdb1", "driver=oracle", + "password=REDACTED", "user=system"); + assertThat(connectionFactoryOptions.getRequiredValue(ConnectionFactoryOptions.PASSWORD)).isEqualTo("secret"); + Object result = DatabaseClient.create(ConnectionFactories.get(connectionFactoryOptions)) + .sql(DatabaseDriver.ORACLE.getValidationQuery()) + .map((row, metadata) -> row.get(0)) + .first() + .block(); + assertThat(result).isEqualTo("Hello"); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/oracle/oracle-compose.yaml b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/oracle/oracle-compose.yaml new file mode 100644 index 00000000000..4775f6c11bc --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/oracle/oracle-compose.yaml @@ -0,0 +1,13 @@ +services: + database: + image: 'gvenzl/oracle-xe:18.4.0-slim' + ports: + - '1521' + environment: + - 'ORACLE_PASSWORD=secret' + healthcheck: + test: ["CMD-SHELL", "healthcheck.sh"] + interval: 10s + timeout: 5s + retries: 10 + diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/docker-compose.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/docker-compose.adoc index 625c335dc9a..0a449d34aa4 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/docker-compose.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/docker-compose.adoc @@ -50,7 +50,7 @@ The following service connections are currently supported: | Containers named "mongo" | `R2dbcConnectionDetails` -| Containers named "mariadb", "mssql/server", "mysql", or "postgres" +| Containers named "gvenzl/oracle-xe", "mariadb", "mssql/server", "mysql", or "postgres" | `RabbitConnectionDetails` | Containers named "rabbitmq"