From 54e7d7f060f7b6a06f51fe911eafca6a179503ae Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 17 Mar 2021 16:26:54 +0000 Subject: [PATCH] Promote ConnectionFactoryBuilder to the "spring-boot" module Closes gh-25492 --- ...ionFactoryBeanCreationFailureAnalyzer.java | 5 +- .../r2dbc/ConnectionFactoryBuilder.java | 140 ++-------------- .../ConnectionFactoryConfigurations.java | 7 +- .../ConnectionFactoryOptionsInitializer.java | 155 ++++++++++++++++++ .../r2dbc/EmbeddedDatabaseConnection.java | 3 + .../r2dbc/ConnectionFactoryBuilderTests.java | 8 +- .../r2dbc/R2dbcAutoConfigurationTests.java | 3 +- spring-boot-project/spring-boot/build.gradle | 2 + .../boot/r2dbc/ConnectionFactoryBuilder.java | 137 ++++++++++++++++ .../r2dbc/EmbeddedDatabaseConnection.java | 84 ++++++++++ .../boot/r2dbc/package-info.java | 20 +++ .../r2dbc/ConnectionFactoryBuilderTests.java | 123 ++++++++++++++ .../EmbeddedDatabaseConnectionTests.java | 81 +++++++++ 13 files changed, 632 insertions(+), 136 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryOptionsInitializer.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/r2dbc/ConnectionFactoryBuilder.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/r2dbc/EmbeddedDatabaseConnection.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/r2dbc/package-info.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/r2dbc/ConnectionFactoryBuilderTests.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/r2dbc/EmbeddedDatabaseConnectionTests.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryBeanCreationFailureAnalyzer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryBeanCreationFailureAnalyzer.java index 3ece313e4f..264aa3f315 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryBeanCreationFailureAnalyzer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryBeanCreationFailureAnalyzer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-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. @@ -16,9 +16,10 @@ package org.springframework.boot.autoconfigure.r2dbc; -import org.springframework.boot.autoconfigure.r2dbc.ConnectionFactoryBuilder.ConnectionFactoryBeanCreationException; +import org.springframework.boot.autoconfigure.r2dbc.ConnectionFactoryOptionsInitializer.ConnectionFactoryBeanCreationException; import org.springframework.boot.diagnostics.AbstractFailureAnalyzer; import org.springframework.boot.diagnostics.FailureAnalysis; +import org.springframework.boot.r2dbc.EmbeddedDatabaseConnection; import org.springframework.context.EnvironmentAware; import org.springframework.core.env.Environment; import org.springframework.util.ObjectUtils; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryBuilder.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryBuilder.java index 5c1ca7438d..b57230ba53 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryBuilder.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-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. @@ -17,17 +17,12 @@ package org.springframework.boot.autoconfigure.r2dbc; import java.util.function.Consumer; -import java.util.function.Predicate; import java.util.function.Supplier; import io.r2dbc.spi.ConnectionFactories; import io.r2dbc.spi.ConnectionFactory; import io.r2dbc.spi.ConnectionFactoryOptions; import io.r2dbc.spi.ConnectionFactoryOptions.Builder; -import io.r2dbc.spi.Option; - -import org.springframework.beans.factory.BeanCreationException; -import org.springframework.util.StringUtils; /** * Builder for {@link ConnectionFactory}. @@ -36,7 +31,10 @@ import org.springframework.util.StringUtils; * @author Tadaya Tsuyukubo * @author Stephane Nicoll * @since 2.3.0 + * @deprecated since 2.5.0 in favor of + * {@link org.springframework.boot.r2dbc.ConnectionFactoryBuilder} */ +@Deprecated public final class ConnectionFactoryBuilder { private final ConnectionFactoryOptions.Builder optionsBuilder; @@ -59,7 +57,7 @@ public final class ConnectionFactoryBuilder { public static ConnectionFactoryBuilder of(R2dbcProperties properties, Supplier embeddedDatabaseConnection) { return new ConnectionFactoryBuilder( - new ConnectionFactoryOptionsInitializer().initializeOptions(properties, embeddedDatabaseConnection)); + new ConnectionFactoryOptionsInitializer().initialize(properties, adapt(embeddedDatabaseConnection))); } /** @@ -133,127 +131,13 @@ public final class ConnectionFactoryBuilder { return this.optionsBuilder.build(); } - static class ConnectionFactoryOptionsInitializer { - - /** - * Initialize a {@link io.r2dbc.spi.ConnectionFactoryOptions.Builder - * ConnectionFactoryOptions.Builder} using the specified properties. - * @param properties the properties to use to initialize the builder - * @param embeddedDatabaseConnection the embedded connection to use as a fallback - * @return an initialized builder - * @throws ConnectionFactoryBeanCreationException if no suitable connection could - * be determined - */ - ConnectionFactoryOptions.Builder initializeOptions(R2dbcProperties properties, - Supplier embeddedDatabaseConnection) { - if (StringUtils.hasText(properties.getUrl())) { - return initializeRegularOptions(properties); - } - EmbeddedDatabaseConnection embeddedConnection = embeddedDatabaseConnection.get(); - if (embeddedConnection != EmbeddedDatabaseConnection.NONE) { - return initializeEmbeddedOptions(properties, embeddedConnection); - } - throw connectionFactoryBeanCreationException("Failed to determine a suitable R2DBC Connection URL", - properties, embeddedConnection); - } - - private ConnectionFactoryOptions.Builder initializeRegularOptions(R2dbcProperties properties) { - ConnectionFactoryOptions urlOptions = ConnectionFactoryOptions.parse(properties.getUrl()); - Builder optionsBuilder = urlOptions.mutate(); - configureIf(optionsBuilder, urlOptions, ConnectionFactoryOptions.USER, properties::getUsername, - StringUtils::hasText); - configureIf(optionsBuilder, urlOptions, ConnectionFactoryOptions.PASSWORD, properties::getPassword, - StringUtils::hasText); - configureIf(optionsBuilder, urlOptions, ConnectionFactoryOptions.DATABASE, - () -> determineDatabaseName(properties), StringUtils::hasText); - if (properties.getProperties() != null) { - properties.getProperties().forEach((key, value) -> optionsBuilder.option(Option.valueOf(key), value)); - } - return optionsBuilder; - } - - private ConnectionFactoryOptions.Builder initializeEmbeddedOptions(R2dbcProperties properties, - EmbeddedDatabaseConnection embeddedDatabaseConnection) { - String url = embeddedDatabaseConnection.getUrl(determineEmbeddedDatabaseName(properties)); - if (url == null) { - throw connectionFactoryBeanCreationException("Failed to determine a suitable R2DBC Connection URL", - properties, embeddedDatabaseConnection); - } - Builder builder = ConnectionFactoryOptions.parse(url).mutate(); - String username = determineEmbeddedUsername(properties); - if (StringUtils.hasText(username)) { - builder.option(ConnectionFactoryOptions.USER, username); - } - if (StringUtils.hasText(properties.getPassword())) { - builder.option(ConnectionFactoryOptions.PASSWORD, properties.getPassword()); - } - return builder; - } - - private String determineDatabaseName(R2dbcProperties properties) { - if (properties.isGenerateUniqueName()) { - return properties.determineUniqueName(); - } - if (StringUtils.hasLength(properties.getName())) { - return properties.getName(); - } - return null; - } - - private String determineEmbeddedDatabaseName(R2dbcProperties properties) { - String databaseName = determineDatabaseName(properties); - return (databaseName != null) ? databaseName : "testdb"; - } - - private String determineEmbeddedUsername(R2dbcProperties properties) { - String username = ifHasText(properties.getUsername()); - return (username != null) ? username : "sa"; - } - - private void configureIf(Builder optionsBuilder, - ConnectionFactoryOptions originalOptions, Option option, Supplier valueSupplier, - Predicate setIf) { - if (originalOptions.hasOption(option)) { - return; - } - T value = valueSupplier.get(); - if (setIf.test(value)) { - optionsBuilder.option(option, value); - } - } - - private ConnectionFactoryBeanCreationException connectionFactoryBeanCreationException(String message, - R2dbcProperties properties, EmbeddedDatabaseConnection embeddedDatabaseConnection) { - return new ConnectionFactoryBeanCreationException(message, properties, embeddedDatabaseConnection); - } - - private String ifHasText(String candidate) { - return (StringUtils.hasText(candidate)) ? candidate : null; - } - - } - - static class ConnectionFactoryBeanCreationException extends BeanCreationException { - - private final R2dbcProperties properties; - - private final EmbeddedDatabaseConnection embeddedDatabaseConnection; - - ConnectionFactoryBeanCreationException(String message, R2dbcProperties properties, - EmbeddedDatabaseConnection embeddedDatabaseConnection) { - super(message); - this.properties = properties; - this.embeddedDatabaseConnection = embeddedDatabaseConnection; - } - - EmbeddedDatabaseConnection getEmbeddedDatabaseConnection() { - return this.embeddedDatabaseConnection; - } - - R2dbcProperties getProperties() { - return this.properties; - } - + private static Supplier adapt( + Supplier embeddedDatabaseConnection) { + return () -> { + EmbeddedDatabaseConnection connection = embeddedDatabaseConnection.get(); + return (connection != null) + ? org.springframework.boot.r2dbc.EmbeddedDatabaseConnection.valueOf(connection.name()) : null; + }; } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryConfigurations.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryConfigurations.java index 3f80c5f81a..599b947c1f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryConfigurations.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryConfigurations.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-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. @@ -30,6 +30,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.SpringBootCondition; import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.boot.r2dbc.EmbeddedDatabaseConnection; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Condition; import org.springframework.context.annotation.ConditionContext; @@ -50,7 +51,9 @@ abstract class ConnectionFactoryConfigurations { protected static ConnectionFactory createConnectionFactory(R2dbcProperties properties, ClassLoader classLoader, List optionsCustomizers) { - return ConnectionFactoryBuilder.of(properties, () -> EmbeddedDatabaseConnection.get(classLoader)) + return org.springframework.boot.r2dbc.ConnectionFactoryBuilder + .withOptions(new ConnectionFactoryOptionsInitializer().initialize(properties, + () -> EmbeddedDatabaseConnection.get(classLoader))) .configure((options) -> { for (ConnectionFactoryOptionsBuilderCustomizer optionsCustomizer : optionsCustomizers) { optionsCustomizer.customize(options); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryOptionsInitializer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryOptionsInitializer.java new file mode 100644 index 0000000000..f5e10f4c73 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryOptionsInitializer.java @@ -0,0 +1,155 @@ +/* + * Copyright 2012-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.boot.autoconfigure.r2dbc; + +import java.util.function.Predicate; +import java.util.function.Supplier; + +import io.r2dbc.spi.ConnectionFactoryOptions; +import io.r2dbc.spi.ConnectionFactoryOptions.Builder; +import io.r2dbc.spi.Option; + +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.boot.r2dbc.EmbeddedDatabaseConnection; +import org.springframework.util.StringUtils; + +/** + * Initialize a {@link ConnectionFactoryOptions.Builder} based on {@link R2dbcProperties}. + * + * @author Stephane Nicoll + */ +class ConnectionFactoryOptionsInitializer { + + /** + * Initialize a {@link io.r2dbc.spi.ConnectionFactoryOptions.Builder + * ConnectionFactoryOptions.Builder} using the specified properties. + * @param properties the properties to use to initialize the builder + * @param embeddedDatabaseConnection the embedded connection to use as a fallback + * @return an initialized builder + * @throws ConnectionFactoryBeanCreationException if no suitable connection could be + * determined + */ + ConnectionFactoryOptions.Builder initialize(R2dbcProperties properties, + Supplier embeddedDatabaseConnection) { + if (StringUtils.hasText(properties.getUrl())) { + return initializeRegularOptions(properties); + } + EmbeddedDatabaseConnection embeddedConnection = embeddedDatabaseConnection.get(); + if (embeddedConnection != EmbeddedDatabaseConnection.NONE) { + return initializeEmbeddedOptions(properties, embeddedConnection); + } + throw connectionFactoryBeanCreationException("Failed to determine a suitable R2DBC Connection URL", properties, + embeddedConnection); + } + + private ConnectionFactoryOptions.Builder initializeRegularOptions(R2dbcProperties properties) { + ConnectionFactoryOptions urlOptions = ConnectionFactoryOptions.parse(properties.getUrl()); + Builder optionsBuilder = urlOptions.mutate(); + configureIf(optionsBuilder, urlOptions, ConnectionFactoryOptions.USER, properties::getUsername, + StringUtils::hasText); + configureIf(optionsBuilder, urlOptions, ConnectionFactoryOptions.PASSWORD, properties::getPassword, + StringUtils::hasText); + configureIf(optionsBuilder, urlOptions, ConnectionFactoryOptions.DATABASE, + () -> determineDatabaseName(properties), StringUtils::hasText); + if (properties.getProperties() != null) { + properties.getProperties().forEach((key, value) -> optionsBuilder.option(Option.valueOf(key), value)); + } + return optionsBuilder; + } + + private Builder initializeEmbeddedOptions(R2dbcProperties properties, + EmbeddedDatabaseConnection embeddedDatabaseConnection) { + String url = embeddedDatabaseConnection.getUrl(determineEmbeddedDatabaseName(properties)); + if (url == null) { + throw connectionFactoryBeanCreationException("Failed to determine a suitable R2DBC Connection URL", + properties, embeddedDatabaseConnection); + } + Builder builder = ConnectionFactoryOptions.parse(url).mutate(); + String username = determineEmbeddedUsername(properties); + if (StringUtils.hasText(username)) { + builder.option(ConnectionFactoryOptions.USER, username); + } + if (StringUtils.hasText(properties.getPassword())) { + builder.option(ConnectionFactoryOptions.PASSWORD, properties.getPassword()); + } + return builder; + } + + private String determineDatabaseName(R2dbcProperties properties) { + if (properties.isGenerateUniqueName()) { + return properties.determineUniqueName(); + } + if (StringUtils.hasLength(properties.getName())) { + return properties.getName(); + } + return null; + } + + private String determineEmbeddedDatabaseName(R2dbcProperties properties) { + String databaseName = determineDatabaseName(properties); + return (databaseName != null) ? databaseName : "testdb"; + } + + private String determineEmbeddedUsername(R2dbcProperties properties) { + String username = ifHasText(properties.getUsername()); + return (username != null) ? username : "sa"; + } + + private void configureIf(Builder optionsBuilder, ConnectionFactoryOptions originalOptions, + Option option, Supplier valueSupplier, Predicate setIf) { + if (originalOptions.hasOption(option)) { + return; + } + T value = valueSupplier.get(); + if (setIf.test(value)) { + optionsBuilder.option(option, value); + } + } + + private ConnectionFactoryBeanCreationException connectionFactoryBeanCreationException(String message, + R2dbcProperties properties, EmbeddedDatabaseConnection embeddedDatabaseConnection) { + return new ConnectionFactoryBeanCreationException(message, properties, embeddedDatabaseConnection); + } + + private String ifHasText(String candidate) { + return (StringUtils.hasText(candidate)) ? candidate : null; + } + + static class ConnectionFactoryBeanCreationException extends BeanCreationException { + + private final R2dbcProperties properties; + + private final EmbeddedDatabaseConnection embeddedDatabaseConnection; + + ConnectionFactoryBeanCreationException(String message, R2dbcProperties properties, + EmbeddedDatabaseConnection embeddedDatabaseConnection) { + super(message); + this.properties = properties; + this.embeddedDatabaseConnection = embeddedDatabaseConnection; + } + + EmbeddedDatabaseConnection getEmbeddedDatabaseConnection() { + return this.embeddedDatabaseConnection; + } + + R2dbcProperties getProperties() { + return this.properties; + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/EmbeddedDatabaseConnection.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/EmbeddedDatabaseConnection.java index 8637d74a18..056ec57059 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/EmbeddedDatabaseConnection.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/EmbeddedDatabaseConnection.java @@ -25,7 +25,10 @@ import org.springframework.util.ClassUtils; * @author Mark Paluch * @author Stephane Nicoll * @since 2.3.0 + * @deprecated since 2.5.0 in favor of + * {@link org.springframework.boot.r2dbc.EmbeddedDatabaseConnection} */ +@Deprecated public enum EmbeddedDatabaseConnection { /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryBuilderTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryBuilderTests.java index 713e2afc86..61af160d59 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryBuilderTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryBuilderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-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. @@ -20,7 +20,7 @@ import io.r2dbc.spi.ConnectionFactoryOptions; import io.r2dbc.spi.Option; import org.junit.jupiter.api.Test; -import org.springframework.boot.autoconfigure.r2dbc.ConnectionFactoryBuilder.ConnectionFactoryBeanCreationException; +import org.springframework.boot.autoconfigure.r2dbc.ConnectionFactoryOptionsInitializer.ConnectionFactoryBeanCreationException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -33,6 +33,7 @@ import static org.assertj.core.api.Assertions.fail; * @author Tadaya Tsuyukubo * @author Stephane Nicoll */ +@Deprecated class ConnectionFactoryBuilderTests { @Test @@ -51,7 +52,8 @@ class ConnectionFactoryBuilderTests { fail("Should have thrown a " + ConnectionFactoryBeanCreationException.class.getName()); } catch (ConnectionFactoryBeanCreationException ex) { - assertThat(ex.getEmbeddedDatabaseConnection()).isEqualTo(EmbeddedDatabaseConnection.NONE); + assertThat(ex.getEmbeddedDatabaseConnection()) + .isEqualTo(org.springframework.boot.r2dbc.EmbeddedDatabaseConnection.NONE); assertThat(ex.getProperties()).isSameAs(properties); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcAutoConfigurationTests.java index 61cafd2c89..499c02792e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-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. @@ -35,6 +35,7 @@ import org.springframework.beans.factory.BeanCreationException; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.r2dbc.SimpleConnectionFactoryProvider.SimpleTestConnectionFactory; +import org.springframework.boot.r2dbc.EmbeddedDatabaseConnection; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; diff --git a/spring-boot-project/spring-boot/build.gradle b/spring-boot-project/spring-boot/build.gradle index 4ef5f337e5..259d8d4c9f 100644 --- a/spring-boot-project/spring-boot/build.gradle +++ b/spring-boot-project/spring-boot/build.gradle @@ -63,6 +63,7 @@ dependencies { optional("org.springframework:spring-messaging") optional("org.springframework:spring-orm") optional("org.springframework:spring-oxm") + optional("org.springframework:spring-r2dbc") optional("org.springframework:spring-test") optional("org.springframework:spring-web") optional("org.springframework:spring-webflux") @@ -83,6 +84,7 @@ dependencies { testImplementation("com.squareup.okhttp3:okhttp") testImplementation("com.sun.xml.messaging.saaj:saaj-impl") testImplementation("io.projectreactor:reactor-test") + testImplementation("io.r2dbc:r2dbc-h2") testImplementation("javax.xml.ws:jaxws-api") testImplementation("mysql:mysql-connector-java") testImplementation("net.sourceforge.jtds:jtds") diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/r2dbc/ConnectionFactoryBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/r2dbc/ConnectionFactoryBuilder.java new file mode 100644 index 0000000000..40e66a7af1 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/r2dbc/ConnectionFactoryBuilder.java @@ -0,0 +1,137 @@ +/* + * Copyright 2012-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.boot.r2dbc; + +import java.util.function.Consumer; + +import io.r2dbc.spi.ConnectionFactories; +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.ConnectionFactoryOptions; +import io.r2dbc.spi.ConnectionFactoryOptions.Builder; + +import org.springframework.util.Assert; + +/** + * Builder for {@link ConnectionFactory}. + * + * @author Mark Paluch + * @author Tadaya Tsuyukubo + * @author Stephane Nicoll + * @since 2.5.0 + */ +public final class ConnectionFactoryBuilder { + + private final Builder optionsBuilder; + + private ConnectionFactoryBuilder(Builder optionsBuilder) { + this.optionsBuilder = optionsBuilder; + } + + /** + * Initialize a new {@link ConnectionFactoryBuilder} based on the specified R2DBC url. + * @param url the url to use + * @return a new builder initialized with the options exposed in the specified url + * @see EmbeddedDatabaseConnection#getUrl(String) + */ + public static ConnectionFactoryBuilder withUrl(String url) { + Assert.hasText(url, () -> "Url must not be null"); + return withOptions(ConnectionFactoryOptions.parse(url).mutate()); + } + + /** + * Initialize a new {@link ConnectionFactoryBuilder} based on the specified + * {@link Builder options}. + * @param options the options to use to initialize the builder + * @return a new builder initialized with the settings defined in the given + * {@link Builder options} + */ + public static ConnectionFactoryBuilder withOptions(Builder options) { + return new ConnectionFactoryBuilder(options); + } + + /** + * Configure additional options. + * @param options a {@link Consumer} to customize the options + * @return this for method chaining + */ + public ConnectionFactoryBuilder configure(Consumer options) { + options.accept(this.optionsBuilder); + return this; + } + + /** + * Configure the {@linkplain ConnectionFactoryOptions#USER username}. + * @param username the connection factory username + * @return this for method chaining + */ + public ConnectionFactoryBuilder username(String username) { + return configure((options) -> options.option(ConnectionFactoryOptions.USER, username)); + } + + /** + * Configure the {@linkplain ConnectionFactoryOptions#PASSWORD password}. + * @param password the connection factory password + * @return this for method chaining + */ + public ConnectionFactoryBuilder password(CharSequence password) { + return configure((options) -> options.option(ConnectionFactoryOptions.PASSWORD, password)); + } + + /** + * Configure the {@linkplain ConnectionFactoryOptions#HOST host name}. + * @param host the connection factory hostname + * @return this for method chaining + */ + public ConnectionFactoryBuilder hostname(String host) { + return configure((options) -> options.option(ConnectionFactoryOptions.HOST, host)); + } + + /** + * Configure the {@linkplain ConnectionFactoryOptions#PORT port}. + * @param port the connection factory port + * @return this for method chaining + */ + public ConnectionFactoryBuilder port(int port) { + return configure((options) -> options.option(ConnectionFactoryOptions.PORT, port)); + } + + /** + * Configure the {@linkplain ConnectionFactoryOptions#DATABASE database}. + * @param database the connection factory database + * @return this for method chaining + */ + public ConnectionFactoryBuilder database(String database) { + return configure((options) -> options.option(ConnectionFactoryOptions.DATABASE, database)); + } + + /** + * Build a {@link ConnectionFactory} based on the state of this builder. + * @return a connection factory + */ + public ConnectionFactory build() { + return ConnectionFactories.get(buildOptions()); + } + + /** + * Build a {@link ConnectionFactoryOptions} based on the state of this builder. + * @return the options + */ + public ConnectionFactoryOptions buildOptions() { + return this.optionsBuilder.build(); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/r2dbc/EmbeddedDatabaseConnection.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/r2dbc/EmbeddedDatabaseConnection.java new file mode 100644 index 0000000000..18b664d2a6 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/r2dbc/EmbeddedDatabaseConnection.java @@ -0,0 +1,84 @@ +/* + * Copyright 2012-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.boot.r2dbc; + +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Connection details for embedded databases compatible with r2dbc. + * + * @author Mark Paluch + * @author Stephane Nicoll + * @since 2.5.0 + */ +public enum EmbeddedDatabaseConnection { + + /** + * No Connection. + */ + NONE(null, null), + + /** + * H2 Database Connection. + */ + H2("io.r2dbc.h2.H2ConnectionFactoryProvider", + "r2dbc:h2:mem:///%s?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"); + + private final String driverClassName; + + private final String url; + + EmbeddedDatabaseConnection(String driverClassName, String url) { + this.driverClassName = driverClassName; + this.url = url; + } + + /** + * Returns the driver class name. + * @return the driver class name + */ + public String getDriverClassName() { + return this.driverClassName; + } + + /** + * Returns the R2DBC URL for the connection using the specified {@code databaseName}. + * @param databaseName the name of the database + * @return the connection URL + */ + public String getUrl(String databaseName) { + Assert.hasText(databaseName, "DatabaseName must not be empty"); + return (this.url != null) ? String.format(this.url, databaseName) : null; + } + + /** + * Returns the most suitable {@link EmbeddedDatabaseConnection} for the given class + * loader. + * @param classLoader the class loader used to check for classes + * @return an {@link EmbeddedDatabaseConnection} or {@link #NONE}. + */ + public static EmbeddedDatabaseConnection get(ClassLoader classLoader) { + for (EmbeddedDatabaseConnection candidate : EmbeddedDatabaseConnection.values()) { + if (candidate != NONE && ClassUtils.isPresent(candidate.getDriverClassName(), classLoader)) { + return candidate; + } + } + return NONE; + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/r2dbc/package-info.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/r2dbc/package-info.java new file mode 100644 index 0000000000..6344ee642d --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/r2dbc/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-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. + */ + +/** + * Support for R2DBC connectivity. + */ +package org.springframework.boot.r2dbc; diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/r2dbc/ConnectionFactoryBuilderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/r2dbc/ConnectionFactoryBuilderTests.java new file mode 100644 index 0000000000..2724f1a8b0 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/r2dbc/ConnectionFactoryBuilderTests.java @@ -0,0 +1,123 @@ +/* + * Copyright 2012-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.boot.r2dbc; + +import java.util.UUID; + +import io.r2dbc.h2.H2ConnectionFactoryMetadata; +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.ConnectionFactoryOptions; +import io.r2dbc.spi.Option; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link ConnectionFactoryBuilder}. + * + * @author Mark Paluch + * @author Tadaya Tsuyukubo + * @author Stephane Nicoll + */ +class ConnectionFactoryBuilderTests { + + @Test + void createWithNullUrlShouldFail() { + assertThatIllegalArgumentException().isThrownBy(() -> ConnectionFactoryBuilder.withUrl(null)); + } + + @Test + void createWithEmptyUrlShouldFail() { + assertThatIllegalArgumentException().isThrownBy(() -> ConnectionFactoryBuilder.withUrl(" ")); + } + + @Test + void createWithEmbeddedConnectionNoneShouldFail() { + assertThatIllegalArgumentException() + .isThrownBy(() -> ConnectionFactoryBuilder.withUrl(EmbeddedDatabaseConnection.NONE.getUrl("test"))); + } + + @Test + void buildOptionsWithBasicUrlShouldExposeOptions() { + ConnectionFactoryOptions options = ConnectionFactoryBuilder.withUrl("r2dbc:simple://:pool:").buildOptions(); + assertThat(options.hasOption(ConnectionFactoryOptions.USER)).isFalse(); + assertThat(options.hasOption(ConnectionFactoryOptions.PASSWORD)).isFalse(); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.DRIVER)).isEqualTo("simple"); + } + + @Test + void buildOptionsWithEmbeddedConnectionH2ShouldExposeOptions() { + ConnectionFactoryOptions options = ConnectionFactoryBuilder + .withUrl(EmbeddedDatabaseConnection.H2.getUrl("testdb")).buildOptions(); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.DRIVER)).isEqualTo("h2"); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.PROTOCOL)).isEqualTo("mem"); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.DATABASE)).isEqualTo("testdb"); + assertThat(options.hasOption(ConnectionFactoryOptions.HOST)).isFalse(); + assertThat(options.hasOption(ConnectionFactoryOptions.PORT)).isFalse(); + assertThat(options.hasOption(ConnectionFactoryOptions.USER)).isFalse(); + assertThat(options.hasOption(ConnectionFactoryOptions.PASSWORD)).isFalse(); + assertThat(options.getValue(Option.valueOf("options"))) + .isEqualTo("DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"); + } + + @Test + void buildOptionsWithCompleteUrlShouldExposeOptions() { + ConnectionFactoryOptions options = ConnectionFactoryBuilder + .withUrl("r2dbc:simple:proto://user:password@myhost:4711/mydatabase").buildOptions(); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.DRIVER)).isEqualTo("simple"); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.PROTOCOL)).isEqualTo("proto"); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.USER)).isEqualTo("user"); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.PASSWORD)).isEqualTo("password"); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.HOST)).isEqualTo("myhost"); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.PORT)).isEqualTo(4711); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.DATABASE)).isEqualTo("mydatabase"); + } + + @Test + void buildOptionsWithSpecificSettingsShouldOverrideUrlOptions() { + ConnectionFactoryOptions options = ConnectionFactoryBuilder + .withUrl("r2dbc:simple://user:password@myhost/mydatabase").username("another-user") + .password("another-password").hostname("another-host").port(1234).database("another-database") + .buildOptions(); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.USER)).isEqualTo("another-user"); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.PASSWORD)).isEqualTo("another-password"); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.HOST)).isEqualTo("another-host"); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.PORT)).isEqualTo(1234); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.DATABASE)).isEqualTo("another-database"); + } + + @Test + void buildOptionsWithDriverPropertiesShouldExposeOptions() { + ConnectionFactoryOptions options = ConnectionFactoryBuilder.withUrl("r2dbc:simple://user:password@myhost") + .configure( + (o) -> o.option(Option.valueOf("simpleOne"), "one").option(Option.valueOf("simpleTwo"), "two")) + .buildOptions(); + assertThat(options.getRequiredValue(Option.valueOf("simpleOne"))).isEqualTo("one"); + assertThat(options.getRequiredValue(Option.valueOf("simpleTwo"))).isEqualTo("two"); + } + + @Test + void buildShouldExposeConnectionFactory() { + String databaseName = UUID.randomUUID().toString(); + ConnectionFactory connectionFactory = ConnectionFactoryBuilder + .withUrl(EmbeddedDatabaseConnection.H2.getUrl(databaseName)).build(); + assertThat(connectionFactory).isNotNull(); + assertThat(connectionFactory.getMetadata().getName()).isEqualTo(H2ConnectionFactoryMetadata.NAME); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/r2dbc/EmbeddedDatabaseConnectionTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/r2dbc/EmbeddedDatabaseConnectionTests.java new file mode 100644 index 0000000000..a6ca5d2f93 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/r2dbc/EmbeddedDatabaseConnectionTests.java @@ -0,0 +1,81 @@ +/* + * Copyright 2012-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.boot.r2dbc; + +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Arrays; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link EmbeddedDatabaseConnection}. + * + * @author Stephane Nicoll + */ +class EmbeddedDatabaseConnectionTests { + + @ParameterizedTest + @MethodSource("urlParameters") + void getUrlWithTestDatabase(EmbeddedDatabaseConnection connection, String expectUrl) { + assertThat(connection.getUrl("test-database")).isEqualTo(expectUrl); + } + + @Test + void getReturnsH2ByDefault() { + assertThat(EmbeddedDatabaseConnection.get(EmbeddedDatabaseConnectionTests.class.getClassLoader())) + .isEqualTo(EmbeddedDatabaseConnection.H2); + } + + @Test + void getWhenH2IsNotOnTheClasspathReturnsNone() { + assertThat(EmbeddedDatabaseConnection.get(new HidePackagesClassLoader("io.r2dbc.h2"))) + .isEqualTo(EmbeddedDatabaseConnection.NONE); + } + + static Stream urlParameters() { + return Stream.of(Arguments.arguments(EmbeddedDatabaseConnection.NONE, null), + Arguments.arguments(EmbeddedDatabaseConnection.H2, + "r2dbc:h2:mem:///test-database?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE")); + } + + private static class HidePackagesClassLoader extends URLClassLoader { + + private final String[] hiddenPackages; + + HidePackagesClassLoader(String... hiddenPackages) { + super(new URL[0], EmbeddedDatabaseConnectionTests.HidePackagesClassLoader.class.getClassLoader()); + this.hiddenPackages = hiddenPackages; + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (Arrays.stream(this.hiddenPackages).anyMatch(name::startsWith)) { + throw new ClassNotFoundException(); + } + return super.loadClass(name, resolve); + } + + } + +}