diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/PropertiesZipkinConnectionDetails.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/PropertiesZipkinConnectionDetails.java new file mode 100644 index 0000000000..3997844031 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/PropertiesZipkinConnectionDetails.java @@ -0,0 +1,39 @@ +/* + * 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.actuate.autoconfigure.tracing.zipkin; + +/** + * Adapts {@link ZipkinProperties} to {@link ZipkinConnectionDetails}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class PropertiesZipkinConnectionDetails implements ZipkinConnectionDetails { + + private final ZipkinProperties properties; + + PropertiesZipkinConnectionDetails(ZipkinProperties properties) { + this.properties = properties; + } + + @Override + public String getSpanEndpoint() { + return this.properties.getEndpoint(); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurations.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurations.java index f61d4ce2d9..2a58cef528 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurations.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurations.java @@ -59,11 +59,14 @@ class ZipkinConfigurations { @Bean @ConditionalOnMissingBean(Sender.class) - URLConnectionSender urlConnectionSender(ZipkinProperties properties) { + URLConnectionSender urlConnectionSender(ZipkinProperties properties, + ObjectProvider connectionDetailsProvider) { + ZipkinConnectionDetails connectionDetails = connectionDetailsProvider + .getIfAvailable(() -> new PropertiesZipkinConnectionDetails(properties)); URLConnectionSender.Builder builder = URLConnectionSender.newBuilder(); builder.connectTimeout((int) properties.getConnectTimeout().toMillis()); builder.readTimeout((int) properties.getReadTimeout().toMillis()); - builder.endpoint(properties.getEndpoint()); + builder.endpoint(connectionDetails.getSpanEndpoint()); return builder.build(); } @@ -77,12 +80,15 @@ class ZipkinConfigurations { @Bean @ConditionalOnMissingBean(Sender.class) ZipkinRestTemplateSender restTemplateSender(ZipkinProperties properties, - ObjectProvider customizers) { + ObjectProvider customizers, + ObjectProvider connectionDetailsProvider) { + ZipkinConnectionDetails connectionDetails = connectionDetailsProvider + .getIfAvailable(() -> new PropertiesZipkinConnectionDetails(properties)); RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder() .setConnectTimeout(properties.getConnectTimeout()) .setReadTimeout(properties.getReadTimeout()); restTemplateBuilder = applyCustomizers(restTemplateBuilder, customizers); - return new ZipkinRestTemplateSender(properties.getEndpoint(), restTemplateBuilder.build()); + return new ZipkinRestTemplateSender(connectionDetails.getSpanEndpoint(), restTemplateBuilder.build()); } private RestTemplateBuilder applyCustomizers(RestTemplateBuilder restTemplateBuilder, @@ -106,10 +112,13 @@ class ZipkinConfigurations { @Bean @ConditionalOnMissingBean(Sender.class) ZipkinWebClientSender webClientSender(ZipkinProperties properties, - ObjectProvider customizers) { + ObjectProvider customizers, + ObjectProvider connectionDetailsProvider) { + ZipkinConnectionDetails connectionDetails = connectionDetailsProvider + .getIfAvailable(() -> new PropertiesZipkinConnectionDetails(properties)); WebClient.Builder builder = WebClient.builder(); customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); - return new ZipkinWebClientSender(properties.getEndpoint(), builder.build()); + return new ZipkinWebClientSender(connectionDetails.getSpanEndpoint(), builder.build()); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConnectionDetails.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConnectionDetails.java new file mode 100644 index 0000000000..f5cc8ce43c --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConnectionDetails.java @@ -0,0 +1,35 @@ +/* + * 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.actuate.autoconfigure.tracing.zipkin; + +import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; + +/** + * Details required to establish a connection to a Zipkin server. + * + * @author Moritz Halbritter + * @since 3.1.0 + */ +public interface ZipkinConnectionDetails extends ConnectionDetails { + + /** + * The endpoint for the span reporting. + * @return the endpoint + */ + String getSpanEndpoint(); + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/build.gradle b/spring-boot-project/spring-boot-autoconfigure/build.gradle index 9cd8994da6..dc5e917042 100644 --- a/spring-boot-project/spring-boot-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-autoconfigure/build.gradle @@ -236,6 +236,8 @@ dependencies { testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("org.mockito:mockito-core") testImplementation("org.mockito:mockito-junit-jupiter") + testImplementation("org.postgresql:postgresql") + testImplementation("org.postgresql:r2dbc-postgresql") testImplementation("org.skyscreamer:jsonassert") testImplementation("org.springframework:spring-test") testImplementation("org.springframework:spring-core-test") diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/AbstractConnectionFactoryConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/AbstractConnectionFactoryConfigurer.java index 3a25c78554..d5f1a12c66 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/AbstractConnectionFactoryConfigurer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/AbstractConnectionFactoryConfigurer.java @@ -16,6 +16,8 @@ package org.springframework.boot.autoconfigure.amqp; +import java.util.stream.Collectors; + import org.springframework.amqp.rabbit.connection.AbstractConnectionFactory; import org.springframework.amqp.rabbit.connection.ConnectionNameStrategy; import org.springframework.boot.context.properties.PropertyMapper; @@ -27,6 +29,9 @@ import org.springframework.util.Assert; * * @param the connection factory type. * @author Chris Bono + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb * @since 2.6.0 */ public abstract class AbstractConnectionFactoryConfigurer { @@ -35,9 +40,31 @@ public abstract class AbstractConnectionFactoryConfigurer address.host() + ":" + address.port()) + .collect(Collectors.joining(",")); + map.from(addresses).to(connectionFactory::setAddresses); map.from(this.rabbitProperties::getAddressShuffleMode) .whenNonNull() .to(connectionFactory::setAddressShuffleMode); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/CachingConnectionFactoryConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/CachingConnectionFactoryConfigurer.java index 97ab3d661b..677ddfc3c2 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/CachingConnectionFactoryConfigurer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/CachingConnectionFactoryConfigurer.java @@ -25,12 +25,32 @@ import org.springframework.boot.context.properties.PropertyMapper; * Configures Rabbit {@link CachingConnectionFactory} with sensible defaults. * * @author Chris Bono + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb * @since 2.6.0 */ public class CachingConnectionFactoryConfigurer extends AbstractConnectionFactoryConfigurer { + /** + * Creates a new configurer that will configure the connection factory using the given + * {@code properties}. + * @param properties the properties to use to configure the connection factory + */ public CachingConnectionFactoryConfigurer(RabbitProperties properties) { - super(properties); + this(properties, new PropertiesRabbitConnectionDetails(properties)); + } + + /** + * Creates a new configurer that will configure the connection factory using the given + * {@code properties} and {@code connectionDetails}, with the latter taking priority. + * @param properties the properties to use to configure the connection factory + * @param connectionDetails the connection details to use to configure the connection + * factory + * @since 3.1.0 + */ + public CachingConnectionFactoryConfigurer(RabbitProperties properties, RabbitConnectionDetails connectionDetails) { + super(properties, connectionDetails); } @Override diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/PropertiesRabbitConnectionDetails.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/PropertiesRabbitConnectionDetails.java new file mode 100644 index 0000000000..1b3359afc0 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/PropertiesRabbitConnectionDetails.java @@ -0,0 +1,63 @@ +/* + * 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.autoconfigure.amqp; + +import java.util.ArrayList; +import java.util.List; + +/** + * Adapts {@link RabbitProperties} to {@link RabbitConnectionDetails}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + */ +public class PropertiesRabbitConnectionDetails implements RabbitConnectionDetails { + + private final RabbitProperties properties; + + public PropertiesRabbitConnectionDetails(RabbitProperties properties) { + this.properties = properties; + } + + @Override + public String getUsername() { + return this.properties.determineUsername(); + } + + @Override + public String getPassword() { + return this.properties.determinePassword(); + } + + @Override + public String getVirtualHost() { + return this.properties.determineVirtualHost(); + } + + @Override + public List
getAddresses() { + List
addresses = new ArrayList<>(); + for (String address : this.properties.determineAddresses().split(",")) { + String[] components = address.split(":"); + addresses.add(new Address(components[0], Integer.parseInt(components[1]))); + } + return addresses; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfiguration.java index c14a74c9eb..78477031a2 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfiguration.java @@ -59,21 +59,6 @@ import org.springframework.core.io.ResourceLoader; *
  • {@link org.springframework.amqp.core.AmqpAdmin } instance as long as * {@literal spring.rabbitmq.dynamic=true}.
  • * - *

    - * The {@link org.springframework.amqp.rabbit.connection.CachingConnectionFactory} honors - * the following properties: - *

      - *
    • {@literal spring.rabbitmq.port} is used to specify the port to which the client - * should connect, and defaults to 5672.
    • - *
    • {@literal spring.rabbitmq.username} is used to specify the (optional) username. - *
    • - *
    • {@literal spring.rabbitmq.password} is used to specify the (optional) password. - *
    • - *
    • {@literal spring.rabbitmq.host} is used to specify the host, and defaults to - * {@literal localhost}.
    • - *
    • {@literal spring.rabbitmq.virtualHost} is used to specify the (optional) virtual - * host to which the client should connect.
    • - *
    * * @author Greg Turnquist * @author Josh Long @@ -82,6 +67,8 @@ import org.springframework.core.io.ResourceLoader; * @author Phillip Webb * @author Artsiom Yudovin * @author Chris Bono + * @author Moritz Halbritter + * @author Andy Wilkinson * @since 1.0.0 */ @AutoConfiguration @@ -93,13 +80,24 @@ public class RabbitAutoConfiguration { @Configuration(proxyBeanMethods = false) protected static class RabbitConnectionFactoryCreator { + private final RabbitProperties properties; + + private final RabbitConnectionDetails connectionDetails; + + protected RabbitConnectionFactoryCreator(RabbitProperties properties, + ObjectProvider connectionDetails) { + this.properties = properties; + this.connectionDetails = connectionDetails + .getIfAvailable(() -> new PropertiesRabbitConnectionDetails(properties)); + } + @Bean @ConditionalOnMissingBean - RabbitConnectionFactoryBeanConfigurer rabbitConnectionFactoryBeanConfigurer(RabbitProperties properties, - ResourceLoader resourceLoader, ObjectProvider credentialsProvider, + RabbitConnectionFactoryBeanConfigurer rabbitConnectionFactoryBeanConfigurer(ResourceLoader resourceLoader, + ObjectProvider credentialsProvider, ObjectProvider credentialsRefreshService) { RabbitConnectionFactoryBeanConfigurer configurer = new RabbitConnectionFactoryBeanConfigurer(resourceLoader, - properties); + this.properties, this.connectionDetails); configurer.setCredentialsProvider(credentialsProvider.getIfUnique()); configurer.setCredentialsRefreshService(credentialsRefreshService.getIfUnique()); return configurer; @@ -107,9 +105,10 @@ public class RabbitAutoConfiguration { @Bean @ConditionalOnMissingBean - CachingConnectionFactoryConfigurer rabbitConnectionFactoryConfigurer(RabbitProperties rabbitProperties, + CachingConnectionFactoryConfigurer rabbitConnectionFactoryConfigurer( ObjectProvider connectionNameStrategy) { - CachingConnectionFactoryConfigurer configurer = new CachingConnectionFactoryConfigurer(rabbitProperties); + CachingConnectionFactoryConfigurer configurer = new CachingConnectionFactoryConfigurer(this.properties, + this.connectionDetails); configurer.setConnectionNameStrategy(connectionNameStrategy.getIfUnique()); return configurer; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitConnectionDetails.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitConnectionDetails.java new file mode 100644 index 0000000000..4cdc51c215 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitConnectionDetails.java @@ -0,0 +1,85 @@ +/* + * 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.autoconfigure.amqp; + +import java.util.List; + +import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; +import org.springframework.util.Assert; + +/** + * Details required to establish a connection to a RabbitMQ service. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + */ +public interface RabbitConnectionDetails extends ConnectionDetails { + + /** + * Login user to authenticate to the broker. + * @return the login user to authenticate to the broker or {@code null} + */ + default String getUsername() { + return null; + } + + /** + * Login to authenticate against the broker. + * @return the login to authenticate against the broker or {@code null} + */ + default String getPassword() { + return null; + } + + /** + * Virtual host to use when connecting to the broker. + * @return the virtual host to use when connecting to the broker or {@code null} + */ + default String getVirtualHost() { + return null; + } + + /** + * List of addresses to which the client should connect. Must return at least one + * address. + * @return the list of addresses to which the client should connect + */ + List
    getAddresses(); + + /** + * Returns the first address. + * @return the first address + * @throws IllegalStateException if the address list is empty + */ + default Address getFirstAddress() { + List
    addresses = getAddresses(); + Assert.state(!addresses.isEmpty(), "Address list is empty"); + return addresses.get(0); + } + + /** + * A RabbitMQ address. + * + * @param host the host + * @param port the port + */ + record Address(String host, int port) { + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitConnectionFactoryBeanConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitConnectionFactoryBeanConfigurer.java index 2c2149bc3e..f54e91ace5 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitConnectionFactoryBeanConfigurer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitConnectionFactoryBeanConfigurer.java @@ -22,6 +22,7 @@ import com.rabbitmq.client.impl.CredentialsProvider; import com.rabbitmq.client.impl.CredentialsRefreshService; import org.springframework.amqp.rabbit.connection.RabbitConnectionFactoryBean; +import org.springframework.boot.autoconfigure.amqp.RabbitConnectionDetails.Address; import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.core.io.ResourceLoader; import org.springframework.util.Assert; @@ -30,6 +31,9 @@ import org.springframework.util.Assert; * Configures {@link RabbitConnectionFactoryBean} with sensible defaults. * * @author Chris Bono + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb * @since 2.6.0 */ public class RabbitConnectionFactoryBeanConfigurer { @@ -38,13 +42,39 @@ public class RabbitConnectionFactoryBeanConfigurer { private final ResourceLoader resourceLoader; + private final RabbitConnectionDetails connectionDetails; + private CredentialsProvider credentialsProvider; private CredentialsRefreshService credentialsRefreshService; + /** + * Creates a new configurer that will use the given {@code resourceLoader} and + * {@code properties}. + * @param resourceLoader the resource loader + * @param properties the properties + */ public RabbitConnectionFactoryBeanConfigurer(ResourceLoader resourceLoader, RabbitProperties properties) { + this(resourceLoader, properties, new PropertiesRabbitConnectionDetails(properties)); + } + + /** + * Creates a new configurer that will use the given {@code resourceLoader}, + * {@code properties}, and {@code connectionDetails}. The connection details have + * priority over the properties. + * @param resourceLoader the resource loader + * @param properties the properties + * @param connectionDetails the connection details. + * @since 3.1.0 + */ + public RabbitConnectionFactoryBeanConfigurer(ResourceLoader resourceLoader, RabbitProperties properties, + RabbitConnectionDetails connectionDetails) { + Assert.notNull(resourceLoader, "ResourceLoader must not be null"); + Assert.notNull(properties, "Properties must not be null"); + Assert.notNull(connectionDetails, "ConnectionDetails must not be null"); this.resourceLoader = resourceLoader; this.rabbitProperties = properties; + this.connectionDetails = connectionDetails; } public void setCredentialsProvider(CredentialsProvider credentialsProvider) { @@ -65,12 +95,13 @@ public class RabbitConnectionFactoryBeanConfigurer { public void configure(RabbitConnectionFactoryBean factory) { Assert.notNull(factory, "RabbitConnectionFactoryBean must not be null"); factory.setResourceLoader(this.resourceLoader); + Address address = this.connectionDetails.getFirstAddress(); PropertyMapper map = PropertyMapper.get(); - map.from(this.rabbitProperties::determineHost).whenNonNull().to(factory::setHost); - map.from(this.rabbitProperties::determinePort).to(factory::setPort); - map.from(this.rabbitProperties::determineUsername).whenNonNull().to(factory::setUsername); - map.from(this.rabbitProperties::determinePassword).whenNonNull().to(factory::setPassword); - map.from(this.rabbitProperties::determineVirtualHost).whenNonNull().to(factory::setVirtualHost); + map.from(address::host).whenNonNull().to(factory::setHost); + map.from(address::port).to(factory::setPort); + map.from(this.connectionDetails::getUsername).whenNonNull().to(factory::setUsername); + map.from(this.connectionDetails::getPassword).whenNonNull().to(factory::setPassword); + map.from(this.connectionDetails::getVirtualHost).whenNonNull().to(factory::setVirtualHost); map.from(this.rabbitProperties::getRequestedHeartbeat) .whenNonNull() .asInt(Duration::getSeconds) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraAutoConfiguration.java index 89d290d13b..1acd74ca42 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraAutoConfiguration.java @@ -23,7 +23,6 @@ import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.function.Supplier; import javax.net.ssl.SSLContext; @@ -64,10 +63,13 @@ import org.springframework.core.io.Resource; * @author Stephane Nicoll * @author Steffen F. Qvistgaard * @author Ittay Stern + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb * @since 1.3.0 */ @AutoConfiguration -@ConditionalOnClass({ CqlSession.class }) +@ConditionalOnClass(CqlSession.class) @EnableConfigurationProperties(CassandraProperties.class) public class CassandraAutoConfiguration { @@ -80,6 +82,17 @@ public class CassandraAutoConfiguration { SPRING_BOOT_DEFAULTS = options.build(); } + private final CassandraProperties properties; + + private final CassandraConnectionDetails connectionDetails; + + CassandraAutoConfiguration(CassandraProperties properties, + ObjectProvider connectionDetails) { + this.properties = properties; + this.connectionDetails = connectionDetails + .getIfAvailable(() -> new PropertiesCassandraConnectionDetails(properties)); + } + @Bean @ConditionalOnMissingBean @Lazy @@ -90,24 +103,25 @@ public class CassandraAutoConfiguration { @Bean @ConditionalOnMissingBean @Scope("prototype") - public CqlSessionBuilder cassandraSessionBuilder(CassandraProperties properties, - DriverConfigLoader driverConfigLoader, ObjectProvider builderCustomizers) { + public CqlSessionBuilder cassandraSessionBuilder(DriverConfigLoader driverConfigLoader, + ObjectProvider builderCustomizers) { CqlSessionBuilder builder = CqlSession.builder().withConfigLoader(driverConfigLoader); - configureAuthentication(properties, builder); - configureSsl(properties, builder); - builder.withKeyspace(properties.getKeyspaceName()); + configureAuthentication(builder); + configureSsl(builder); + builder.withKeyspace(this.properties.getKeyspaceName()); builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); return builder; } - private void configureAuthentication(CassandraProperties properties, CqlSessionBuilder builder) { - if (properties.getUsername() != null) { - builder.withAuthCredentials(properties.getUsername(), properties.getPassword()); + private void configureAuthentication(CqlSessionBuilder builder) { + String username = this.connectionDetails.getUsername(); + if (username != null) { + builder.withAuthCredentials(username, this.connectionDetails.getPassword()); } } - private void configureSsl(CassandraProperties properties, CqlSessionBuilder builder) { - if (properties.isSsl()) { + private void configureSsl(CqlSessionBuilder builder) { + if (this.connectionDetails instanceof PropertiesCassandraConnectionDetails && this.properties.isSsl()) { try { builder.withSslContext(SSLContext.getDefault()); } @@ -119,20 +133,20 @@ public class CassandraAutoConfiguration { @Bean(destroyMethod = "") @ConditionalOnMissingBean - public DriverConfigLoader cassandraDriverConfigLoader(CassandraProperties properties, + public DriverConfigLoader cassandraDriverConfigLoader( ObjectProvider builderCustomizers) { ProgrammaticDriverConfigLoaderBuilder builder = new DefaultProgrammaticDriverConfigLoaderBuilder( - () -> cassandraConfiguration(properties), DefaultDriverConfigLoader.DEFAULT_ROOT_PATH); + () -> cassandraConfiguration(), DefaultDriverConfigLoader.DEFAULT_ROOT_PATH); builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); return builder.build(); } - private Config cassandraConfiguration(CassandraProperties properties) { + private Config cassandraConfiguration() { ConfigFactory.invalidateCaches(); Config config = ConfigFactory.defaultOverrides(); - config = config.withFallback(mapConfig(properties)); - if (properties.getConfig() != null) { - config = config.withFallback(loadConfig(properties.getConfig())); + config = config.withFallback(mapConfig()); + if (this.properties.getConfig() != null) { + config = config.withFallback(loadConfig(this.properties.getConfig())); } config = config.withFallback(SPRING_BOOT_DEFAULTS); config = config.withFallback(ConfigFactory.defaultReference()); @@ -148,32 +162,32 @@ public class CassandraAutoConfiguration { } } - private Config mapConfig(CassandraProperties properties) { + private Config mapConfig() { CassandraDriverOptions options = new CassandraDriverOptions(); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); - map.from(properties.getSessionName()) + map.from(this.properties.getSessionName()) .whenHasText() .to((sessionName) -> options.add(DefaultDriverOption.SESSION_NAME, sessionName)); - map.from(properties::getUsername) - .to((username) -> options.add(DefaultDriverOption.AUTH_PROVIDER_USER_NAME, username) - .add(DefaultDriverOption.AUTH_PROVIDER_PASSWORD, properties.getPassword())); - map.from(properties::getCompression) + map.from(this.connectionDetails.getUsername()) + .to((value) -> options.add(DefaultDriverOption.AUTH_PROVIDER_USER_NAME, value) + .add(DefaultDriverOption.AUTH_PROVIDER_PASSWORD, this.connectionDetails.getPassword())); + map.from(this.properties::getCompression) .to((compression) -> options.add(DefaultDriverOption.PROTOCOL_COMPRESSION, compression)); - mapConnectionOptions(properties, options); - mapPoolingOptions(properties, options); - mapRequestOptions(properties, options); - mapControlConnectionOptions(properties, options); - map.from(mapContactPoints(properties)) + mapConnectionOptions(options); + mapPoolingOptions(options); + mapRequestOptions(options); + mapControlConnectionOptions(options); + map.from(mapContactPoints()) .to((contactPoints) -> options.add(DefaultDriverOption.CONTACT_POINTS, contactPoints)); - map.from(properties.getLocalDatacenter()) + map.from(this.connectionDetails.getLocalDatacenter()) .whenHasText() .to((localDatacenter) -> options.add(DefaultDriverOption.LOAD_BALANCING_LOCAL_DATACENTER, localDatacenter)); return options.build(); } - private void mapConnectionOptions(CassandraProperties properties, CassandraDriverOptions options) { + private void mapConnectionOptions(CassandraDriverOptions options) { PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); - Connection connectionProperties = properties.getConnection(); + Connection connectionProperties = this.properties.getConnection(); map.from(connectionProperties::getConnectTimeout) .asInt(Duration::toMillis) .to((connectTimeout) -> options.add(DefaultDriverOption.CONNECTION_CONNECT_TIMEOUT, connectTimeout)); @@ -182,9 +196,9 @@ public class CassandraAutoConfiguration { .to((initQueryTimeout) -> options.add(DefaultDriverOption.CONNECTION_INIT_QUERY_TIMEOUT, initQueryTimeout)); } - private void mapPoolingOptions(CassandraProperties properties, CassandraDriverOptions options) { + private void mapPoolingOptions(CassandraDriverOptions options) { PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); - CassandraProperties.Pool poolProperties = properties.getPool(); + CassandraProperties.Pool poolProperties = this.properties.getPool(); map.from(poolProperties::getIdleTimeout) .asInt(Duration::toMillis) .to((idleTimeout) -> options.add(DefaultDriverOption.HEARTBEAT_TIMEOUT, idleTimeout)); @@ -193,9 +207,9 @@ public class CassandraAutoConfiguration { .to((heartBeatInterval) -> options.add(DefaultDriverOption.HEARTBEAT_INTERVAL, heartBeatInterval)); } - private void mapRequestOptions(CassandraProperties properties, CassandraDriverOptions options) { + private void mapRequestOptions(CassandraDriverOptions options) { PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); - Request requestProperties = properties.getRequest(); + Request requestProperties = this.properties.getRequest(); map.from(requestProperties::getTimeout) .asInt(Duration::toMillis) .to(((timeout) -> options.add(DefaultDriverOption.REQUEST_TIMEOUT, timeout))); @@ -222,40 +236,19 @@ public class CassandraAutoConfiguration { .to((drainInterval) -> options.add(DefaultDriverOption.REQUEST_THROTTLER_DRAIN_INTERVAL, drainInterval)); } - private void mapControlConnectionOptions(CassandraProperties properties, CassandraDriverOptions options) { + private void mapControlConnectionOptions(CassandraDriverOptions options) { PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); - Controlconnection controlProperties = properties.getControlconnection(); + Controlconnection controlProperties = this.properties.getControlconnection(); map.from(controlProperties::getTimeout) .asInt(Duration::toMillis) .to((timeout) -> options.add(DefaultDriverOption.CONTROL_CONNECTION_TIMEOUT, timeout)); } - private List mapContactPoints(CassandraProperties properties) { - if (properties.getContactPoints() != null) { - return properties.getContactPoints() - .stream() - .map((candidate) -> formatContactPoint(candidate, properties.getPort())) - .toList(); - } - return null; - } - - private String formatContactPoint(String candidate, int port) { - int i = candidate.lastIndexOf(':'); - if (i == -1 || !isPort(() -> candidate.substring(i + 1))) { - return String.format("%s:%s", candidate, port); - } - return candidate; - } - - private boolean isPort(Supplier value) { - try { - int i = Integer.parseInt(value.get()); - return i > 0 && i < 65535; - } - catch (Exception ex) { - return false; - } + private List mapContactPoints() { + return this.connectionDetails.getContactPoints() + .stream() + .map((node) -> node.host() + ":" + node.port()) + .toList(); } private static class CassandraDriverOptions { @@ -293,4 +286,61 @@ public class CassandraAutoConfiguration { } + /** + * Adapts {@link CassandraProperties} to {@link CassandraConnectionDetails}. + */ + private static final class PropertiesCassandraConnectionDetails implements CassandraConnectionDetails { + + private final CassandraProperties properties; + + private PropertiesCassandraConnectionDetails(CassandraProperties properties) { + this.properties = properties; + } + + @Override + public List getContactPoints() { + List contactPoints = this.properties.getContactPoints(); + return (contactPoints != null) ? contactPoints.stream().map(this::asNode).toList() + : Collections.emptyList(); + } + + @Override + public String getUsername() { + return this.properties.getUsername(); + } + + @Override + public String getPassword() { + return this.properties.getPassword(); + } + + @Override + public String getLocalDatacenter() { + return this.properties.getLocalDatacenter(); + } + + private Node asNode(String contactPoint) { + int i = contactPoint.lastIndexOf(':'); + if (i >= 0) { + String portCandidate = contactPoint.substring(i + 1); + Integer port = asPort(portCandidate); + if (port != null) { + return new Node(contactPoint.substring(0, i), port); + } + } + return new Node(contactPoint, this.properties.getPort()); + } + + private Integer asPort(String value) { + try { + int i = Integer.parseInt(value); + return (i > 0 && i < 65535) ? i : null; + } + catch (Exception ex) { + return null; + } + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraConnectionDetails.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraConnectionDetails.java new file mode 100644 index 0000000000..b02972269a --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraConnectionDetails.java @@ -0,0 +1,71 @@ +/* + * 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.autoconfigure.cassandra; + +import java.util.List; + +import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; + +/** + * Details required to establish a connection to a Cassandra service. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + */ +public interface CassandraConnectionDetails extends ConnectionDetails { + + /** + * Cluster node addresses. + * @return the cluster node addresses + */ + List getContactPoints(); + + /** + * Login user of the server. + * @return the login user of the server or {@code null} + */ + default String getUsername() { + return null; + } + + /** + * Login password of the server. + * @return the login password of the server or {@code null} + */ + default String getPassword() { + return null; + } + + /** + * Datacenter that is considered "local". Contact points should be from this + * datacenter. + * @return the datacenter that is considered "local" + */ + String getLocalDatacenter(); + + /** + * A Cassandra node. + * + * @param host the hostname + * @param port the port + */ + record Node(String host, int port) { + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfiguration.java index 11d8575e44..2a4d5fe7f9 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfiguration.java @@ -34,14 +34,18 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; +import org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration.CouchbaseCondition; import org.springframework.boot.autoconfigure.couchbase.CouchbaseProperties.Timeouts; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.util.ResourceUtils; @@ -52,34 +56,49 @@ import org.springframework.util.ResourceUtils; * @author Eddú Meléndez * @author Stephane Nicoll * @author Yulin Qin + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb * @since 1.4.0 */ @AutoConfiguration(after = JacksonAutoConfiguration.class) @ConditionalOnClass(Cluster.class) -@ConditionalOnProperty("spring.couchbase.connection-string") +@Conditional(CouchbaseCondition.class) @EnableConfigurationProperties(CouchbaseProperties.class) public class CouchbaseAutoConfiguration { + private final CouchbaseProperties properties; + + private final CouchbaseConnectionDetails connectionDetails; + + CouchbaseAutoConfiguration(CouchbaseProperties properties, + ObjectProvider connectionDetails) { + this.properties = properties; + this.connectionDetails = connectionDetails + .getIfAvailable(() -> new PropertiesCouchbaseConnectionDetails(properties)); + } + @Bean @ConditionalOnMissingBean - public ClusterEnvironment couchbaseClusterEnvironment(CouchbaseProperties properties, + public ClusterEnvironment couchbaseClusterEnvironment( ObjectProvider customizers) { - Builder builder = initializeEnvironmentBuilder(properties); + Builder builder = initializeEnvironmentBuilder(); customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); return builder.build(); } @Bean(destroyMethod = "disconnect") @ConditionalOnMissingBean - public Cluster couchbaseCluster(CouchbaseProperties properties, ClusterEnvironment couchbaseClusterEnvironment) { - ClusterOptions options = ClusterOptions.clusterOptions(properties.getUsername(), properties.getPassword()) + public Cluster couchbaseCluster(ClusterEnvironment couchbaseClusterEnvironment) { + ClusterOptions options = ClusterOptions + .clusterOptions(this.connectionDetails.getUsername(), this.connectionDetails.getPassword()) .environment(couchbaseClusterEnvironment); - return Cluster.connect(properties.getConnectionString(), options); + return Cluster.connect(this.connectionDetails.getConnectionString(), options); } - private ClusterEnvironment.Builder initializeEnvironmentBuilder(CouchbaseProperties properties) { + private ClusterEnvironment.Builder initializeEnvironmentBuilder() { ClusterEnvironment.Builder builder = ClusterEnvironment.builder(); - Timeouts timeouts = properties.getEnv().getTimeouts(); + Timeouts timeouts = this.properties.getEnv().getTimeouts(); builder.timeoutConfig((config) -> config.kvTimeout(timeouts.getKeyValue()) .analyticsTimeout(timeouts.getAnalytics()) .kvDurableTimeout(timeouts.getKeyValueDurable()) @@ -89,13 +108,14 @@ public class CouchbaseAutoConfiguration { .managementTimeout(timeouts.getManagement()) .connectTimeout(timeouts.getConnect()) .disconnectTimeout(timeouts.getDisconnect())); - CouchbaseProperties.Io io = properties.getEnv().getIo(); + CouchbaseProperties.Io io = this.properties.getEnv().getIo(); builder.ioConfig((config) -> config.maxHttpConnections(io.getMaxEndpoints()) .numKvConnections(io.getMinEndpoints()) .idleHttpConnectionTimeout(io.getIdleHttpConnectionTimeout())); - if (properties.getEnv().getSsl().getEnabled()) { + if ((this.connectionDetails instanceof PropertiesCouchbaseConnectionDetails) + && this.properties.getEnv().getSsl().getEnabled()) { builder.securityConfig((config) -> config.enableTls(true) - .trustManagerFactory(getTrustManagerFactory(properties.getEnv().getSsl()))); + .trustManagerFactory(getTrustManagerFactory(this.properties.getEnv().getSsl()))); } return builder; } @@ -157,4 +177,54 @@ public class CouchbaseAutoConfiguration { } + /** + * Condition that matches when {@code spring.couchbase.connection-string} has been + * configured or there is a {@link CouchbaseConnectionDetails} bean. + */ + static final class CouchbaseCondition extends AnyNestedCondition { + + CouchbaseCondition() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @ConditionalOnProperty(prefix = "spring.couchbase", name = "connection-string") + private static final class CouchbaseUrlCondition { + + } + + @ConditionalOnBean(CouchbaseConnectionDetails.class) + private static final class CouchbaseConnectionDetailsCondition { + + } + + } + + /** + * Adapts {@link CouchbaseProperties} to {@link CouchbaseConnectionDetails}. + */ + static final class PropertiesCouchbaseConnectionDetails implements CouchbaseConnectionDetails { + + private final CouchbaseProperties properties; + + PropertiesCouchbaseConnectionDetails(CouchbaseProperties properties) { + this.properties = properties; + } + + @Override + public String getConnectionString() { + return this.properties.getConnectionString(); + } + + @Override + public String getUsername() { + return this.properties.getUsername(); + } + + @Override + public String getPassword() { + return this.properties.getPassword(); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseConnectionDetails.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseConnectionDetails.java new file mode 100644 index 0000000000..f4ca1c76ee --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseConnectionDetails.java @@ -0,0 +1,49 @@ +/* + * 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.autoconfigure.couchbase; + +import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; + +/** + * Details required to establish a connection to a Couchbase service. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + */ +public interface CouchbaseConnectionDetails extends ConnectionDetails { + + /** + * Connection string used to locate the Couchbase cluster. + * @return the connection string used to locate the Couchbase cluster + */ + String getConnectionString(); + + /** + * Cluster username. + * @return the cluster username + */ + String getUsername(); + + /** + * Cluster password. + * @return the cluster password + */ + String getPassword(); + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/MongoDatabaseFactoryConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/MongoDatabaseFactoryConfiguration.java index 544bfcc17f..17f054ce56 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/MongoDatabaseFactoryConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/MongoDatabaseFactoryConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * 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. @@ -18,9 +18,12 @@ package org.springframework.boot.autoconfigure.data.mongo; import com.mongodb.client.MongoClient; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; +import org.springframework.boot.autoconfigure.mongo.MongoConnectionDetails; import org.springframework.boot.autoconfigure.mongo.MongoProperties; +import org.springframework.boot.autoconfigure.mongo.PropertiesMongoConnectionDetails; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.mongodb.MongoDatabaseFactory; @@ -32,6 +35,8 @@ import org.springframework.data.mongodb.core.SimpleMongoClientDatabaseFactory; * * @author Andy Wilkinson * @author Stephane Nicoll + * @author Moritz Halbritter + * @author Phillip Webb */ @Configuration(proxyBeanMethods = false) @ConditionalOnMissingBean(MongoDatabaseFactory.class) @@ -39,8 +44,12 @@ import org.springframework.data.mongodb.core.SimpleMongoClientDatabaseFactory; class MongoDatabaseFactoryConfiguration { @Bean - MongoDatabaseFactorySupport mongoDatabaseFactory(MongoClient mongoClient, MongoProperties properties) { - return new SimpleMongoClientDatabaseFactory(mongoClient, properties.getMongoClientDatabase()); + MongoDatabaseFactorySupport mongoDatabaseFactory(MongoClient mongoClient, MongoProperties properties, + ObjectProvider connectionDetails) { + return new SimpleMongoClientDatabaseFactory(mongoClient, + connectionDetails.getIfAvailable(() -> new PropertiesMongoConnectionDetails(properties)) + .getConnectionString() + .getDatabase()); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/MongoDatabaseFactoryDependentConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/MongoDatabaseFactoryDependentConfiguration.java index 9639b743b3..e82c6fb570 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/MongoDatabaseFactoryDependentConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/MongoDatabaseFactoryDependentConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * 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. @@ -20,10 +20,14 @@ import com.mongodb.ClientSessionOptions; import com.mongodb.client.ClientSession; import com.mongodb.client.MongoDatabase; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.mongo.MongoConnectionDetails; +import org.springframework.boot.autoconfigure.mongo.MongoConnectionDetails.GridFs; import org.springframework.boot.autoconfigure.mongo.MongoProperties; import org.springframework.boot.autoconfigure.mongo.MongoProperties.Gridfs; +import org.springframework.boot.autoconfigure.mongo.PropertiesMongoConnectionDetails; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.dao.DataAccessException; @@ -46,17 +50,13 @@ import org.springframework.util.StringUtils; * Configuration for Mongo-related beans that depend on a {@link MongoDatabaseFactory}. * * @author Andy Wilkinson + * @author Moritz Halbritter + * @author Phillip Webb */ @Configuration(proxyBeanMethods = false) @ConditionalOnBean(MongoDatabaseFactory.class) class MongoDatabaseFactoryDependentConfiguration { - private final MongoProperties properties; - - MongoDatabaseFactoryDependentConfiguration(MongoProperties properties) { - this.properties = properties; - } - @Bean @ConditionalOnMissingBean(MongoOperations.class) MongoTemplate mongoTemplate(MongoDatabaseFactory factory, MongoConverter converter) { @@ -75,31 +75,36 @@ class MongoDatabaseFactoryDependentConfiguration { @Bean @ConditionalOnMissingBean(GridFsOperations.class) - GridFsTemplate gridFsTemplate(MongoDatabaseFactory factory, MongoTemplate mongoTemplate) { - return new GridFsTemplate(new GridFsMongoDatabaseFactory(factory, this.properties), - mongoTemplate.getConverter(), this.properties.getGridfs().getBucket()); + GridFsTemplate gridFsTemplate(MongoProperties properties, MongoDatabaseFactory factory, MongoTemplate mongoTemplate, + ObjectProvider connectionDetailsProvider) { + MongoConnectionDetails connectionDetails = connectionDetailsProvider + .getIfAvailable(() -> new PropertiesMongoConnectionDetails(properties)); + return new GridFsTemplate(new GridFsMongoDatabaseFactory(factory, connectionDetails), + mongoTemplate.getConverter(), + (connectionDetails.getGridFs() != null) ? connectionDetails.getGridFs().getBucket() : null); } /** - * {@link MongoDatabaseFactory} decorator to respect {@link Gridfs#getDatabase()} if - * set. + * {@link MongoDatabaseFactory} decorator to respect {@link Gridfs#getDatabase()} or + * {@link GridFs#getGridFs()} from the {@link MongoConnectionDetails} if set. */ static class GridFsMongoDatabaseFactory implements MongoDatabaseFactory { private final MongoDatabaseFactory mongoDatabaseFactory; - private final MongoProperties properties; + private final MongoConnectionDetails connectionDetails; - GridFsMongoDatabaseFactory(MongoDatabaseFactory mongoDatabaseFactory, MongoProperties properties) { + GridFsMongoDatabaseFactory(MongoDatabaseFactory mongoDatabaseFactory, + MongoConnectionDetails connectionDetails) { Assert.notNull(mongoDatabaseFactory, "MongoDatabaseFactory must not be null"); - Assert.notNull(properties, "Properties must not be null"); + Assert.notNull(connectionDetails, "ConnectionDetails must not be null"); this.mongoDatabaseFactory = mongoDatabaseFactory; - this.properties = properties; + this.connectionDetails = connectionDetails; } @Override public MongoDatabase getMongoDatabase() throws DataAccessException { - String gridFsDatabase = this.properties.getGridfs().getDatabase(); + String gridFsDatabase = getGridFsDatabase(this.connectionDetails); if (StringUtils.hasText(gridFsDatabase)) { return this.mongoDatabaseFactory.getMongoDatabase(gridFsDatabase); } @@ -126,6 +131,10 @@ class MongoDatabaseFactoryDependentConfiguration { return this.mongoDatabaseFactory.withSession(session); } + private String getGridFsDatabase(MongoConnectionDetails connectionDetails) { + return (connectionDetails.getGridFs() != null) ? connectionDetails.getGridFs().getDatabase() : null; + } + } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/MongoReactiveDataAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/MongoReactiveDataAutoConfiguration.java index 332d5fb6d5..742270adc4 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/MongoReactiveDataAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/MongoReactiveDataAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * 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. @@ -26,14 +26,17 @@ import org.bson.codecs.Codec; import org.bson.codecs.configuration.CodecRegistry; import reactor.core.publisher.Mono; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.mongo.MongoConnectionDetails; +import org.springframework.boot.autoconfigure.mongo.MongoConnectionDetails.GridFs; import org.springframework.boot.autoconfigure.mongo.MongoProperties; -import org.springframework.boot.autoconfigure.mongo.MongoProperties.Gridfs; import org.springframework.boot.autoconfigure.mongo.MongoReactiveAutoConfiguration; +import org.springframework.boot.autoconfigure.mongo.PropertiesMongoConnectionDetails; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; @@ -60,12 +63,12 @@ import org.springframework.util.StringUtils; *

    * Registers a {@link ReactiveMongoTemplate} bean if no other bean of the same type is * configured. - *

    - * Honors the {@literal spring.data.mongodb.database} property if set, otherwise connects - * to the {@literal test} database. * * @author Mark Paluch * @author Artsiom Yudovin + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb * @since 2.0.0 */ @AutoConfiguration(after = MongoReactiveAutoConfiguration.class) @@ -75,12 +78,19 @@ import org.springframework.util.StringUtils; @Import(MongoDataConfiguration.class) public class MongoReactiveDataAutoConfiguration { + private final MongoConnectionDetails connectionDetails; + + MongoReactiveDataAutoConfiguration(MongoProperties properties, + ObjectProvider connectionDetails) { + this.connectionDetails = connectionDetails + .getIfAvailable(() -> new PropertiesMongoConnectionDetails(properties)); + } + @Bean @ConditionalOnMissingBean(ReactiveMongoDatabaseFactory.class) - public SimpleReactiveMongoDatabaseFactory reactiveMongoDatabaseFactory(MongoProperties properties, - MongoClient mongo) { - String database = properties.getMongoClientDatabase(); - return new SimpleReactiveMongoDatabaseFactory(mongo, database); + public SimpleReactiveMongoDatabaseFactory reactiveMongoDatabaseFactory(MongoClient mongo) { + return new SimpleReactiveMongoDatabaseFactory(mongo, + this.connectionDetails.getConnectionString().getDatabase()); } @Bean @@ -108,26 +118,27 @@ public class MongoReactiveDataAutoConfiguration { @Bean @ConditionalOnMissingBean(ReactiveGridFsOperations.class) public ReactiveGridFsTemplate reactiveGridFsTemplate(ReactiveMongoDatabaseFactory reactiveMongoDatabaseFactory, - MappingMongoConverter mappingMongoConverter, DataBufferFactory dataBufferFactory, - MongoProperties properties) { + MappingMongoConverter mappingMongoConverter, DataBufferFactory dataBufferFactory) { return new ReactiveGridFsTemplate(dataBufferFactory, - new GridFsReactiveMongoDatabaseFactory(reactiveMongoDatabaseFactory, properties), mappingMongoConverter, - properties.getGridfs().getBucket()); + new GridFsReactiveMongoDatabaseFactory(reactiveMongoDatabaseFactory, this.connectionDetails), + mappingMongoConverter, + (this.connectionDetails.getGridFs() != null) ? this.connectionDetails.getGridFs().getBucket() : null); } /** - * {@link ReactiveMongoDatabaseFactory} decorator to use {@link Gridfs#getDatabase()} - * when set. + * {@link ReactiveMongoDatabaseFactory} decorator to use {@link GridFs#getGridFs()} + * from the {@link MongoConnectionDetails} when set. */ static class GridFsReactiveMongoDatabaseFactory implements ReactiveMongoDatabaseFactory { private final ReactiveMongoDatabaseFactory delegate; - private final MongoProperties properties; + private final MongoConnectionDetails connectionDetails; - GridFsReactiveMongoDatabaseFactory(ReactiveMongoDatabaseFactory delegate, MongoProperties properties) { + GridFsReactiveMongoDatabaseFactory(ReactiveMongoDatabaseFactory delegate, + MongoConnectionDetails connectionDetails) { this.delegate = delegate; - this.properties = properties; + this.connectionDetails = connectionDetails; } @Override @@ -137,13 +148,17 @@ public class MongoReactiveDataAutoConfiguration { @Override public Mono getMongoDatabase() throws DataAccessException { - String gridFsDatabase = this.properties.getGridfs().getDatabase(); + String gridFsDatabase = getGridFsDatabase(this.connectionDetails); if (StringUtils.hasText(gridFsDatabase)) { return this.delegate.getMongoDatabase(gridFsDatabase); } return this.delegate.getMongoDatabase(); } + private String getGridFsDatabase(MongoConnectionDetails connectionDetails) { + return (connectionDetails.getGridFs() != null) ? connectionDetails.getGridFs().getDatabase() : null; + } + @Override public Mono getMongoDatabase(String dbName) throws DataAccessException { return this.delegate.getMongoDatabase(dbName); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/JedisConnectionConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/JedisConnectionConfiguration.java index 58df1e8366..5110ca7293 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/JedisConnectionConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/JedisConnectionConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * 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. @@ -42,6 +42,9 @@ import org.springframework.util.StringUtils; * * @author Mark Paluch * @author Stephane Nicoll + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass({ GenericObjectPool.class, JedisConnection.class, Jedis.class }) @@ -52,8 +55,10 @@ class JedisConnectionConfiguration extends RedisConnectionConfiguration { JedisConnectionConfiguration(RedisProperties properties, ObjectProvider standaloneConfigurationProvider, ObjectProvider sentinelConfiguration, - ObjectProvider clusterConfiguration) { - super(properties, standaloneConfigurationProvider, sentinelConfiguration, clusterConfiguration); + ObjectProvider clusterConfiguration, + ObjectProvider connectionDetailsProvider) { + super(properties, standaloneConfigurationProvider, sentinelConfiguration, clusterConfiguration, + connectionDetailsProvider); } @Bean @@ -90,7 +95,9 @@ class JedisConnectionConfiguration extends RedisConnectionConfiguration { private JedisClientConfigurationBuilder applyProperties(JedisClientConfigurationBuilder builder) { PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); - map.from(getProperties().isSsl()).whenTrue().toCall(builder::useSsl); + boolean ssl = (!(getConnectionDetails() instanceof PropertiesRedisConnectionDetails)) ? false + : getProperties().isSsl(); + map.from(ssl).whenTrue().toCall(builder::useSsl); map.from(getProperties().getTimeout()).to(builder::readTimeout); map.from(getProperties().getConnectTimeout()).to(builder::connectTimeout); map.from(getProperties().getClientName()).whenHasText().to(builder::clientName); @@ -117,8 +124,7 @@ class JedisConnectionConfiguration extends RedisConnectionConfiguration { } private void customizeConfigurationFromUrl(JedisClientConfiguration.JedisClientConfigurationBuilder builder) { - ConnectionInfo connectionInfo = parseUrl(getProperties().getUrl()); - if (connectionInfo.isUseSsl()) { + if (urlUsesSsl()) { builder.useSsl(); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/LettuceConnectionConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/LettuceConnectionConfiguration.java index ab13a3fe67..276c69d0da 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/LettuceConnectionConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/LettuceConnectionConfiguration.java @@ -52,6 +52,8 @@ import org.springframework.util.StringUtils; * * @author Mark Paluch * @author Andy Wilkinson + * @author Moritz Halbritter + * @author Phillip Webb */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass(RedisClient.class) @@ -61,8 +63,10 @@ class LettuceConnectionConfiguration extends RedisConnectionConfiguration { LettuceConnectionConfiguration(RedisProperties properties, ObjectProvider standaloneConfigurationProvider, ObjectProvider sentinelConfigurationProvider, - ObjectProvider clusterConfigurationProvider) { - super(properties, standaloneConfigurationProvider, sentinelConfigurationProvider, clusterConfigurationProvider); + ObjectProvider clusterConfigurationProvider, + ObjectProvider connectionDetailsProvider) { + super(properties, standaloneConfigurationProvider, sentinelConfigurationProvider, clusterConfigurationProvider, + connectionDetailsProvider); } @Bean(destroyMethod = "shutdown") @@ -116,7 +120,7 @@ class LettuceConnectionConfiguration extends RedisConnectionConfiguration { private LettuceClientConfigurationBuilder applyProperties( LettuceClientConfiguration.LettuceClientConfigurationBuilder builder) { - if (getProperties().isSsl()) { + if (getConnectionDetails() instanceof PropertiesRedisConnectionDetails && getProperties().isSsl()) { builder.useSsl(); } if (getProperties().getTimeout() != null) { @@ -161,8 +165,7 @@ class LettuceConnectionConfiguration extends RedisConnectionConfiguration { } private void customizeConfigurationFromUrl(LettuceClientConfiguration.LettuceClientConfigurationBuilder builder) { - ConnectionInfo connectionInfo = parseUrl(getProperties().getUrl()); - if (connectionInfo.isUseSsl()) { + if (urlUsesSsl()) { builder.useSsl(); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/PropertiesRedisConnectionDetails.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/PropertiesRedisConnectionDetails.java new file mode 100644 index 0000000000..81279785b6 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/PropertiesRedisConnectionDetails.java @@ -0,0 +1,127 @@ +/* + * 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.autoconfigure.data.redis; + +import java.util.List; + +import org.springframework.boot.autoconfigure.data.redis.RedisConnectionConfiguration.ConnectionInfo; + +/** + * Adapts {@link RedisProperties} to {@link RedisConnectionDetails}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class PropertiesRedisConnectionDetails implements RedisConnectionDetails { + + private final RedisProperties properties; + + PropertiesRedisConnectionDetails(RedisProperties properties) { + this.properties = properties; + } + + @Override + public String getUsername() { + if (this.properties.getUrl() != null) { + ConnectionInfo connectionInfo = connectionInfo(this.properties.getUrl()); + String userInfo = connectionInfo.getUri().getUserInfo(); + int index = (userInfo != null) ? userInfo.indexOf(':') : -1; + if (index != -1) { + return userInfo.substring(0, index); + } + } + return this.properties.getUsername(); + } + + @Override + public String getPassword() { + if (this.properties.getUrl() != null) { + ConnectionInfo connectionInfo = connectionInfo(this.properties.getUrl()); + String userInfo = connectionInfo.getUri().getUserInfo(); + int index = (userInfo != null) ? userInfo.indexOf(':') : -1; + if (index != -1) { + return userInfo.substring(index + 1); + } + } + return this.properties.getPassword(); + } + + @Override + public Standalone getStandalone() { + if (this.properties.getUrl() != null) { + ConnectionInfo connectionInfo = connectionInfo(this.properties.getUrl()); + return Standalone.of(connectionInfo.getUri().getHost(), connectionInfo.getUri().getPort(), + this.properties.getDatabase()); + } + return Standalone.of(this.properties.getHost(), this.properties.getPort(), this.properties.getDatabase()); + } + + private ConnectionInfo connectionInfo(String url) { + return (url != null) ? RedisConnectionConfiguration.parseUrl(url) : null; + } + + @Override + public Sentinel getSentinel() { + org.springframework.boot.autoconfigure.data.redis.RedisProperties.Sentinel sentinel = this.properties + .getSentinel(); + if (sentinel == null) { + return null; + } + return new Sentinel() { + + @Override + public int getDatabase() { + return PropertiesRedisConnectionDetails.this.properties.getDatabase(); + } + + @Override + public String getMaster() { + return sentinel.getMaster(); + } + + @Override + public List getNodes() { + return sentinel.getNodes().stream().map(PropertiesRedisConnectionDetails.this::asNode).toList(); + } + + @Override + public String getUsername() { + return sentinel.getUsername(); + } + + @Override + public String getPassword() { + return sentinel.getPassword(); + } + + }; + } + + @Override + public Cluster getCluster() { + RedisProperties.Cluster cluster = this.properties.getCluster(); + List nodes = (cluster != null) ? cluster.getNodes().stream().map(this::asNode).toList() : null; + return (nodes != null) ? () -> nodes : null; + } + + private Node asNode(String node) { + String[] components = node.split(":"); + return new Node(components[0], Integer.parseInt(components[1])); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisConnectionConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisConnectionConfiguration.java index f458d254ea..9626b17557 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisConnectionConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisConnectionConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * 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. @@ -22,6 +22,9 @@ import java.util.ArrayList; import java.util.List; import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.data.redis.RedisConnectionDetails.Cluster; +import org.springframework.boot.autoconfigure.data.redis.RedisConnectionDetails.Node; +import org.springframework.boot.autoconfigure.data.redis.RedisConnectionDetails.Sentinel; import org.springframework.boot.autoconfigure.data.redis.RedisProperties.Pool; import org.springframework.data.redis.connection.RedisClusterConfiguration; import org.springframework.data.redis.connection.RedisNode; @@ -29,7 +32,6 @@ import org.springframework.data.redis.connection.RedisPassword; import org.springframework.data.redis.connection.RedisSentinelConfiguration; import org.springframework.data.redis.connection.RedisStandaloneConfiguration; import org.springframework.util.ClassUtils; -import org.springframework.util.StringUtils; /** * Base Redis connection configuration. @@ -39,6 +41,9 @@ import org.springframework.util.StringUtils; * @author Alen Turkovic * @author Scott Frederick * @author Eddú Meléndez + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb */ abstract class RedisConnectionConfiguration { @@ -53,14 +58,19 @@ abstract class RedisConnectionConfiguration { private final RedisClusterConfiguration clusterConfiguration; + private final RedisConnectionDetails connectionDetails; + protected RedisConnectionConfiguration(RedisProperties properties, ObjectProvider standaloneConfigurationProvider, ObjectProvider sentinelConfigurationProvider, - ObjectProvider clusterConfigurationProvider) { + ObjectProvider clusterConfigurationProvider, + ObjectProvider connectionDetailsProvider) { this.properties = properties; this.standaloneConfiguration = standaloneConfigurationProvider.getIfAvailable(); this.sentinelConfiguration = sentinelConfigurationProvider.getIfAvailable(); this.clusterConfiguration = clusterConfigurationProvider.getIfAvailable(); + this.connectionDetails = connectionDetailsProvider + .getIfAvailable(() -> new PropertiesRedisConnectionDetails(properties)); } protected final RedisStandaloneConfiguration getStandaloneConfig() { @@ -68,20 +78,11 @@ abstract class RedisConnectionConfiguration { return this.standaloneConfiguration; } RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(); - if (StringUtils.hasText(this.properties.getUrl())) { - ConnectionInfo connectionInfo = parseUrl(this.properties.getUrl()); - config.setHostName(connectionInfo.getHostName()); - config.setPort(connectionInfo.getPort()); - config.setUsername(connectionInfo.getUsername()); - config.setPassword(RedisPassword.of(connectionInfo.getPassword())); - } - else { - config.setHostName(this.properties.getHost()); - config.setPort(this.properties.getPort()); - config.setUsername(this.properties.getUsername()); - config.setPassword(RedisPassword.of(this.properties.getPassword())); - } - config.setDatabase(this.properties.getDatabase()); + config.setHostName(this.connectionDetails.getStandalone().getHost()); + config.setPort(this.connectionDetails.getStandalone().getPort()); + config.setUsername(this.connectionDetails.getUsername()); + config.setPassword(RedisPassword.of(this.connectionDetails.getPassword())); + config.setDatabase(this.connectionDetails.getStandalone().getDatabase()); return config; } @@ -89,20 +90,21 @@ abstract class RedisConnectionConfiguration { if (this.sentinelConfiguration != null) { return this.sentinelConfiguration; } - RedisProperties.Sentinel sentinelProperties = this.properties.getSentinel(); - if (sentinelProperties != null) { + if (this.connectionDetails.getSentinel() != null) { RedisSentinelConfiguration config = new RedisSentinelConfiguration(); - config.master(sentinelProperties.getMaster()); - config.setSentinels(createSentinels(sentinelProperties)); - config.setUsername(this.properties.getUsername()); - if (this.properties.getPassword() != null) { - config.setPassword(RedisPassword.of(this.properties.getPassword())); + config.master(this.connectionDetails.getSentinel().getMaster()); + config.setSentinels(createSentinels(this.connectionDetails.getSentinel())); + config.setUsername(this.connectionDetails.getUsername()); + String password = this.connectionDetails.getPassword(); + if (password != null) { + config.setPassword(RedisPassword.of(password)); } - config.setSentinelUsername(sentinelProperties.getUsername()); - if (sentinelProperties.getPassword() != null) { - config.setSentinelPassword(RedisPassword.of(sentinelProperties.getPassword())); + config.setSentinelUsername(this.connectionDetails.getSentinel().getUsername()); + String sentinelPassword = this.connectionDetails.getSentinel().getPassword(); + if (sentinelPassword != null) { + config.setSentinelPassword(RedisPassword.of(sentinelPassword)); } - config.setDatabase(this.properties.getDatabase()); + config.setDatabase(this.connectionDetails.getSentinel().getDatabase()); return config; } return null; @@ -116,19 +118,25 @@ abstract class RedisConnectionConfiguration { if (this.clusterConfiguration != null) { return this.clusterConfiguration; } - if (this.properties.getCluster() == null) { - return null; - } RedisProperties.Cluster clusterProperties = this.properties.getCluster(); - RedisClusterConfiguration config = new RedisClusterConfiguration(clusterProperties.getNodes()); - if (clusterProperties.getMaxRedirects() != null) { - config.setMaxRedirects(clusterProperties.getMaxRedirects()); + if (this.connectionDetails.getCluster() != null) { + RedisClusterConfiguration config = new RedisClusterConfiguration( + getNodes(this.connectionDetails.getCluster())); + if (clusterProperties != null && clusterProperties.getMaxRedirects() != null) { + config.setMaxRedirects(clusterProperties.getMaxRedirects()); + } + config.setUsername(this.connectionDetails.getUsername()); + String password = this.connectionDetails.getPassword(); + if (password != null) { + config.setPassword(RedisPassword.of(password)); + } + return config; } - config.setUsername(this.properties.getUsername()); - if (this.properties.getPassword() != null) { - config.setPassword(RedisPassword.of(this.properties.getPassword())); - } - return config; + return null; + } + + private List getNodes(Cluster cluster) { + return cluster.getNodes().stream().map((node) -> "%s:%d".formatted(node.host(), node.port())).toList(); } protected final RedisProperties getProperties() { @@ -140,20 +148,23 @@ abstract class RedisConnectionConfiguration { return (enabled != null) ? enabled : COMMONS_POOL2_AVAILABLE; } - private List createSentinels(RedisProperties.Sentinel sentinel) { + private List createSentinels(Sentinel sentinel) { List nodes = new ArrayList<>(); - for (String node : sentinel.getNodes()) { - try { - nodes.add(RedisNode.fromString(node)); - } - catch (RuntimeException ex) { - throw new IllegalStateException("Invalid redis sentinel property '" + node + "'", ex); - } + for (Node node : sentinel.getNodes()) { + nodes.add(new RedisNode(node.host(), node.port())); } return nodes; } - protected ConnectionInfo parseUrl(String url) { + protected final boolean urlUsesSsl() { + return parseUrl(this.properties.getUrl()).isUseSsl(); + } + + protected final RedisConnectionDetails getConnectionDetails() { + return this.connectionDetails; + } + + static ConnectionInfo parseUrl(String url) { try { URI uri = new URI(url); String scheme = uri.getScheme(); @@ -198,18 +209,14 @@ abstract class RedisConnectionConfiguration { this.password = password; } + URI getUri() { + return this.uri; + } + boolean isUseSsl() { return this.useSsl; } - String getHostName() { - return this.uri.getHost(); - } - - int getPort() { - return this.uri.getPort(); - } - String getUsername() { return this.username; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisConnectionDetails.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisConnectionDetails.java new file mode 100644 index 0000000000..7aed831582 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisConnectionDetails.java @@ -0,0 +1,190 @@ +/* + * 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.autoconfigure.data.redis; + +import java.util.List; + +import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; +import org.springframework.util.Assert; + +/** + * Details required to establish a connection to a Redis service. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @since 3.1.0 + */ +public interface RedisConnectionDetails extends ConnectionDetails { + + /** + * Login username of the redis server. + * @return the login username of the redis server + */ + default String getUsername() { + return null; + } + + /** + * Login password of the redis server. + * @return the login password of the redis server + */ + default String getPassword() { + return null; + } + + /** + * Redis standalone configuration. Mutually exclusive with {@link #getSentinel()} and + * {@link #getCluster()}. + * @return the Redis standalone configuration + */ + default Standalone getStandalone() { + return null; + } + + /** + * Redis sentinel configuration. Mutually exclusive with {@link #getStandalone()} and + * {@link #getCluster()}. + * @return the Redis sentinel configuration + */ + default Sentinel getSentinel() { + return null; + } + + /** + * Redis cluster configuration. Mutually exclusive with {@link #getStandalone()} and + * {@link #getSentinel()}. + * @return the Redis cluster configuration + */ + default Cluster getCluster() { + return null; + } + + /** + * Redis standalone configuration. + */ + interface Standalone { + + /** + * Redis server host. + * @return the redis server host + */ + String getHost(); + + /** + * Redis server port. + * @return the redis server port + */ + int getPort(); + + /** + * Database index used by the connection factory. + * @return the database index used by the connection factory + */ + default int getDatabase() { + return 0; + } + + static Standalone of(String host, int port) { + return of(host, port, 0); + } + + static Standalone of(String host, int port, int database) { + Assert.hasLength(host, "Host must not be empty"); + return new Standalone() { + + @Override + public String getHost() { + return host; + } + + @Override + public int getPort() { + return port; + } + + @Override + public int getDatabase() { + return database; + } + + }; + } + + } + + /** + * Redis sentinel configuration. + */ + interface Sentinel { + + /** + * Database index used by the connection factory. + * @return the database index used by the connection factory + */ + int getDatabase(); + + /** + * Name of the Redis server. + * @return the name of the Redis server + */ + String getMaster(); + + /** + * List of nodes. + * @return the list of nodes + */ + List getNodes(); + + /** + * Login username for authenticating with sentinel(s). + * @return the login username for authenticating with sentinel(s) or {@code null} + */ + String getUsername(); + + /** + * Password for authenticating with sentinel(s). + * @return the password for authenticating with sentinel(s) or {@code null} + */ + String getPassword(); + + } + + /** + * Redis cluster configuration. + */ + interface Cluster { + + /** + * Nodes to bootstrap from. This represents an "initial" list of cluster nodes and + * is required to have at least one entry. + * @return nodes to bootstrap from + */ + List getNodes(); + + } + + /** + * A node in a sentinel or cluster configuration. + * + * @param host the hostname of the node + * @param port the port of the node + */ + record Node(String host, int port) { + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchConnectionDetails.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchConnectionDetails.java new file mode 100644 index 0000000000..33c499ebaa --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchConnectionDetails.java @@ -0,0 +1,135 @@ +/* + * 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.autoconfigure.elasticsearch; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; + +import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; + +/** + * Details required to establish a connection to an Elasticsearch service. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + */ +public interface ElasticsearchConnectionDetails extends ConnectionDetails { + + /** + * List of the Elasticsearch nodes to use. + * @return list of the Elasticsearch nodes to use + */ + List getNodes(); + + /** + * Username for authentication with Elasticsearch. + * @return username for authentication with Elasticsearch or {@code null} + */ + default String getUsername() { + return null; + } + + /** + * Password for authentication with Elasticsearch. + * @return password for authentication with Elasticsearch or {@code null} + */ + default String getPassword() { + return null; + } + + /** + * Prefix added to the path of every request sent to Elasticsearch. + * @return prefix added to the path of every request sent to Elasticsearch or + * {@code null} + */ + default String getPathPrefix() { + return null; + } + + /** + * An Elasticsearch node. + * + * @param hostname the hostname + * @param port the port + * @param protocol the protocol + * @param username the username or {@code null} + * @param password the password or {@code null} + */ + record Node(String hostname, int port, Node.Protocol protocol, String username, String password) { + + public Node(String host, int port, Node.Protocol protocol) { + this(host, port, protocol, null, null); + } + + URI toUri() { + try { + return new URI(this.protocol.getScheme(), userInfo(), this.hostname, this.port, null, null, null); + } + catch (URISyntaxException ex) { + throw new IllegalStateException("Can't construct URI", ex); + } + } + + private String userInfo() { + if (this.username == null) { + return null; + } + return (this.password != null) ? (this.username + ":" + this.password) : this.username; + } + + /** + * Connection protocol. + */ + public enum Protocol { + + /** + * HTTP. + */ + HTTP("http"), + + /** + * HTTPS. + */ + HTTPS("https"); + + private final String scheme; + + Protocol(String scheme) { + this.scheme = scheme; + } + + String getScheme() { + return this.scheme; + } + + static Protocol forScheme(String scheme) { + for (Protocol protocol : values()) { + if (protocol.scheme.equals(scheme)) { + return protocol; + } + } + throw new IllegalArgumentException("Unknown scheme '" + scheme + "'"); + } + + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientConfigurations.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientConfigurations.java index 93ef387e35..40f945e830 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientConfigurations.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientConfigurations.java @@ -17,8 +17,9 @@ package org.springframework.boot.autoconfigure.elasticsearch; import java.net.URI; -import java.net.URISyntaxException; import java.time.Duration; +import java.util.List; +import java.util.stream.Stream; import org.apache.http.HttpHost; import org.apache.http.auth.AuthScope; @@ -37,6 +38,8 @@ import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; +import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchConnectionDetails.Node; +import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchConnectionDetails.Node.Protocol; import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -47,6 +50,9 @@ import org.springframework.util.StringUtils; * * @author Stephane Nicoll * @author Filip Hrisafov + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb */ class ElasticsearchRestClientConfigurations { @@ -56,20 +62,27 @@ class ElasticsearchRestClientConfigurations { private final ElasticsearchProperties properties; - RestClientBuilderConfiguration(ElasticsearchProperties properties) { + private final ElasticsearchConnectionDetails connectionDetails; + + RestClientBuilderConfiguration(ElasticsearchProperties properties, + ObjectProvider connectionDetails) { this.properties = properties; + this.connectionDetails = connectionDetails + .getIfAvailable(() -> new PropertiesElasticsearchConnectionDetails(properties)); } @Bean RestClientBuilderCustomizer defaultRestClientBuilderCustomizer() { - return new DefaultRestClientBuilderCustomizer(this.properties); + return new DefaultRestClientBuilderCustomizer(this.properties, this.connectionDetails); } @Bean RestClientBuilder elasticsearchRestClientBuilder( ObjectProvider builderCustomizers) { - HttpHost[] hosts = this.properties.getUris().stream().map(this::createHttpHost).toArray(HttpHost[]::new); - RestClientBuilder builder = RestClient.builder(hosts); + RestClientBuilder builder = RestClient.builder(this.connectionDetails.getNodes() + .stream() + .map((node) -> new HttpHost(node.hostname(), node.port(), node.protocol().getScheme())) + .toArray(HttpHost[]::new)); builder.setHttpClientConfigCallback((httpClientBuilder) -> { builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(httpClientBuilder)); return httpClientBuilder; @@ -78,36 +91,14 @@ class ElasticsearchRestClientConfigurations { builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(requestConfigBuilder)); return requestConfigBuilder; }); - if (this.properties.getPathPrefix() != null) { - builder.setPathPrefix(this.properties.getPathPrefix()); + String pathPrefix = this.connectionDetails.getPathPrefix(); + if (pathPrefix != null) { + builder.setPathPrefix(pathPrefix); } builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); return builder; } - private HttpHost createHttpHost(String uri) { - try { - return createHttpHost(URI.create(uri)); - } - catch (IllegalArgumentException ex) { - return HttpHost.create(uri); - } - } - - private HttpHost createHttpHost(URI uri) { - if (!StringUtils.hasLength(uri.getUserInfo())) { - return HttpHost.create(uri.toString()); - } - try { - return HttpHost.create(new URI(uri.getScheme(), null, uri.getHost(), uri.getPort(), uri.getPath(), - uri.getQuery(), uri.getFragment()) - .toString()); - } - catch (URISyntaxException ex) { - throw new IllegalStateException(ex); - } - } - } @Configuration(proxyBeanMethods = false) @@ -146,8 +137,12 @@ class ElasticsearchRestClientConfigurations { private final ElasticsearchProperties properties; - DefaultRestClientBuilderCustomizer(ElasticsearchProperties properties) { + private final ElasticsearchConnectionDetails connectionDetails; + + DefaultRestClientBuilderCustomizer(ElasticsearchProperties properties, + ElasticsearchConnectionDetails connectionDetails) { this.properties = properties; + this.connectionDetails = connectionDetails; } @Override @@ -156,7 +151,7 @@ class ElasticsearchRestClientConfigurations { @Override public void customize(HttpAsyncClientBuilder builder) { - builder.setDefaultCredentialsProvider(new PropertiesCredentialsProvider(this.properties)); + builder.setDefaultCredentialsProvider(new ConnectionDetailsCredentialsProvider(this.connectionDetails)); map.from(this.properties::isSocketKeepAlive) .to((keepAlive) -> builder .setDefaultIOReactorConfig(IOReactorConfig.custom().setSoKeepAlive(keepAlive).build())); @@ -176,28 +171,20 @@ class ElasticsearchRestClientConfigurations { } - private static class PropertiesCredentialsProvider extends BasicCredentialsProvider { + private static class ConnectionDetailsCredentialsProvider extends BasicCredentialsProvider { - PropertiesCredentialsProvider(ElasticsearchProperties properties) { - if (StringUtils.hasText(properties.getUsername())) { - Credentials credentials = new UsernamePasswordCredentials(properties.getUsername(), - properties.getPassword()); + ConnectionDetailsCredentialsProvider(ElasticsearchConnectionDetails connectionDetails) { + String username = connectionDetails.getUsername(); + if (StringUtils.hasText(username)) { + Credentials credentials = new UsernamePasswordCredentials(username, connectionDetails.getPassword()); setCredentials(AuthScope.ANY, credentials); } - properties.getUris() - .stream() - .map(this::toUri) - .filter(this::hasUserInfo) - .forEach(this::addUserInfoCredentials); + Stream uris = getUris(connectionDetails); + uris.filter(this::hasUserInfo).forEach(this::addUserInfoCredentials); } - private URI toUri(String uri) { - try { - return URI.create(uri); - } - catch (IllegalArgumentException ex) { - return null; - } + private Stream getUris(ElasticsearchConnectionDetails connectionDetails) { + return connectionDetails.getNodes().stream().map(Node::toUri); } private boolean hasUserInfo(URI uri) { @@ -222,4 +209,59 @@ class ElasticsearchRestClientConfigurations { } + /** + * Adapts {@link ElasticsearchProperties} to {@link ElasticsearchConnectionDetails}. + */ + private static class PropertiesElasticsearchConnectionDetails implements ElasticsearchConnectionDetails { + + private final ElasticsearchProperties properties; + + PropertiesElasticsearchConnectionDetails(ElasticsearchProperties properties) { + this.properties = properties; + } + + @Override + public List getNodes() { + return this.properties.getUris().stream().map(this::createNode).toList(); + } + + @Override + public String getUsername() { + return this.properties.getUsername(); + } + + @Override + public String getPassword() { + return this.properties.getPassword(); + } + + @Override + public String getPathPrefix() { + return this.properties.getPathPrefix(); + } + + private Node createNode(String uri) { + if (!(uri.startsWith("http://") || uri.startsWith("https://"))) { + uri = "http://" + uri; + } + return createNode(URI.create(uri)); + } + + private Node createNode(URI uri) { + String userInfo = uri.getUserInfo(); + Protocol protocol = Protocol.forScheme(uri.getScheme()); + if (!StringUtils.hasLength(userInfo)) { + return new Node(uri.getHost(), uri.getPort(), protocol, null, null); + } + int separatorIndex = userInfo.indexOf(':'); + if (separatorIndex == -1) { + return new Node(uri.getHost(), uri.getPort(), protocol, userInfo, null); + } + String[] components = userInfo.split(":"); + return new Node(uri.getHost(), uri.getPort(), protocol, components[0], + (components.length > 1) ? components[1] : ""); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java index 04a715ce63..c0bc7bc171 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java @@ -47,6 +47,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration.FlywayAutoConfigurationRuntimeHints; import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration.FlywayDataSourceCondition; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails; import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; import org.springframework.boot.context.properties.ConfigurationPropertiesBinding; @@ -86,6 +87,7 @@ import org.springframework.util.StringUtils; * @author Semyon Danilov * @author Chris Bono * @author Moritz Halbritter + * @author Andy Wilkinson * @since 1.1.0 */ @AutoConfiguration(after = { DataSourceAutoConfiguration.class, JdbcTemplateAutoConfiguration.class, @@ -125,7 +127,7 @@ public class FlywayAutoConfiguration { ObjectProvider fluentConfigurationCustomizers, ObjectProvider javaMigrations, ObjectProvider callbacks) { return flyway(properties, resourceLoader, dataSource, flywayDataSource, fluentConfigurationCustomizers, - javaMigrations, callbacks, new ResourceProviderCustomizer()); + javaMigrations, callbacks, new ResourceProviderCustomizer(), null); } @Bean @@ -133,9 +135,15 @@ public class FlywayAutoConfiguration { @FlywayDataSource ObjectProvider flywayDataSource, ObjectProvider fluentConfigurationCustomizers, ObjectProvider javaMigrations, ObjectProvider callbacks, - ResourceProviderCustomizer resourceProviderCustomizer) { + ResourceProviderCustomizer resourceProviderCustomizer, + ObjectProvider connectionDetailsProvider) { FluentConfiguration configuration = new FluentConfiguration(resourceLoader.getClassLoader()); - configureDataSource(configuration, properties, flywayDataSource.getIfAvailable(), dataSource.getIfUnique()); + JdbcConnectionDetails connectionDetails = (connectionDetailsProvider != null) + ? connectionDetailsProvider.getIfAvailable() : null; + connectionDetails = (connectionDetails != null) ? connectionDetails + : new FlywayPropertiesJdbcConnectionDetails(properties); + configureDataSource(configuration, flywayDataSource.getIfAvailable(), dataSource.getIfUnique(), + connectionDetails); configureProperties(configuration, properties); configureCallbacks(configuration, callbacks.orderedStream().toList()); configureJavaMigrations(configuration, javaMigrations.orderedStream().toList()); @@ -144,38 +152,41 @@ public class FlywayAutoConfiguration { return configuration.load(); } - private void configureDataSource(FluentConfiguration configuration, FlywayProperties properties, - DataSource flywayDataSource, DataSource dataSource) { - DataSource migrationDataSource = getMigrationDataSource(properties, flywayDataSource, dataSource); + private void configureDataSource(FluentConfiguration configuration, DataSource flywayDataSource, + DataSource dataSource, JdbcConnectionDetails connectionDetails) { + DataSource migrationDataSource = getMigrationDataSource(flywayDataSource, dataSource, connectionDetails); configuration.dataSource(migrationDataSource); } - private DataSource getMigrationDataSource(FlywayProperties properties, DataSource flywayDataSource, - DataSource dataSource) { + private DataSource getMigrationDataSource(DataSource flywayDataSource, DataSource dataSource, + JdbcConnectionDetails connectionDetails) { if (flywayDataSource != null) { return flywayDataSource; } - if (properties.getUrl() != null) { + String url = connectionDetails.getJdbcUrl(); + if (url != null) { DataSourceBuilder builder = DataSourceBuilder.create().type(SimpleDriverDataSource.class); - builder.url(properties.getUrl()); - applyCommonBuilderProperties(properties, builder); + builder.url(url); + applyConnectionDetails(connectionDetails, builder); return builder.build(); } - if (properties.getUser() != null && dataSource != null) { + String user = connectionDetails.getUsername(); + if (user != null && dataSource != null) { DataSourceBuilder builder = DataSourceBuilder.derivedFrom(dataSource) .type(SimpleDriverDataSource.class); - applyCommonBuilderProperties(properties, builder); + applyConnectionDetails(connectionDetails, builder); return builder.build(); } Assert.state(dataSource != null, "Flyway migration DataSource missing"); return dataSource; } - private void applyCommonBuilderProperties(FlywayProperties properties, DataSourceBuilder builder) { - builder.username(properties.getUser()); - builder.password(properties.getPassword()); - if (StringUtils.hasText(properties.getDriverClassName())) { - builder.driverClassName(properties.getDriverClassName()); + private void applyConnectionDetails(JdbcConnectionDetails connectionDetails, DataSourceBuilder builder) { + builder.username(connectionDetails.getUsername()); + builder.password(connectionDetails.getPassword()); + String driverClassName = connectionDetails.getDriverClassName(); + if (StringUtils.hasText(driverClassName)) { + builder.driverClassName(driverClassName); } } @@ -373,6 +384,11 @@ public class FlywayAutoConfiguration { } + @ConditionalOnBean(JdbcConnectionDetails.class) + private static final class JdbcConnectionDetailsCondition { + + } + @ConditionalOnProperty(prefix = "spring.flyway", name = "url") private static final class FlywayUrlCondition { @@ -389,4 +405,37 @@ public class FlywayAutoConfiguration { } + /** + * Adapts {@link FlywayProperties} to {@link JdbcConnectionDetails}. + */ + private static final class FlywayPropertiesJdbcConnectionDetails implements JdbcConnectionDetails { + + private final FlywayProperties properties; + + private FlywayPropertiesJdbcConnectionDetails(FlywayProperties properties) { + this.properties = properties; + } + + @Override + public String getUsername() { + return this.properties.getUser(); + } + + @Override + public String getPassword() { + return this.properties.getPassword(); + } + + @Override + public String getJdbcUrl() { + return this.properties.getUrl(); + } + + @Override + public String getDriverClassName() { + return this.properties.getDriverClassName(); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/influx/InfluxDbAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/influx/InfluxDbAutoConfiguration.java index 4e3a7d09cc..d793ff83dd 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/influx/InfluxDbAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/influx/InfluxDbAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * 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. @@ -16,6 +16,8 @@ package org.springframework.boot.autoconfigure.influx; +import java.net.URI; + import okhttp3.OkHttpClient; import org.influxdb.InfluxDB; import org.influxdb.impl.InfluxDBImpl; @@ -23,11 +25,15 @@ import org.influxdb.impl.InfluxDBImpl; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Condition; +import org.springframework.context.annotation.Conditional; /** * {@link EnableAutoConfiguration Auto-configuration} for InfluxDB. @@ -35,6 +41,9 @@ import org.springframework.context.annotation.Bean; * @author Sergey Kuptsov * @author Stephane Nicoll * @author Eddú Meléndez + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb * @since 2.0.0 */ @AutoConfiguration @@ -44,11 +53,14 @@ public class InfluxDbAutoConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnProperty("spring.influx.url") + @Conditional(InfluxDBCondition.class) public InfluxDB influxDb(InfluxDbProperties properties, ObjectProvider builder, - ObjectProvider customizers) { - InfluxDB influxDb = new InfluxDBImpl(properties.getUrl(), properties.getUser(), properties.getPassword(), - determineBuilder(builder.getIfAvailable())); + ObjectProvider customizers, + ObjectProvider connectionDetailsProvider) { + InfluxDbConnectionDetails connectionDetails = connectionDetailsProvider + .getIfAvailable(() -> new PropertiesInfluxDbConnectionDetails(properties)); + InfluxDB influxDb = new InfluxDBImpl(connectionDetails.getUrl().toString(), connectionDetails.getUsername(), + connectionDetails.getPassword(), determineBuilder(builder.getIfAvailable())); customizers.orderedStream().forEach((customizer) -> customizer.customize(influxDb)); return influxDb; } @@ -60,4 +72,54 @@ public class InfluxDbAutoConfiguration { return new OkHttpClient.Builder(); } + /** + * {@link Condition} that matches when either {@code spring.influx.url} has been set + * or there is an {@link InfluxDbConnectionDetails} bean. + */ + static final class InfluxDBCondition extends AnyNestedCondition { + + InfluxDBCondition() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @ConditionalOnProperty(prefix = "spring.influx", name = "url") + private static final class InfluxUrlCondition { + + } + + @ConditionalOnBean(InfluxDbConnectionDetails.class) + private static final class InfluxDbConnectionDetailsCondition { + + } + + } + + /** + * Adapts {@link InfluxDbProperties} to {@link InfluxDbConnectionDetails}. + */ + static class PropertiesInfluxDbConnectionDetails implements InfluxDbConnectionDetails { + + private final InfluxDbProperties properties; + + PropertiesInfluxDbConnectionDetails(InfluxDbProperties properties) { + this.properties = properties; + } + + @Override + public URI getUrl() { + return URI.create(this.properties.getUrl()); + } + + @Override + public String getUsername() { + return this.properties.getUser(); + } + + @Override + public String getPassword() { + return this.properties.getPassword(); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/influx/InfluxDbConnectionDetails.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/influx/InfluxDbConnectionDetails.java new file mode 100644 index 0000000000..0f00832b61 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/influx/InfluxDbConnectionDetails.java @@ -0,0 +1,51 @@ +/* + * 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.autoconfigure.influx; + +import java.net.URI; + +import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; + +/** + * Details required to establish a connection to an InfluxDB service. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + */ +public interface InfluxDbConnectionDetails extends ConnectionDetails { + + /** + * URL of the InfluxDB instance to which to connect. + * @return the URL of the InfluxDB instance to which to connect + */ + URI getUrl(); + + /** + * Login user. + * @return the login user or {@code null} + */ + String getUsername(); + + /** + * Login password. + * @return the login password or {@code null} + */ + String getPassword(); + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration.java index 6dc95e5820..95523db179 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * 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. @@ -24,10 +24,13 @@ import com.zaxxer.hikari.HikariDataSource; import oracle.jdbc.OracleConnection; import oracle.ucp.jdbc.PoolDataSourceImpl; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.boot.jdbc.DatabaseDriver; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -40,6 +43,8 @@ import org.springframework.util.StringUtils; * @author Phillip Webb * @author Stephane Nicoll * @author Fabio Grassi + * @author Moritz Halbritter + * @author Andy Wilkinson */ abstract class DataSourceConfiguration { @@ -48,6 +53,17 @@ abstract class DataSourceConfiguration { return (T) properties.initializeDataSourceBuilder().type(type).build(); } + @SuppressWarnings("unchecked") + protected static T createDataSource(JdbcConnectionDetails connectionDetails, Class type, + ClassLoader classLoader) { + return (T) DataSourceBuilder.create(classLoader) + .url(connectionDetails.getJdbcUrl()) + .username(connectionDetails.getUsername()) + .password(connectionDetails.getPassword()) + .type(type) + .build(); + } + /** * Tomcat Pool DataSource configuration. */ @@ -58,13 +74,26 @@ abstract class DataSourceConfiguration { matchIfMissing = true) static class Tomcat { + @Bean + @ConditionalOnBean(JdbcConnectionDetails.class) + static TomcatJdbcConnectionDetailsBeanPostProcessor tomcatJdbcConnectionDetailsBeanPostProcessor( + ObjectProvider connectionDetailsProvider) { + return new TomcatJdbcConnectionDetailsBeanPostProcessor(connectionDetailsProvider); + } + @Bean @ConfigurationProperties(prefix = "spring.datasource.tomcat") - org.apache.tomcat.jdbc.pool.DataSource dataSource(DataSourceProperties properties) { - org.apache.tomcat.jdbc.pool.DataSource dataSource = createDataSource(properties, - org.apache.tomcat.jdbc.pool.DataSource.class); - DatabaseDriver databaseDriver = DatabaseDriver.fromJdbcUrl(properties.determineUrl()); - String validationQuery = databaseDriver.getValidationQuery(); + org.apache.tomcat.jdbc.pool.DataSource dataSource(DataSourceProperties properties, + ObjectProvider connectionDetailsProvider) { + JdbcConnectionDetails connectionDetails = connectionDetailsProvider.getIfAvailable(); + Class dataSourceType = org.apache.tomcat.jdbc.pool.DataSource.class; + org.apache.tomcat.jdbc.pool.DataSource dataSource = (connectionDetails != null) + ? createDataSource(connectionDetails, dataSourceType, properties.getClassLoader()) + : createDataSource(properties, dataSourceType); + String validationQuery; + String url = (connectionDetails != null) ? connectionDetails.getJdbcUrl() : properties.determineUrl(); + DatabaseDriver databaseDriver = DatabaseDriver.fromJdbcUrl(url); + validationQuery = databaseDriver.getValidationQuery(); if (validationQuery != null) { dataSource.setTestOnBorrow(true); dataSource.setValidationQuery(validationQuery); @@ -84,10 +113,21 @@ abstract class DataSourceConfiguration { matchIfMissing = true) static class Hikari { + @Bean + @ConditionalOnBean(JdbcConnectionDetails.class) + static HikariJdbcConnectionDetailsBeanPostProcessor jdbcConnectionDetailsHikariBeanPostProcessor( + ObjectProvider connectionDetailsProvider) { + return new HikariJdbcConnectionDetailsBeanPostProcessor(connectionDetailsProvider); + } + @Bean @ConfigurationProperties(prefix = "spring.datasource.hikari") - HikariDataSource dataSource(DataSourceProperties properties) { - HikariDataSource dataSource = createDataSource(properties, HikariDataSource.class); + HikariDataSource dataSource(DataSourceProperties properties, + ObjectProvider connectionDetailsProvider) { + JdbcConnectionDetails connectionDetails = connectionDetailsProvider.getIfAvailable(); + HikariDataSource dataSource = (connectionDetails != null) + ? createDataSource(connectionDetails, HikariDataSource.class, properties.getClassLoader()) + : createDataSource(properties, HikariDataSource.class); if (StringUtils.hasText(properties.getName())) { dataSource.setPoolName(properties.getName()); } @@ -106,10 +146,22 @@ abstract class DataSourceConfiguration { matchIfMissing = true) static class Dbcp2 { + @Bean + @ConditionalOnBean(JdbcConnectionDetails.class) + static Dbcp2JdbcConnectionDetailsBeanPostProcessor dbcp2JdbcConnectionDetailsBeanPostProcessor( + ObjectProvider connectionDetailsProvider) { + return new Dbcp2JdbcConnectionDetailsBeanPostProcessor(connectionDetailsProvider); + } + @Bean @ConfigurationProperties(prefix = "spring.datasource.dbcp2") - org.apache.commons.dbcp2.BasicDataSource dataSource(DataSourceProperties properties) { - return createDataSource(properties, org.apache.commons.dbcp2.BasicDataSource.class); + org.apache.commons.dbcp2.BasicDataSource dataSource(DataSourceProperties properties, + ObjectProvider connectionDetailsProvider) { + JdbcConnectionDetails connectionDetails = connectionDetailsProvider.getIfAvailable(); + Class dataSourceType = org.apache.commons.dbcp2.BasicDataSource.class; + return (connectionDetails != null) + ? createDataSource(connectionDetails, dataSourceType, properties.getClassLoader()) + : createDataSource(properties, dataSourceType); } } @@ -124,10 +176,21 @@ abstract class DataSourceConfiguration { matchIfMissing = true) static class OracleUcp { + @Bean + @ConditionalOnBean(JdbcConnectionDetails.class) + static OracleUcpJdbcConnectionDetailsBeanPostProcessor oracleUcpJdbcConnectionDetailsBeanPostProcessor( + ObjectProvider connectionDetailsProvider) { + return new OracleUcpJdbcConnectionDetailsBeanPostProcessor(connectionDetailsProvider); + } + @Bean @ConfigurationProperties(prefix = "spring.datasource.oracleucp") - PoolDataSourceImpl dataSource(DataSourceProperties properties) throws SQLException { - PoolDataSourceImpl dataSource = createDataSource(properties, PoolDataSourceImpl.class); + PoolDataSourceImpl dataSource(DataSourceProperties properties, + ObjectProvider connectionDetailsProvider) throws SQLException { + JdbcConnectionDetails connectionDetails = connectionDetailsProvider.getIfAvailable(); + PoolDataSourceImpl dataSource = (connectionDetails != null) + ? createDataSource(connectionDetails, PoolDataSourceImpl.class, properties.getClassLoader()) + : createDataSource(properties, PoolDataSourceImpl.class); dataSource.setValidateConnectionOnBorrow(true); if (StringUtils.hasText(properties.getName())) { dataSource.setConnectionPoolName(properties.getName()); @@ -146,7 +209,17 @@ abstract class DataSourceConfiguration { static class Generic { @Bean - DataSource dataSource(DataSourceProperties properties) { + DataSource dataSource(DataSourceProperties properties, + ObjectProvider connectionDetailsProvider) { + JdbcConnectionDetails connectionDetails = connectionDetailsProvider.getIfAvailable(); + if (connectionDetails != null) { + return DataSourceBuilder.create(properties.getClassLoader()) + .url(connectionDetails.getJdbcUrl()) + .username(connectionDetails.getUsername()) + .password(connectionDetails.getPassword()) + .type(properties.getType()) + .build(); + } return properties.initializeDataSourceBuilder().build(); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/Dbcp2JdbcConnectionDetailsBeanPostProcessor.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/Dbcp2JdbcConnectionDetailsBeanPostProcessor.java new file mode 100644 index 0000000000..8aa2648e18 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/Dbcp2JdbcConnectionDetailsBeanPostProcessor.java @@ -0,0 +1,46 @@ +/* + * 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.autoconfigure.jdbc; + +import org.apache.commons.dbcp2.BasicDataSource; + +import org.springframework.beans.factory.ObjectProvider; + +/** + * Post-processes beans of type {@link BasicDataSource} and name 'dataSource' to apply the + * values from {@link JdbcConnectionDetails}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class Dbcp2JdbcConnectionDetailsBeanPostProcessor extends JdbcConnectionDetailsBeanPostProcessor { + + Dbcp2JdbcConnectionDetailsBeanPostProcessor(ObjectProvider connectionDetailsProvider) { + super(BasicDataSource.class, connectionDetailsProvider); + } + + @Override + protected Object processDataSource(BasicDataSource dataSource, JdbcConnectionDetails connectionDetails) { + dataSource.setUrl(connectionDetails.getJdbcUrl()); + dataSource.setUsername(connectionDetails.getUsername()); + dataSource.setPassword(connectionDetails.getPassword()); + dataSource.setDriverClassName(connectionDetails.getDriverClassName()); + return dataSource; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/HikariJdbcConnectionDetailsBeanPostProcessor.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/HikariJdbcConnectionDetailsBeanPostProcessor.java new file mode 100644 index 0000000000..ca4b1b18f6 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/HikariJdbcConnectionDetailsBeanPostProcessor.java @@ -0,0 +1,46 @@ +/* + * 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.autoconfigure.jdbc; + +import com.zaxxer.hikari.HikariDataSource; + +import org.springframework.beans.factory.ObjectProvider; + +/** + * Post-processes beans of type {@link HikariDataSource} and name 'dataSource' to apply + * the values from {@link JdbcConnectionDetails}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class HikariJdbcConnectionDetailsBeanPostProcessor extends JdbcConnectionDetailsBeanPostProcessor { + + HikariJdbcConnectionDetailsBeanPostProcessor(ObjectProvider connectionDetailsProvider) { + super(HikariDataSource.class, connectionDetailsProvider); + } + + @Override + protected Object processDataSource(HikariDataSource dataSource, JdbcConnectionDetails connectionDetails) { + dataSource.setJdbcUrl(connectionDetails.getJdbcUrl()); + dataSource.setUsername(connectionDetails.getUsername()); + dataSource.setPassword(connectionDetails.getPassword()); + dataSource.setDriverClassName(connectionDetails.getDriverClassName()); + return dataSource; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/JdbcConnectionDetails.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/JdbcConnectionDetails.java new file mode 100644 index 0000000000..f6ca3c4afe --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/JdbcConnectionDetails.java @@ -0,0 +1,74 @@ +/* + * 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.autoconfigure.jdbc; + +import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; +import org.springframework.boot.jdbc.DatabaseDriver; + +/** + * Details required to establish a connection to an SQL service using JDBC. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + */ +public interface JdbcConnectionDetails extends ConnectionDetails { + + /** + * Hostname for the database. + * @return the username for the database + */ + String getUsername(); + + /** + * Password for the database. + * @return the password for the database + */ + String getPassword(); + + /** + * JDBC url for the database. + * @return the JDBC url for the database + */ + String getJdbcUrl(); + + /** + * The name of the JDBC driver class. Defaults to the class name of the driver + * specified in the JDBC URL. + * @return the JDBC driver class name + * @see #getJdbcUrl() + * @see DatabaseDriver#fromJdbcUrl(String) + * @see DatabaseDriver#getDriverClassName() + */ + default String getDriverClassName() { + return DatabaseDriver.fromJdbcUrl(getJdbcUrl()).getDriverClassName(); + } + + /** + * Returns the name of the XA DataSource class. Defaults to the class name from the + * driver specified in the JDBC URL. + * @return the XA DataSource class name + * @see #getJdbcUrl() + * @see DatabaseDriver#fromJdbcUrl(String) + * @see DatabaseDriver#getXaDataSourceClassName() + */ + default String getXaDataSourceClassName() { + return DatabaseDriver.fromJdbcUrl(getJdbcUrl()).getXaDataSourceClassName(); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/JdbcConnectionDetailsBeanPostProcessor.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/JdbcConnectionDetailsBeanPostProcessor.java new file mode 100644 index 0000000000..e4cb0c8acf --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/JdbcConnectionDetailsBeanPostProcessor.java @@ -0,0 +1,64 @@ +/* + * 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.autoconfigure.jdbc; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.core.Ordered; +import org.springframework.core.PriorityOrdered; + +/** + * Abstract base class for DataSource bean post processors which apply values from + * {@link JdbcConnectionDetails}. Acts on beans named 'dataSource' of type {@code T}. + * + * @param type of the datasource + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +abstract class JdbcConnectionDetailsBeanPostProcessor implements BeanPostProcessor, PriorityOrdered { + + private final Class dataSourceClass; + + private final ObjectProvider connectionDetailsProvider; + + JdbcConnectionDetailsBeanPostProcessor(Class dataSourceClass, + ObjectProvider connectionDetailsProvider) { + this.dataSourceClass = dataSourceClass; + this.connectionDetailsProvider = connectionDetailsProvider; + } + + @Override + @SuppressWarnings("unchecked") + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + if (this.dataSourceClass.isAssignableFrom(bean.getClass()) && "dataSource".equals(beanName)) { + JdbcConnectionDetails connectionDetails = this.connectionDetailsProvider.getObject(); + return processDataSource((T) bean, connectionDetails); + } + return bean; + } + + protected abstract Object processDataSource(T dataSource, JdbcConnectionDetails connectionDetails); + + @Override + public int getOrder() { + // Runs after ConfigurationPropertiesBindingPostProcessor + return Ordered.HIGHEST_PRECEDENCE + 2; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/OracleUcpJdbcConnectionDetailsBeanPostProcessor.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/OracleUcpJdbcConnectionDetailsBeanPostProcessor.java new file mode 100644 index 0000000000..4356a06f13 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/OracleUcpJdbcConnectionDetailsBeanPostProcessor.java @@ -0,0 +1,54 @@ +/* + * 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.autoconfigure.jdbc; + +import java.sql.SQLException; + +import oracle.ucp.jdbc.PoolDataSourceImpl; + +import org.springframework.beans.factory.ObjectProvider; + +/** + * Post-processes beans of type {@link PoolDataSourceImpl} and name 'dataSource' to apply + * the values from {@link JdbcConnectionDetails}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class OracleUcpJdbcConnectionDetailsBeanPostProcessor + extends JdbcConnectionDetailsBeanPostProcessor { + + OracleUcpJdbcConnectionDetailsBeanPostProcessor(ObjectProvider connectionDetailsProvider) { + super(PoolDataSourceImpl.class, connectionDetailsProvider); + } + + @Override + protected Object processDataSource(PoolDataSourceImpl dataSource, JdbcConnectionDetails connectionDetails) { + try { + dataSource.setURL(connectionDetails.getJdbcUrl()); + dataSource.setUser(connectionDetails.getUsername()); + dataSource.setPassword(connectionDetails.getPassword()); + dataSource.setConnectionFactoryClassName(connectionDetails.getDriverClassName()); + return dataSource; + } + catch (SQLException ex) { + throw new RuntimeException("Failed to set URL / user / password of datasource", ex); + } + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/TomcatJdbcConnectionDetailsBeanPostProcessor.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/TomcatJdbcConnectionDetailsBeanPostProcessor.java new file mode 100644 index 0000000000..4efbcdf1fe --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/TomcatJdbcConnectionDetailsBeanPostProcessor.java @@ -0,0 +1,46 @@ +/* + * 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.autoconfigure.jdbc; + +import org.apache.tomcat.jdbc.pool.DataSource; + +import org.springframework.beans.factory.ObjectProvider; + +/** + * Post-processes beans of type {@link DataSource} and name 'dataSource' to apply the + * values from {@link JdbcConnectionDetails}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class TomcatJdbcConnectionDetailsBeanPostProcessor extends JdbcConnectionDetailsBeanPostProcessor { + + TomcatJdbcConnectionDetailsBeanPostProcessor(ObjectProvider connectionDetailsProvider) { + super(DataSource.class, connectionDetailsProvider); + } + + @Override + protected Object processDataSource(DataSource dataSource, JdbcConnectionDetails connectionDetails) { + dataSource.setUrl(connectionDetails.getJdbcUrl()); + dataSource.setUsername(connectionDetails.getUsername()); + dataSource.setPassword(connectionDetails.getPassword()); + dataSource.setDriverClassName(connectionDetails.getDriverClassName()); + return dataSource; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfiguration.java index 37699912ac..46a9f60f92 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * 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. @@ -40,7 +40,6 @@ import org.springframework.boot.context.properties.source.ConfigurationPropertyN import org.springframework.boot.context.properties.source.ConfigurationPropertyNameAliases; import org.springframework.boot.context.properties.source.ConfigurationPropertySource; import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; -import org.springframework.boot.jdbc.DatabaseDriver; import org.springframework.boot.jdbc.XADataSourceWrapper; import org.springframework.context.annotation.Bean; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; @@ -54,6 +53,8 @@ import org.springframework.util.StringUtils; * @author Phillip Webb * @author Josh Long * @author Madhura Bhave + * @author Moritz Halbritter + * @author Andy Wilkinson * @since 1.2.0 */ @AutoConfiguration(before = DataSourceAutoConfiguration.class) @@ -67,8 +68,10 @@ public class XADataSourceAutoConfiguration implements BeanClassLoaderAware { @Bean public DataSource dataSource(XADataSourceWrapper wrapper, DataSourceProperties properties, - ObjectProvider xaDataSource) throws Exception { - return wrapper.wrapDataSource(xaDataSource.getIfAvailable(() -> createXaDataSource(properties))); + ObjectProvider connectionDetails, ObjectProvider xaDataSource) + throws Exception { + return wrapper.wrapDataSource(xaDataSource.getIfAvailable(() -> createXaDataSource(properties, + connectionDetails.getIfAvailable(() -> new PropertiesJdbcConnectionDetails(properties))))); } @Override @@ -76,14 +79,11 @@ public class XADataSourceAutoConfiguration implements BeanClassLoaderAware { this.classLoader = classLoader; } - private XADataSource createXaDataSource(DataSourceProperties properties) { - String className = properties.getXa().getDataSourceClassName(); - if (!StringUtils.hasLength(className)) { - className = DatabaseDriver.fromJdbcUrl(properties.determineUrl()).getXaDataSourceClassName(); - } + private XADataSource createXaDataSource(DataSourceProperties properties, JdbcConnectionDetails connectionDetails) { + String className = connectionDetails.getXaDataSourceClassName(); Assert.state(StringUtils.hasLength(className), "No XA DataSource class name specified"); XADataSource dataSource = createXaDataSourceInstance(className); - bindXaProperties(dataSource, properties); + bindXaProperties(dataSource, properties, connectionDetails); return dataSource; } @@ -99,18 +99,19 @@ public class XADataSourceAutoConfiguration implements BeanClassLoaderAware { } } - private void bindXaProperties(XADataSource target, DataSourceProperties dataSourceProperties) { - Binder binder = new Binder(getBinderSource(dataSourceProperties)); + private void bindXaProperties(XADataSource target, DataSourceProperties dataSourceProperties, + JdbcConnectionDetails connectionDetails) { + Binder binder = new Binder(getBinderSource(dataSourceProperties, connectionDetails)); binder.bind(ConfigurationPropertyName.EMPTY, Bindable.ofInstance(target)); } - private ConfigurationPropertySource getBinderSource(DataSourceProperties dataSourceProperties) { - Map properties = new HashMap<>(); - properties.putAll(dataSourceProperties.getXa().getProperties()); - properties.computeIfAbsent("user", (key) -> dataSourceProperties.determineUsername()); - properties.computeIfAbsent("password", (key) -> dataSourceProperties.determinePassword()); + private ConfigurationPropertySource getBinderSource(DataSourceProperties dataSourceProperties, + JdbcConnectionDetails connectionDetails) { + Map properties = new HashMap<>(dataSourceProperties.getXa().getProperties()); + properties.computeIfAbsent("user", (key) -> connectionDetails.getUsername()); + properties.computeIfAbsent("password", (key) -> connectionDetails.getPassword()); try { - properties.computeIfAbsent("url", (key) -> dataSourceProperties.determineUrl()); + properties.computeIfAbsent("url", (key) -> connectionDetails.getJdbcUrl()); } catch (DataSourceBeanCreationException ex) { // Continue as not all XA DataSource's require a URL @@ -121,4 +122,45 @@ public class XADataSourceAutoConfiguration implements BeanClassLoaderAware { return source.withAliases(aliases); } + /** + * Adapts {@link DataSourceProperties} to {@link JdbcConnectionDetails}. + */ + private static class PropertiesJdbcConnectionDetails implements JdbcConnectionDetails { + + private final DataSourceProperties properties; + + PropertiesJdbcConnectionDetails(DataSourceProperties properties) { + this.properties = properties; + } + + @Override + public String getUsername() { + return this.properties.determineUsername(); + } + + @Override + public String getPassword() { + return this.properties.determinePassword(); + } + + @Override + public String getJdbcUrl() { + return this.properties.determineUrl(); + } + + @Override + public String getDriverClassName() { + return (this.properties.getDriverClassName() != null) ? this.properties.getDriverClassName() + : JdbcConnectionDetails.super.getDriverClassName(); + } + + @Override + public String getXaDataSourceClassName() { + return (this.properties.getXa().getDataSourceClassName() != null) + ? this.properties.getXa().getDataSourceClassName() + : JdbcConnectionDetails.super.getXaDataSourceClassName(); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfiguration.java index cf7b44e1a9..9b9632ddc9 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfiguration.java @@ -18,6 +18,12 @@ package org.springframework.boot.autoconfigure.kafka; import java.io.IOException; import java.time.Duration; +import java.util.List; +import java.util.Map; + +import org.apache.kafka.clients.CommonClientConfigs; +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.clients.producer.ProducerConfig; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; @@ -26,6 +32,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; +import org.springframework.boot.autoconfigure.kafka.KafkaConnectionDetails.Node; import org.springframework.boot.autoconfigure.kafka.KafkaProperties.Jaas; import org.springframework.boot.autoconfigure.kafka.KafkaProperties.Retry.Topic; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -56,6 +63,9 @@ import org.springframework.retry.backoff.SleepingBackOffPolicy; * @author Eddú Meléndez * @author Nakul Mishra * @author Tomaz Fernandes + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb * @since 1.5.0 */ @AutoConfiguration @@ -66,8 +76,12 @@ public class KafkaAutoConfiguration { private final KafkaProperties properties; - public KafkaAutoConfiguration(KafkaProperties properties) { + private final KafkaConnectionDetails connectionDetails; + + KafkaAutoConfiguration(KafkaProperties properties, ObjectProvider connectionDetails) { this.properties = properties; + this.connectionDetails = connectionDetails + .getIfAvailable(() -> new PropertiesKafkaConnectionDetails(properties)); } @Bean @@ -94,8 +108,9 @@ public class KafkaAutoConfiguration { @ConditionalOnMissingBean(ConsumerFactory.class) public DefaultKafkaConsumerFactory kafkaConsumerFactory( ObjectProvider customizers) { - DefaultKafkaConsumerFactory factory = new DefaultKafkaConsumerFactory<>( - this.properties.buildConsumerProperties()); + Map properties = this.properties.buildConsumerProperties(); + applyKafkaConnectionDetailsForConsumer(properties); + DefaultKafkaConsumerFactory factory = new DefaultKafkaConsumerFactory<>(properties); customizers.orderedStream().forEach((customizer) -> customizer.customize(factory)); return factory; } @@ -104,8 +119,9 @@ public class KafkaAutoConfiguration { @ConditionalOnMissingBean(ProducerFactory.class) public DefaultKafkaProducerFactory kafkaProducerFactory( ObjectProvider customizers) { - DefaultKafkaProducerFactory factory = new DefaultKafkaProducerFactory<>( - this.properties.buildProducerProperties()); + Map properties = this.properties.buildProducerProperties(); + applyKafkaConnectionDetailsForProducer(properties); + DefaultKafkaProducerFactory factory = new DefaultKafkaProducerFactory<>(properties); String transactionIdPrefix = this.properties.getProducer().getTransactionIdPrefix(); if (transactionIdPrefix != null) { factory.setTransactionIdPrefix(transactionIdPrefix); @@ -140,7 +156,9 @@ public class KafkaAutoConfiguration { @Bean @ConditionalOnMissingBean public KafkaAdmin kafkaAdmin() { - KafkaAdmin kafkaAdmin = new KafkaAdmin(this.properties.buildAdminProperties()); + Map properties = this.properties.buildAdminProperties(); + applyKafkaConnectionDetailsForAdmin(properties); + KafkaAdmin kafkaAdmin = new KafkaAdmin(properties); KafkaProperties.Admin admin = this.properties.getAdmin(); if (admin.getCloseTimeout() != null) { kafkaAdmin.setCloseTimeout((int) admin.getCloseTimeout().getSeconds()); @@ -168,6 +186,34 @@ public class KafkaAutoConfiguration { return builder.create(kafkaTemplate); } + private void applyKafkaConnectionDetailsForConsumer(Map properties) { + properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, + nodesToStringList(this.connectionDetails.getConsumerBootstrapNodes())); + if (!(this.connectionDetails instanceof PropertiesKafkaConnectionDetails)) { + properties.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, "PLAINTEXT"); + } + } + + private void applyKafkaConnectionDetailsForProducer(Map properties) { + properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, + nodesToStringList(this.connectionDetails.getProducerBootstrapNodes())); + if (!(this.connectionDetails instanceof PropertiesKafkaConnectionDetails)) { + properties.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, "PLAINTEXT"); + } + } + + private void applyKafkaConnectionDetailsForAdmin(Map properties) { + properties.put(CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG, + nodesToStringList(this.connectionDetails.getAdminBootstrapNodes())); + if (!(this.connectionDetails instanceof PropertiesKafkaConnectionDetails)) { + properties.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, "PLAINTEXT"); + } + } + + private List nodesToStringList(List nodes) { + return nodes.stream().map((node) -> node.host() + ":" + node.port()).toList(); + } + private static void setBackOffPolicy(RetryTopicConfigurationBuilder builder, Topic retryTopic) { long delay = (retryTopic.getDelay() != null) ? retryTopic.getDelay().toMillis() : 0; if (delay > 0) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaConnectionDetails.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaConnectionDetails.java new file mode 100644 index 0000000000..3e1421d445 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaConnectionDetails.java @@ -0,0 +1,81 @@ +/* + * 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.autoconfigure.kafka; + +import java.util.List; + +import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; + +/** + * Details required to establish a connection to a Kafka service. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + */ +public interface KafkaConnectionDetails extends ConnectionDetails { + + /** + * Returns the list of bootstrap nodes. + * @return the list of bootstrap nodes + */ + List getBootstrapNodes(); + + /** + * Returns the list of bootstrap nodes used for consumers. + * @return the list of bootstrap nodes used for consumers + */ + default List getConsumerBootstrapNodes() { + return getBootstrapNodes(); + } + + /** + * Returns the list of bootstrap nodes used for producers. + * @return the list of bootstrap nodes used for producers + */ + default List getProducerBootstrapNodes() { + return getBootstrapNodes(); + } + + /** + * Returns the list of bootstrap nodes used for the admin. + * @return the list of bootstrap nodes used for the admin + */ + default List getAdminBootstrapNodes() { + return getBootstrapNodes(); + } + + /** + * Returns the list of bootstrap nodes used for Kafka Streams. + * @return the list of bootstrap nodes used for Kafka Streams + */ + default List getStreamsBootstrapNodes() { + return getBootstrapNodes(); + } + + /** + * A Kafka node. + * + * @param host the hostname + * @param port the port + */ + record Node(String host, int port) { + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaStreamsAnnotationDrivenConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaStreamsAnnotationDrivenConfiguration.java index feec421f84..a3ae685e7f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaStreamsAnnotationDrivenConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaStreamsAnnotationDrivenConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * 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. @@ -16,8 +16,11 @@ package org.springframework.boot.autoconfigure.kafka; +import java.util.List; import java.util.Map; +import org.apache.kafka.clients.CommonClientConfigs; +import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.streams.StreamsBuilder; import org.apache.kafka.streams.StreamsConfig; @@ -27,6 +30,7 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.kafka.KafkaConnectionDetails.Node; import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -42,6 +46,8 @@ import org.springframework.kafka.core.CleanupConfig; * @author Gary Russell * @author Stephane Nicoll * @author Eddú Meléndez + * @author Moritz Halbritter + * @author Andy Wilkinson */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass(StreamsBuilder.class) @@ -56,17 +62,21 @@ class KafkaStreamsAnnotationDrivenConfiguration { @ConditionalOnMissingBean @Bean(KafkaStreamsDefaultConfiguration.DEFAULT_STREAMS_CONFIG_BEAN_NAME) - KafkaStreamsConfiguration defaultKafkaStreamsConfig(Environment environment) { - Map streamsProperties = this.properties.buildStreamsProperties(); + KafkaStreamsConfiguration defaultKafkaStreamsConfig(Environment environment, + ObjectProvider connectionDetailsProvider) { + KafkaConnectionDetails connectionDetails = connectionDetailsProvider + .getIfAvailable(() -> new PropertiesKafkaConnectionDetails(this.properties)); + Map properties = this.properties.buildStreamsProperties(); + applyKafkaConnectionDetailsForStreams(connectionDetails, properties); if (this.properties.getStreams().getApplicationId() == null) { String applicationName = environment.getProperty("spring.application.name"); if (applicationName == null) { throw new InvalidConfigurationPropertyValueException("spring.kafka.streams.application-id", null, "This property is mandatory and fallback 'spring.application.name' is not set either."); } - streamsProperties.put(StreamsConfig.APPLICATION_ID_CONFIG, applicationName); + properties.put(StreamsConfig.APPLICATION_ID_CONFIG, applicationName); } - return new KafkaStreamsConfiguration(streamsProperties); + return new KafkaStreamsConfiguration(properties); } @Bean @@ -77,6 +87,19 @@ class KafkaStreamsAnnotationDrivenConfiguration { return new KafkaStreamsFactoryBeanConfigurer(this.properties, factoryBean); } + private void applyKafkaConnectionDetailsForStreams(KafkaConnectionDetails connectionDetails, + Map properties) { + properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, + nodesToStringList(connectionDetails.getStreamsBootstrapNodes())); + if (!(connectionDetails instanceof PropertiesKafkaConnectionDetails)) { + properties.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, "PLAINTEXT"); + } + } + + private List nodesToStringList(List nodes) { + return nodes.stream().map((node) -> node.host() + ":" + node.port()).toList(); + } + // Separate class required to avoid BeanCurrentlyInCreationException static class KafkaStreamsFactoryBeanConfigurer implements InitializingBean { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/PropertiesKafkaConnectionDetails.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/PropertiesKafkaConnectionDetails.java new file mode 100644 index 0000000000..83152cd917 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/PropertiesKafkaConnectionDetails.java @@ -0,0 +1,75 @@ +/* + * 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.autoconfigure.kafka; + +import java.util.List; + +/** + * Adapts {@link KafkaProperties} to {@link KafkaConnectionDetails}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class PropertiesKafkaConnectionDetails implements KafkaConnectionDetails { + + private final int DEFAULT_PORT = 9092; + + private final KafkaProperties properties; + + PropertiesKafkaConnectionDetails(KafkaProperties properties) { + this.properties = properties; + } + + @Override + public List getBootstrapNodes() { + return asNodes(this.properties.getBootstrapServers()); + } + + @Override + public List getConsumerBootstrapNodes() { + return bootstrapNodes(this.properties.getConsumer().getBootstrapServers()); + } + + @Override + public List getProducerBootstrapNodes() { + return bootstrapNodes(this.properties.getProducer().getBootstrapServers()); + } + + @Override + public List getStreamsBootstrapNodes() { + return bootstrapNodes(this.properties.getStreams().getBootstrapServers()); + } + + private List bootstrapNodes(List bootstrapServers) { + return (bootstrapServers != null) ? asNodes(bootstrapServers) : getBootstrapNodes(); + } + + private List asNodes(List bootstrapServers) { + return bootstrapServers.stream().map(this::asNode).toList(); + } + + private Node asNode(String bootstrapNode) { + int separatorIndex = bootstrapNode.indexOf(':'); + if (separatorIndex == -1) { + return new Node(bootstrapNode, this.DEFAULT_PORT); + } + return new Node(bootstrapNode.substring(0, separatorIndex), + Integer.parseInt(bootstrapNode.substring(separatorIndex + 1))); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfiguration.java index c6b65c50e3..1ae41a98f6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfiguration.java @@ -32,6 +32,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails; import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration.LiquibaseAutoConfigurationRuntimeHints; import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration.LiquibaseDataSourceCondition; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; @@ -61,6 +62,7 @@ import org.springframework.util.StringUtils; * @author András Deák * @author Ferenc Gratzer * @author Evgeniy Cheban + * @author Moritz Halbritter * @since 1.1.0 */ @AutoConfiguration(after = { DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class }) @@ -83,38 +85,34 @@ public class LiquibaseAutoConfiguration { @EnableConfigurationProperties(LiquibaseProperties.class) public static class LiquibaseConfiguration { - private final LiquibaseProperties properties; - - public LiquibaseConfiguration(LiquibaseProperties properties) { - this.properties = properties; - } - @Bean public SpringLiquibase liquibase(ObjectProvider dataSource, - @LiquibaseDataSource ObjectProvider liquibaseDataSource) { + @LiquibaseDataSource ObjectProvider liquibaseDataSource, LiquibaseProperties properties, + ObjectProvider connectionDetails) { SpringLiquibase liquibase = createSpringLiquibase(liquibaseDataSource.getIfAvailable(), - dataSource.getIfUnique()); - liquibase.setChangeLog(this.properties.getChangeLog()); - liquibase.setClearCheckSums(this.properties.isClearChecksums()); - liquibase.setContexts(this.properties.getContexts()); - liquibase.setDefaultSchema(this.properties.getDefaultSchema()); - liquibase.setLiquibaseSchema(this.properties.getLiquibaseSchema()); - liquibase.setLiquibaseTablespace(this.properties.getLiquibaseTablespace()); - liquibase.setDatabaseChangeLogTable(this.properties.getDatabaseChangeLogTable()); - liquibase.setDatabaseChangeLogLockTable(this.properties.getDatabaseChangeLogLockTable()); - liquibase.setDropFirst(this.properties.isDropFirst()); - liquibase.setShouldRun(this.properties.isEnabled()); - liquibase.setLabelFilter(this.properties.getLabelFilter()); - liquibase.setChangeLogParameters(this.properties.getParameters()); - liquibase.setRollbackFile(this.properties.getRollbackFile()); - liquibase.setTestRollbackOnUpdate(this.properties.isTestRollbackOnUpdate()); - liquibase.setTag(this.properties.getTag()); + dataSource.getIfUnique(), + connectionDetails.getIfAvailable(() -> new LiquibasePropertiesJdbcConnectionDetails(properties))); + liquibase.setChangeLog(properties.getChangeLog()); + liquibase.setClearCheckSums(properties.isClearChecksums()); + liquibase.setContexts(properties.getContexts()); + liquibase.setDefaultSchema(properties.getDefaultSchema()); + liquibase.setLiquibaseSchema(properties.getLiquibaseSchema()); + liquibase.setLiquibaseTablespace(properties.getLiquibaseTablespace()); + liquibase.setDatabaseChangeLogTable(properties.getDatabaseChangeLogTable()); + liquibase.setDatabaseChangeLogLockTable(properties.getDatabaseChangeLogLockTable()); + liquibase.setDropFirst(properties.isDropFirst()); + liquibase.setShouldRun(properties.isEnabled()); + liquibase.setLabelFilter(properties.getLabelFilter()); + liquibase.setChangeLogParameters(properties.getParameters()); + liquibase.setRollbackFile(properties.getRollbackFile()); + liquibase.setTestRollbackOnUpdate(properties.isTestRollbackOnUpdate()); + liquibase.setTag(properties.getTag()); return liquibase; } - private SpringLiquibase createSpringLiquibase(DataSource liquibaseDataSource, DataSource dataSource) { - LiquibaseProperties properties = this.properties; - DataSource migrationDataSource = getMigrationDataSource(liquibaseDataSource, dataSource, properties); + private SpringLiquibase createSpringLiquibase(DataSource liquibaseDataSource, DataSource dataSource, + JdbcConnectionDetails connectionDetails) { + DataSource migrationDataSource = getMigrationDataSource(liquibaseDataSource, dataSource, connectionDetails); SpringLiquibase liquibase = (migrationDataSource == liquibaseDataSource || migrationDataSource == dataSource) ? new SpringLiquibase() : new DataSourceClosingSpringLiquibase(); @@ -123,31 +121,34 @@ public class LiquibaseAutoConfiguration { } private DataSource getMigrationDataSource(DataSource liquibaseDataSource, DataSource dataSource, - LiquibaseProperties properties) { + JdbcConnectionDetails connectionDetails) { if (liquibaseDataSource != null) { return liquibaseDataSource; } - if (properties.getUrl() != null) { + String url = connectionDetails.getJdbcUrl(); + if (url != null) { DataSourceBuilder builder = DataSourceBuilder.create().type(SimpleDriverDataSource.class); - builder.url(properties.getUrl()); - applyCommonBuilderProperties(properties, builder); + builder.url(url); + applyConnectionDetails(connectionDetails, builder); return builder.build(); } - if (properties.getUser() != null && dataSource != null) { + String user = connectionDetails.getUsername(); + if (user != null && dataSource != null) { DataSourceBuilder builder = DataSourceBuilder.derivedFrom(dataSource) .type(SimpleDriverDataSource.class); - applyCommonBuilderProperties(properties, builder); + applyConnectionDetails(connectionDetails, builder); return builder.build(); } Assert.state(dataSource != null, "Liquibase migration DataSource missing"); return dataSource; } - private void applyCommonBuilderProperties(LiquibaseProperties properties, DataSourceBuilder builder) { - builder.username(properties.getUser()); - builder.password(properties.getPassword()); - if (StringUtils.hasText(properties.getDriverClassName())) { - builder.driverClassName(properties.getDriverClassName()); + private void applyConnectionDetails(JdbcConnectionDetails connectionDetails, DataSourceBuilder builder) { + builder.username(connectionDetails.getUsername()); + builder.password(connectionDetails.getPassword()); + String driverClassName = connectionDetails.getDriverClassName(); + if (StringUtils.hasText(driverClassName)) { + builder.driverClassName(driverClassName); } } @@ -164,6 +165,11 @@ public class LiquibaseAutoConfiguration { } + @ConditionalOnBean(JdbcConnectionDetails.class) + private static final class JdbcConnectionDetailsCondition { + + } + @ConditionalOnProperty(prefix = "spring.liquibase", name = "url") private static final class LiquibaseUrlCondition { @@ -180,4 +186,37 @@ public class LiquibaseAutoConfiguration { } + /** + * Adapts {@link LiquibaseProperties} to {@link JdbcConnectionDetails}. + */ + private static final class LiquibasePropertiesJdbcConnectionDetails implements JdbcConnectionDetails { + + private final LiquibaseProperties properties; + + private LiquibasePropertiesJdbcConnectionDetails(LiquibaseProperties properties) { + this.properties = properties; + } + + @Override + public String getUsername() { + return this.properties.getUser(); + } + + @Override + public String getPassword() { + return this.properties.getPassword(); + } + + @Override + public String getJdbcUrl() { + return this.properties.getUrl(); + } + + @Override + public String getDriverClassName() { + return this.properties.getDriverClassName(); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoAutoConfiguration.java index fbd2bf2c8f..8d3ce82282 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoAutoConfiguration.java @@ -62,8 +62,12 @@ public class MongoAutoConfiguration { } @Bean - MongoPropertiesClientSettingsBuilderCustomizer mongoPropertiesCustomizer(MongoProperties properties) { - return new MongoPropertiesClientSettingsBuilderCustomizer(properties); + StandardMongoClientSettingsBuilderCustomizer standardMongoSettingsCustomizer(MongoProperties properties, + ObjectProvider connectionDetailsProvider) { + MongoConnectionDetails connectionDetails = connectionDetailsProvider + .getIfAvailable(() -> new PropertiesMongoConnectionDetails(properties)); + return new StandardMongoClientSettingsBuilderCustomizer(connectionDetails.getConnectionString(), + properties.getUuidRepresentation()); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoConnectionDetails.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoConnectionDetails.java new file mode 100644 index 0000000000..bfd107505a --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoConnectionDetails.java @@ -0,0 +1,88 @@ +/* + * 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.autoconfigure.mongo; + +import com.mongodb.ConnectionString; + +import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; + +/** + * Details required to establish a connection to a MongoDB service. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + */ +public interface MongoConnectionDetails extends ConnectionDetails { + + /** + * The {@link ConnectionString} for MongoDB. + * @return the connection string + */ + ConnectionString getConnectionString(); + + /** + * GridFS configuration. + * @return the GridFS configuration or {@code null} + */ + default GridFs getGridFs() { + return null; + } + + /** + * GridFS configuration. + */ + interface GridFs { + + /** + * GridFS database name. + * @return the GridFS database name or {@code null} + */ + String getDatabase(); + + /** + * GridFS bucket name. + * @return the GridFS bucket name or {@code null} + */ + String getBucket(); + + /** + * Factory method to create a new {@link GridFs} instance. + * @param database the database + * @param bucket the bucket name + * @return a new {@link GridFs} instance + */ + static GridFs of(String database, String bucket) { + return new GridFs() { + + @Override + public String getDatabase() { + return database; + } + + @Override + public String getBucket() { + return bucket; + } + + }; + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoPropertiesClientSettingsBuilderCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoPropertiesClientSettingsBuilderCustomizer.java index df645ae837..222841879b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoPropertiesClientSettingsBuilderCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoPropertiesClientSettingsBuilderCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * 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. @@ -34,7 +34,10 @@ import org.springframework.util.CollectionUtils; * @author Scott Frederick * @author Safeer Ansari * @since 2.4.0 + * @deprecated since 3.1.0 in favor of + * {@link StandardMongoClientSettingsBuilderCustomizer} */ +@Deprecated(since = "3.1.0", forRemoval = true) public class MongoPropertiesClientSettingsBuilderCustomizer implements MongoClientSettingsBuilderCustomizer, Ordered { private final MongoProperties properties; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoReactiveAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoReactiveAutoConfiguration.java index 31319fafee..9d877159b1 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoReactiveAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoReactiveAutoConfiguration.java @@ -69,8 +69,12 @@ public class MongoReactiveAutoConfiguration { } @Bean - MongoPropertiesClientSettingsBuilderCustomizer mongoPropertiesCustomizer(MongoProperties properties) { - return new MongoPropertiesClientSettingsBuilderCustomizer(properties); + StandardMongoClientSettingsBuilderCustomizer standardMongoSettingsCustomizer(MongoProperties properties, + ObjectProvider connectionDetailsProvider) { + MongoConnectionDetails connectionDetails = connectionDetailsProvider + .getIfAvailable(() -> new PropertiesMongoConnectionDetails(properties)); + return new StandardMongoClientSettingsBuilderCustomizer(connectionDetails.getConnectionString(), + properties.getUuidRepresentation()); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/PropertiesMongoConnectionDetails.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/PropertiesMongoConnectionDetails.java new file mode 100644 index 0000000000..f627bf04d0 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/PropertiesMongoConnectionDetails.java @@ -0,0 +1,82 @@ +/* + * 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.autoconfigure.mongo; + +import com.mongodb.ConnectionString; + +/** + * Adapts {@link MongoProperties} to {@link MongoConnectionDetails}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + */ +public class PropertiesMongoConnectionDetails implements MongoConnectionDetails { + + private final MongoProperties properties; + + public PropertiesMongoConnectionDetails(MongoProperties properties) { + this.properties = properties; + } + + @Override + public ConnectionString getConnectionString() { + // mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database.collection][?options]] + if (this.properties.getUri() != null) { + return new ConnectionString(this.properties.getUri()); + } + StringBuilder builder = new StringBuilder("mongodb://"); + if (this.properties.getUsername() != null) { + builder.append(this.properties.getUsername()); + builder.append(":"); + builder.append(this.properties.getPassword()); + builder.append("@"); + } + builder.append((this.properties.getHost() != null) ? this.properties.getHost() : "localhost"); + if (this.properties.getPort() != null) { + builder.append(":"); + builder.append(this.properties.getPort()); + } + if (this.properties.getAdditionalHosts() != null) { + builder.append(String.join(",", this.properties.getAdditionalHosts())); + } + if (this.properties.getMongoClientDatabase() != null || this.properties.getReplicaSetName() != null + || this.properties.getAuthenticationDatabase() != null) { + builder.append("/"); + if (this.properties.getMongoClientDatabase() != null) { + builder.append(this.properties.getMongoClientDatabase()); + } + else if (this.properties.getAuthenticationDatabase() != null) { + builder.append(this.properties.getAuthenticationDatabase()); + } + if (this.properties.getReplicaSetName() != null) { + builder.append("?"); + builder.append("repliceSet="); + builder.append(this.properties.getReplicaSetName()); + } + } + return new ConnectionString(builder.toString()); + } + + @Override + public GridFs getGridFs() { + return GridFs.of(PropertiesMongoConnectionDetails.this.properties.getGridfs().getDatabase(), + PropertiesMongoConnectionDetails.this.properties.getGridfs().getBucket()); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/StandardMongoClientSettingsBuilderCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/StandardMongoClientSettingsBuilderCustomizer.java new file mode 100644 index 0000000000..3876cc3a36 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/StandardMongoClientSettingsBuilderCustomizer.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.autoconfigure.mongo; + +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import org.bson.UuidRepresentation; + +import org.springframework.core.Ordered; + +/** + * A {@link MongoClientSettingsBuilderCustomizer} that applies standard settings to a + * {@link MongoClientSettings}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + */ +public class StandardMongoClientSettingsBuilderCustomizer implements MongoClientSettingsBuilderCustomizer, Ordered { + + private final ConnectionString connectionString; + + private final UuidRepresentation uuidRepresentation; + + private int order = 0; + + public StandardMongoClientSettingsBuilderCustomizer(ConnectionString connectionString, + UuidRepresentation uuidRepresentation) { + this.connectionString = connectionString; + this.uuidRepresentation = uuidRepresentation; + } + + @Override + public void customize(MongoClientSettings.Builder settingsBuilder) { + settingsBuilder.uuidRepresentation(this.uuidRepresentation); + settingsBuilder.applyConnectionString(this.connectionString); + } + + @Override + public int getOrder() { + return this.order; + } + + /** + * Set the order value of this object. + * @param order the new order value + * @see #getOrder() + */ + public void setOrder(int order) { + this.order = order; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfiguration.java index 298a093f81..39b0d334f4 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfiguration.java @@ -36,12 +36,14 @@ import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.neo4j.Neo4jProperties.Authentication; import org.springframework.boot.autoconfigure.neo4j.Neo4jProperties.Pool; import org.springframework.boot.autoconfigure.neo4j.Neo4jProperties.Security; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; import org.springframework.context.annotation.Bean; import org.springframework.core.env.Environment; +import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** @@ -49,6 +51,9 @@ import org.springframework.util.StringUtils; * * @author Michael J. Simons * @author Stephane Nicoll + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb * @since 2.4.0 */ @AutoConfiguration @@ -56,50 +61,24 @@ import org.springframework.util.StringUtils; @EnableConfigurationProperties(Neo4jProperties.class) public class Neo4jAutoConfiguration { - private static final URI DEFAULT_SERVER_URI = URI.create("bolt://localhost:7687"); - @Bean @ConditionalOnMissingBean public Driver neo4jDriver(Neo4jProperties properties, Environment environment, - ObjectProvider configBuilderCustomizers) { - AuthToken authToken = mapAuthToken(properties.getAuthentication(), environment); - Config config = mapDriverConfig(properties, configBuilderCustomizers.orderedStream().toList()); - URI serverUri = determineServerUri(properties, environment); - return GraphDatabase.driver(serverUri, authToken, config); + ObjectProvider configBuilderCustomizers, + ObjectProvider connectionDetailsProvider) { + Neo4jConnectionDetails connectionDetails = connectionDetailsProvider + .getIfAvailable(() -> new PropertiesNeo4jConnectionDetails(properties)); + AuthToken authToken = connectionDetails.getAuthToken(); + Config config = mapDriverConfig(properties, connectionDetails, + configBuilderCustomizers.orderedStream().toList()); + return GraphDatabase.driver(connectionDetails.getUri(), authToken, config); } - URI determineServerUri(Neo4jProperties properties, Environment environment) { - URI uri = properties.getUri(); - return (uri != null) ? uri : DEFAULT_SERVER_URI; - } - - AuthToken mapAuthToken(Neo4jProperties.Authentication authentication, Environment environment) { - String username = authentication.getUsername(); - String password = authentication.getPassword(); - String kerberosTicket = authentication.getKerberosTicket(); - String realm = authentication.getRealm(); - - boolean hasUsername = StringUtils.hasText(username); - boolean hasPassword = StringUtils.hasText(password); - boolean hasKerberosTicket = StringUtils.hasText(kerberosTicket); - - if (hasUsername && hasKerberosTicket) { - throw new IllegalStateException(String - .format("Cannot specify both username ('%s') and kerberos ticket ('%s')", username, kerberosTicket)); - } - if (hasUsername && hasPassword) { - return AuthTokens.basic(username, password, realm); - } - if (hasKerberosTicket) { - return AuthTokens.kerberos(kerberosTicket); - } - return AuthTokens.none(); - } - - Config mapDriverConfig(Neo4jProperties properties, List customizers) { + Config mapDriverConfig(Neo4jProperties properties, Neo4jConnectionDetails connectionDetails, + List customizers) { Config.ConfigBuilder builder = Config.builder(); configurePoolSettings(builder, properties.getPool()); - URI uri = properties.getUri(); + URI uri = connectionDetails.getUri(); String scheme = (uri != null) ? uri.getScheme() : "bolt"; configureDriverSettings(builder, properties, isSimpleScheme(scheme)); builder.withLogging(new Neo4jSpringJclLogging()); @@ -191,4 +170,43 @@ public class Neo4jAutoConfiguration { } } + /** + * Adapts {@link Neo4jProperties} to {@link Neo4jConnectionDetails}. + */ + static class PropertiesNeo4jConnectionDetails implements Neo4jConnectionDetails { + + private final Neo4jProperties properties; + + PropertiesNeo4jConnectionDetails(Neo4jProperties properties) { + this.properties = properties; + } + + @Override + public URI getUri() { + URI uri = this.properties.getUri(); + return (uri != null) ? uri : Neo4jConnectionDetails.super.getUri(); + } + + @Override + public AuthToken getAuthToken() { + Authentication authentication = this.properties.getAuthentication(); + String username = authentication.getUsername(); + String kerberosTicket = authentication.getKerberosTicket(); + boolean hasUsername = StringUtils.hasText(username); + boolean hasKerberosTicket = StringUtils.hasText(kerberosTicket); + Assert.state(!(hasUsername && hasKerberosTicket), + () -> "Cannot specify both username ('%s') and kerberos ticket ('%s')".formatted(username, + kerberosTicket)); + String password = authentication.getPassword(); + if (hasUsername && StringUtils.hasText(password)) { + return AuthTokens.basic(username, password, authentication.getRealm()); + } + if (hasKerberosTicket) { + return AuthTokens.kerberos(kerberosTicket); + } + return AuthTokens.none(); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jConnectionDetails.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jConnectionDetails.java new file mode 100644 index 0000000000..17a950ebd8 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jConnectionDetails.java @@ -0,0 +1,52 @@ +/* + * 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.autoconfigure.neo4j; + +import java.net.URI; + +import org.neo4j.driver.AuthToken; +import org.neo4j.driver.AuthTokens; + +import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; + +/** + * Details required to establish a connection to a Neo4j service. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + */ +public interface Neo4jConnectionDetails extends ConnectionDetails { + + /** + * Returns the URI of the Neo4j server. Defaults to {@code bolt://localhost:7687"}. + * @return the Neo4j server URI + */ + default URI getUri() { + return URI.create("bolt://localhost:7687"); + } + + /** + * Returns the token to use for authentication. Defaults to {@link AuthTokens#none()}. + * @return the auth token + */ + default AuthToken getAuthToken() { + return AuthTokens.none(); + } + +} 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 c2833a6b26..31f9a46111 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 @@ -53,7 +53,7 @@ class ConnectionFactoryBeanCreationFailureAnalyzer private String getDescription(ConnectionFactoryBeanCreationException cause) { StringBuilder description = new StringBuilder(); description.append("Failed to configure a ConnectionFactory: "); - if (!StringUtils.hasText(cause.getProperties().getUrl())) { + if (!StringUtils.hasText(cause.getUrl())) { description.append("'url' attribute is not specified and "); } description.append(String.format("no embedded database could be configured.%n")); 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 419af5ebf1..f1c2e9e213 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 @@ -51,14 +51,18 @@ import org.springframework.util.StringUtils; * @author Mark Paluch * @author Stephane Nicoll * @author Rodolpho S. Couto + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb */ abstract class ConnectionFactoryConfigurations { - protected static ConnectionFactory createConnectionFactory(R2dbcProperties properties, ClassLoader classLoader, + protected static ConnectionFactory createConnectionFactory(R2dbcProperties properties, + R2dbcConnectionDetails connectionDetails, ClassLoader classLoader, List optionsCustomizers) { try { return org.springframework.boot.r2dbc.ConnectionFactoryBuilder - .withOptions(new ConnectionFactoryOptionsInitializer().initialize(properties, + .withOptions(new ConnectionFactoryOptionsInitializer().initialize(properties, connectionDetails, () -> EmbeddedDatabaseConnection.get(classLoader))) .configure((options) -> { for (ConnectionFactoryOptionsBuilderCustomizer optionsCustomizer : optionsCustomizers) { @@ -87,10 +91,12 @@ abstract class ConnectionFactoryConfigurations { static class PooledConnectionFactoryConfiguration { @Bean(destroyMethod = "dispose") - ConnectionPool connectionFactory(R2dbcProperties properties, ResourceLoader resourceLoader, + ConnectionPool connectionFactory(R2dbcProperties properties, + ObjectProvider connectionDetails, ResourceLoader resourceLoader, ObjectProvider customizers) { ConnectionFactory connectionFactory = createConnectionFactory(properties, - resourceLoader.getClassLoader(), customizers.orderedStream().toList()); + connectionDetails.getIfAvailable(), resourceLoader.getClassLoader(), + customizers.orderedStream().toList()); R2dbcProperties.Pool pool = properties.getPool(); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); ConnectionPoolConfiguration.Builder builder = ConnectionPoolConfiguration.builder(connectionFactory); @@ -116,10 +122,11 @@ abstract class ConnectionFactoryConfigurations { static class GenericConfiguration { @Bean - ConnectionFactory connectionFactory(R2dbcProperties properties, ResourceLoader resourceLoader, + ConnectionFactory connectionFactory(R2dbcProperties properties, + ObjectProvider connectionDetails, ResourceLoader resourceLoader, ObjectProvider customizers) { - return createConnectionFactory(properties, resourceLoader.getClassLoader(), - customizers.orderedStream().toList()); + return createConnectionFactory(properties, connectionDetails.getIfAvailable(), + resourceLoader.getClassLoader(), customizers.orderedStream().toList()); } } 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 index 6a17dfde1c..5bc1eed2cc 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * 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. @@ -16,12 +16,10 @@ 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; @@ -31,6 +29,9 @@ import org.springframework.util.StringUtils; * Initialize a {@link Builder} based on {@link R2dbcProperties}. * * @author Stephane Nicoll + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb */ class ConnectionFactoryOptionsInitializer { @@ -38,45 +39,31 @@ class ConnectionFactoryOptionsInitializer { * Initialize a {@link Builder ConnectionFactoryOptions.Builder} using the specified * properties. * @param properties the properties to use to initialize the builder + * @param connectionDetails the connection details 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, + ConnectionFactoryOptions.Builder initialize(R2dbcProperties properties, R2dbcConnectionDetails connectionDetails, Supplier embeddedDatabaseConnection) { - if (StringUtils.hasText(properties.getUrl())) { - return initializeRegularOptions(properties); + if (connectionDetails != null) { + return connectionDetails.getConnectionFactoryOptions().mutate(); } EmbeddedDatabaseConnection embeddedConnection = embeddedDatabaseConnection.get(); if (embeddedConnection != EmbeddedDatabaseConnection.NONE) { return initializeEmbeddedOptions(properties, embeddedConnection); } - throw connectionFactoryBeanCreationException("Failed to determine a suitable R2DBC Connection URL", properties, + throw connectionFactoryBeanCreationException("Failed to determine a suitable R2DBC Connection URL", null, 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); + throw connectionFactoryBeanCreationException("Failed to determine a suitable R2DBC Connection URL", url, + embeddedDatabaseConnection); } Builder builder = ConnectionFactoryOptions.parse(url).mutate(); String username = determineEmbeddedUsername(properties); @@ -89,6 +76,11 @@ class ConnectionFactoryOptionsInitializer { return builder; } + private String determineEmbeddedDatabaseName(R2dbcProperties properties) { + String databaseName = determineDatabaseName(properties); + return (databaseName != null) ? databaseName : "testdb"; + } + private String determineDatabaseName(R2dbcProperties properties) { if (properties.isGenerateUniqueName()) { return properties.determineUniqueName(); @@ -99,30 +91,14 @@ class ConnectionFactoryOptionsInitializer { 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); + String r2dbcUrl, EmbeddedDatabaseConnection embeddedDatabaseConnection) { + return new ConnectionFactoryBeanCreationException(message, r2dbcUrl, embeddedDatabaseConnection); } private String ifHasText(String candidate) { @@ -131,25 +107,25 @@ class ConnectionFactoryOptionsInitializer { static class ConnectionFactoryBeanCreationException extends BeanCreationException { - private final R2dbcProperties properties; + private final String url; private final EmbeddedDatabaseConnection embeddedDatabaseConnection; - ConnectionFactoryBeanCreationException(String message, R2dbcProperties properties, + ConnectionFactoryBeanCreationException(String message, String url, EmbeddedDatabaseConnection embeddedDatabaseConnection) { super(message); - this.properties = properties; + this.url = url; this.embeddedDatabaseConnection = embeddedDatabaseConnection; } + String getUrl() { + return this.url; + } + 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/R2dbcAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcAutoConfiguration.java index d320d1ae88..7dc72964c7 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * 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. @@ -16,16 +16,26 @@ package org.springframework.boot.autoconfigure.r2dbc; +import java.util.function.Predicate; +import java.util.function.Supplier; + import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.ConnectionFactoryOptions; +import io.r2dbc.spi.ConnectionFactoryOptions.Builder; +import io.r2dbc.spi.Option; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnResource; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; +import org.springframework.util.StringUtils; /** * {@link EnableAutoConfiguration Auto-configuration} for R2DBC. @@ -42,4 +52,63 @@ import org.springframework.context.annotation.Import; ConnectionFactoryConfigurations.GenericConfiguration.class, ConnectionFactoryDependentConfiguration.class }) public class R2dbcAutoConfiguration { + @Bean + @ConditionalOnMissingBean(R2dbcConnectionDetails.class) + @ConditionalOnProperty("spring.r2dbc.url") + PropertiesR2dbcConnectionDetails propertiesR2dbcConnectionDetails(R2dbcProperties properties) { + return new PropertiesR2dbcConnectionDetails(properties); + } + + /** + * Adapts {@link R2dbcProperties} to {@link R2dbcConnectionDetails}. + */ + static class PropertiesR2dbcConnectionDetails implements R2dbcConnectionDetails { + + private final R2dbcProperties properties; + + PropertiesR2dbcConnectionDetails(R2dbcProperties properties) { + this.properties = properties; + } + + @Override + public ConnectionFactoryOptions getConnectionFactoryOptions() { + ConnectionFactoryOptions urlOptions = ConnectionFactoryOptions.parse(this.properties.getUrl()); + Builder optionsBuilder = urlOptions.mutate(); + configureIf(optionsBuilder, urlOptions, ConnectionFactoryOptions.USER, this.properties::getUsername, + StringUtils::hasText); + configureIf(optionsBuilder, urlOptions, ConnectionFactoryOptions.PASSWORD, this.properties::getPassword, + StringUtils::hasText); + configureIf(optionsBuilder, urlOptions, ConnectionFactoryOptions.DATABASE, + () -> determineDatabaseName(this.properties), StringUtils::hasText); + if (this.properties.getProperties() != null) { + this.properties.getProperties() + .forEach((key, value) -> optionsBuilder.option(Option.valueOf(key), value)); + } + return optionsBuilder.build(); + } + + 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 String determineDatabaseName(R2dbcProperties properties) { + if (properties.isGenerateUniqueName()) { + return properties.determineUniqueName(); + } + if (StringUtils.hasLength(properties.getName())) { + return properties.getName(); + } + return null; + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcConnectionDetails.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcConnectionDetails.java new file mode 100644 index 0000000000..c2230a5c61 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcConnectionDetails.java @@ -0,0 +1,39 @@ +/* + * 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.autoconfigure.r2dbc; + +import io.r2dbc.spi.ConnectionFactoryOptions; + +import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; + +/** + * Details required to establish a connection to an SQL service using R2DBC. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + */ +public interface R2dbcConnectionDetails extends ConnectionDetails { + + /** + * Connection factory options for connecting to the database. + * @return the connection factory options + */ + ConnectionFactoryOptions getConnectionFactoryOptions(); + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/service/connection/ConnectionDetails.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/service/connection/ConnectionDetails.java new file mode 100644 index 0000000000..210f145fd8 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/service/connection/ConnectionDetails.java @@ -0,0 +1,35 @@ +/* + * 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.autoconfigure.service.connection; + +import org.springframework.boot.origin.OriginProvider; + +/** + * Base interface for types that provide the details required to establish a connection to + * a remote service. + *

    + * Implementation classes can also implement {@link OriginProvider} in order to provide + * origin information. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + */ +public interface ConnectionDetails { + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/service/connection/package-info.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/service/connection/package-info.java new file mode 100644 index 0000000000..87d65078fa --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/service/connection/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. + */ + +/** + * Support for service connections that affect auto-configuration. + */ +package org.springframework.boot.autoconfigure.service.connection; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java index 9244a0c097..6c3352c96a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java @@ -96,6 +96,9 @@ import static org.mockito.Mockito.mock; * @author Gary Russell * @author HaiTao Zhang * @author Franjo Zilic + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb */ @ExtendWith(OutputCaptureExtension.class) class RabbitAutoConfigurationTests { @@ -169,6 +172,26 @@ class RabbitAutoConfigurationTests { }); } + @Test + @SuppressWarnings("unchecked") + void testConnectionFactoryWithOverridesWhenUsingConnectionDetails() { + this.contextRunner.withUserConfiguration(TestConfiguration.class, ConnectionDetailsConfiguration.class) + .withPropertyValues("spring.rabbitmq.host:remote-server", "spring.rabbitmq.port:9000", + "spring.rabbitmq.username:alice", "spring.rabbitmq.password:secret", + "spring.rabbitmq.virtual_host:/vhost") + .run((context) -> { + CachingConnectionFactory connectionFactory = context.getBean(CachingConnectionFactory.class); + assertThat(connectionFactory.getHost()).isEqualTo("rabbit.example.com"); + assertThat(connectionFactory.getPort()).isEqualTo(12345); + assertThat(connectionFactory.getVirtualHost()).isEqualTo("/vhost-1"); + assertThat(connectionFactory.getUsername()).isEqualTo("user-1"); + assertThat(connectionFactory.getRabbitConnectionFactory().getPassword()).isEqualTo("password-1"); + List

    addresses = (List
    ) ReflectionTestUtils.getField(connectionFactory, "addresses"); + assertThat(addresses).containsExactly(new Address("rabbit.example.com", 12345), + new Address("rabbit2.example.com", 23456)); + }); + } + @Test @SuppressWarnings("unchecked") void testConnectionFactoryWithCustomConnectionNameStrategy() { @@ -1218,6 +1241,38 @@ class RabbitAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class ConnectionDetailsConfiguration { + + @Bean + RabbitConnectionDetails rabbitConnectionDetails() { + return new RabbitConnectionDetails() { + + @Override + public String getUsername() { + return "user-1"; + } + + @Override + public String getPassword() { + return "password-1"; + } + + @Override + public String getVirtualHost() { + return "/vhost-1"; + } + + @Override + public List
    getAddresses() { + return List.of(new Address("rabbit.example.com", 12345), new Address("rabbit2.example.com", 23456)); + } + + }; + } + + } + static class TestListener { @RabbitListener(queues = "test", autoStartup = "false") diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cassandra/CassandraAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cassandra/CassandraAutoConfigurationTests.java index 9007c60e07..8369789dc0 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cassandra/CassandraAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cassandra/CassandraAutoConfigurationTests.java @@ -18,6 +18,7 @@ package org.springframework.boot.autoconfigure.cassandra; import java.time.Duration; import java.util.Collections; +import java.util.List; import com.datastax.oss.driver.api.core.CqlIdentifier; import com.datastax.oss.driver.api.core.CqlSession; @@ -45,6 +46,9 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; * @author Eddú Meléndez * @author Stephane Nicoll * @author Ittay Stern + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb */ class CassandraAutoConfigurationTests { @@ -90,6 +94,26 @@ class CassandraAutoConfigurationTests { }); } + @Test + void shouldUseConnectionDetails() { + this.contextRunner + .withPropertyValues("spring.cassandra.contact-points=localhost:9042", "spring.cassandra.username=a-user", + "spring.cassandra.password=a-password", "spring.cassandra.local-datacenter=some-datacenter") + .withBean(CassandraConnectionDetails.class, this::cassandraConnectionDetails) + .run((context) -> { + assertThat(context).hasSingleBean(DriverConfigLoader.class); + DriverExecutionProfile configuration = context.getBean(DriverConfigLoader.class) + .getInitialConfig() + .getDefaultProfile(); + assertThat(configuration.getStringList(DefaultDriverOption.CONTACT_POINTS)) + .containsOnly("cassandra.example.com:9042"); + assertThat(configuration.getString(DefaultDriverOption.AUTH_PROVIDER_USER_NAME)).isEqualTo("user-1"); + assertThat(configuration.getString(DefaultDriverOption.AUTH_PROVIDER_PASSWORD)).isEqualTo("secret-1"); + assertThat(configuration.getString(DefaultDriverOption.LOAD_BALANCING_LOCAL_DATACENTER)) + .isEqualTo("datacenter-1"); + }); + } + @Test void driverConfigLoaderWithContactPointAndNoPort() { this.contextRunner @@ -310,6 +334,32 @@ class CassandraAutoConfigurationTests { }); } + private CassandraConnectionDetails cassandraConnectionDetails() { + return new CassandraConnectionDetails() { + + @Override + public List getContactPoints() { + return List.of(new Node("cassandra.example.com", 9042)); + } + + @Override + public String getUsername() { + return "user-1"; + } + + @Override + public String getPassword() { + return "secret-1"; + } + + @Override + public String getLocalDatacenter() { + return "datacenter-1"; + } + + }; + } + @Configuration(proxyBeanMethods = false) static class SimpleDriverConfigLoaderBuilderCustomizerConfig { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfigurationTests.java index 2edf5a8341..b02e6ffb4a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfigurationTests.java @@ -48,6 +48,9 @@ import static org.mockito.Mockito.mock; * * @author Eddú Meléndez * @author Stephane Nicoll + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb */ class CouchbaseAutoConfigurationTests { @@ -60,6 +63,19 @@ class CouchbaseAutoConfigurationTests { .doesNotHaveBean(Cluster.class)); } + @Test + void shouldUseConnectionDetails() { + this.contextRunner.withBean(CouchbaseConnectionDetails.class, this::couchbaseConnectionDetails) + .run((context) -> { + assertThat(context).hasSingleBean(ClusterEnvironment.class).hasSingleBean(Cluster.class); + Cluster cluster = context.getBean(Cluster.class); + assertThat(cluster.core()).extracting("connectionString.hosts") + .asList() + .extractingResultOf("host") + .containsExactly("couchbase.example.com"); + }); + } + @Test void connectionStringCreateEnvironmentAndCluster() { this.contextRunner.withUserConfiguration(CouchbaseTestConfiguration.class) @@ -71,6 +87,21 @@ class CouchbaseAutoConfigurationTests { }); } + @Test + void connectionDetailsShouldOverrideProperties() { + this.contextRunner.withBean(CouchbaseConnectionDetails.class, this::couchbaseConnectionDetails) + .withPropertyValues("spring.couchbase.connection-string=localhost", "spring.couchbase.username=a-user", + "spring.couchbase.password=a-password") + .run((context) -> { + assertThat(context).hasSingleBean(ClusterEnvironment.class).hasSingleBean(Cluster.class); + Cluster cluster = context.getBean(Cluster.class); + assertThat(cluster.core()).extracting("connectionString.hosts") + .asList() + .extractingResultOf("host") + .containsExactly("couchbase.example.com"); + }); + } + @Test void whenObjectMapperBeanIsDefinedThenClusterEnvironmentObjectMapperIsDerivedFromIt() { this.contextRunner.withUserConfiguration(CouchbaseTestConfiguration.class) @@ -176,6 +207,27 @@ class CouchbaseAutoConfigurationTests { }); } + private CouchbaseConnectionDetails couchbaseConnectionDetails() { + return new CouchbaseConnectionDetails() { + + @Override + public String getConnectionString() { + return "couchbase.example.com"; + } + + @Override + public String getUsername() { + return "user-1"; + } + + @Override + public String getPassword() { + return "password-1"; + } + + }; + } + @Configuration(proxyBeanMethods = false) static class ClusterEnvironmentCustomizerConfiguration { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/mongo/MongoDataAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/mongo/MongoDataAutoConfigurationTests.java index 4f0b12e0b1..6f75319a6e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/mongo/MongoDataAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/mongo/MongoDataAutoConfigurationTests.java @@ -19,6 +19,7 @@ package org.springframework.boot.autoconfigure.data.mongo; import java.time.LocalDateTime; import java.util.Arrays; +import com.mongodb.ConnectionString; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoClients; import org.junit.jupiter.api.Test; @@ -31,6 +32,7 @@ import org.springframework.boot.autoconfigure.data.mongo.city.City; import org.springframework.boot.autoconfigure.data.mongo.country.Country; import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; +import org.springframework.boot.autoconfigure.mongo.MongoConnectionDetails; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; @@ -58,6 +60,9 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Josh Long * @author Oliver Gierke * @author Mark Paluch + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb */ class MongoDataAutoConfigurationTests { @@ -80,6 +85,17 @@ class MongoDataAutoConfigurationTests { }); } + @Test + void usesMongoConnectionDetailsIfAvailable() { + this.contextRunner.withUserConfiguration(ConnectionDetailsConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(GridFsTemplate.class); + GridFsTemplate template = context.getBean(GridFsTemplate.class); + assertThat(template).hasFieldOrPropertyWithValue("bucket", "connection-details-bucket"); + MongoDatabaseFactory factory = (MongoDatabaseFactory) ReflectionTestUtils.getField(template, "dbFactory"); + assertThat(factory.getMongoDatabase().getName()).isEqualTo("grid-database-1"); + }); + } + @Test void whenGridFsBucketIsConfiguredThenGridFsTemplateIsAutoConfiguredAndUsesIt() { this.contextRunner.withPropertyValues("spring.data.mongodb.gridfs.bucket:test-bucket").run((context) -> { @@ -250,6 +266,28 @@ class MongoDataAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class ConnectionDetailsConfiguration { + + @Bean + MongoConnectionDetails mongoConnectionDetails() { + return new MongoConnectionDetails() { + + @Override + public ConnectionString getConnectionString() { + return new ConnectionString("mongodb://localhost/db"); + } + + @Override + public GridFs getGridFs() { + return GridFs.of("grid-database-1", "connection-details-bucket"); + } + + }; + } + + } + static class MyConverter implements Converter { @Override diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/mongo/MongoReactiveDataAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/mongo/MongoReactiveDataAutoConfigurationTests.java index 993aa22ac0..ee11af22c9 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/mongo/MongoReactiveDataAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/mongo/MongoReactiveDataAutoConfigurationTests.java @@ -16,13 +16,17 @@ package org.springframework.boot.autoconfigure.data.mongo; +import com.mongodb.ConnectionString; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.mongo.MongoConnectionDetails; import org.springframework.boot.autoconfigure.mongo.MongoReactiveAutoConfiguration; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.data.mongodb.ReactiveMongoDatabaseFactory; import org.springframework.data.mongodb.core.ReactiveMongoTemplate; import org.springframework.data.mongodb.gridfs.ReactiveGridFsTemplate; @@ -35,6 +39,9 @@ import static org.assertj.core.api.Assertions.assertThat; * * @author Mark Paluch * @author Artsiom Yudovin + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb */ class MongoReactiveDataAutoConfigurationTests { @@ -58,6 +65,15 @@ class MongoReactiveDataAutoConfigurationTests { .run((context) -> assertThat(grisFsTemplateDatabaseName(context)).isEqualTo("grid")); } + @Test + void usesMongoConnectionDetailsIfAvailable() { + this.contextRunner.withUserConfiguration(ConnectionDetailsConfiguration.class).run((context) -> { + assertThat(grisFsTemplateDatabaseName(context)).isEqualTo("grid-database-1"); + ReactiveGridFsTemplate template = context.getBean(ReactiveGridFsTemplate.class); + assertThat(template).hasFieldOrPropertyWithValue("bucket", "connection-details-bucket"); + }); + } + @Test void whenGridFsBucketIsConfiguredThenGridFsTemplateUsesIt() { this.contextRunner.withPropertyValues("spring.data.mongodb.gridfs.bucket:test-bucket").run((context) -> { @@ -82,4 +98,38 @@ class MongoReactiveDataAutoConfigurationTests { return factory.getMongoDatabase().block().getName(); } + @Configuration(proxyBeanMethods = false) + static class ConnectionDetailsConfiguration { + + @Bean + MongoConnectionDetails mongoConnectionDetails() { + return new MongoConnectionDetails() { + + @Override + public ConnectionString getConnectionString() { + return new ConnectionString("mongodb://localhost/db"); + } + + @Override + public GridFs getGridFs() { + return new GridFs() { + + @Override + public String getDatabase() { + return "grid-database-1"; + } + + @Override + public String getBucket() { + return "connection-details-bucket"; + } + + }; + } + + }; + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationJedisTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationJedisTests.java index 2ac757cbe6..043f5c8ccc 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationJedisTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationJedisTests.java @@ -39,6 +39,9 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Mark Paluch * @author Stephane Nicoll * @author Weix Sun + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb */ @ClassPathExclusions("lettuce-core-*.jar") class RedisAutoConfigurationJedisTests { @@ -79,6 +82,14 @@ class RedisAutoConfigurationJedisTests { }); } + @Test + void usesConnectionDetailsIfAvailable() { + this.contextRunner.withUserConfiguration(ConnectionDetailsConfiguration.class).run((context) -> { + JedisConnectionFactory cf = context.getBean(JedisConnectionFactory.class); + assertThat(cf.isUseSsl()).isFalse(); + }); + } + @Test void testRedisUrlConfiguration() { this.contextRunner @@ -240,6 +251,35 @@ class RedisAutoConfigurationJedisTests { } + @Configuration(proxyBeanMethods = false) + static class ConnectionDetailsConfiguration { + + @Bean + RedisConnectionDetails redisConnectionDetails() { + return new RedisConnectionDetails() { + + @Override + public Standalone getStandalone() { + return new Standalone() { + + @Override + public String getHost() { + return "localhost"; + } + + @Override + public int getPort() { + return 6379; + } + + }; + } + + }; + } + + } + @Configuration(proxyBeanMethods = false) static class JedisConnectionFactoryCaptorConfiguration { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationTests.java index e1ffb9baaa..fa1bfbc6ee 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationTests.java @@ -42,6 +42,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisClusterConfiguration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisNode; +import org.springframework.data.redis.connection.RedisPassword; import org.springframework.data.redis.connection.RedisSentinelConfiguration; import org.springframework.data.redis.connection.RedisStandaloneConfiguration; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; @@ -71,6 +72,9 @@ import static org.mockito.Mockito.mock; * @author Alen Turkovic * @author Scott Frederick * @author Weix Sun + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb */ class RedisAutoConfigurationTests { @@ -490,6 +494,51 @@ class RedisAutoConfigurationTests { (options) -> assertThat(options.getTopologyRefreshOptions().useDynamicRefreshSources()).isTrue())); } + @Test + void usesStandaloneFromConnectionDetailsIfAvailable() { + this.contextRunner.withUserConfiguration(ConnectionDetailsStandaloneConfiguration.class).run((context) -> { + LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class); + assertThat(cf.isUseSsl()).isFalse(); + RedisStandaloneConfiguration configuration = cf.getStandaloneConfiguration(); + assertThat(configuration.getHostName()).isEqualTo("redis.example.com"); + assertThat(configuration.getPort()).isEqualTo(16379); + assertThat(configuration.getDatabase()).isOne(); + assertThat(configuration.getUsername()).isEqualTo("user-1"); + assertThat(configuration.getPassword()).isEqualTo(RedisPassword.of("password-1")); + }); + } + + @Test + void usesSentinelFromConnectionDetailsIfAvailable() { + this.contextRunner.withUserConfiguration(ConnectionDetailsSentinelConfiguration.class).run((context) -> { + LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class); + assertThat(cf.isUseSsl()).isFalse(); + RedisSentinelConfiguration configuration = cf.getSentinelConfiguration(); + assertThat(configuration).isNotNull(); + assertThat(configuration.getSentinelUsername()).isEqualTo("sentinel-1"); + assertThat(configuration.getSentinelPassword().get()).isEqualTo("secret-1".toCharArray()); + assertThat(configuration.getSentinels()).containsExactly(new RedisNode("node-1", 12345)); + assertThat(configuration.getUsername()).isEqualTo("user-1"); + assertThat(configuration.getPassword()).isEqualTo(RedisPassword.of("password-1")); + assertThat(configuration.getDatabase()).isOne(); + assertThat(configuration.getMaster().getName()).isEqualTo("master.redis.example.com"); + }); + } + + @Test + void usesClusterFromConnectionDetailsIfAvailable() { + this.contextRunner.withUserConfiguration(ConnectionDetailsClusterConfiguration.class).run((context) -> { + LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class); + assertThat(cf.isUseSsl()).isFalse(); + RedisClusterConfiguration configuration = cf.getClusterConfiguration(); + assertThat(configuration).isNotNull(); + assertThat(configuration.getUsername()).isEqualTo("user-1"); + assertThat(configuration.getPassword().get()).isEqualTo("password-1".toCharArray()); + assertThat(configuration.getClusterNodes()).containsExactly(new RedisNode("node-1", 12345), + new RedisNode("node-2", 23456)); + }); + } + private ContextConsumer assertClientOptions( Class expectedType, Consumer options) { return (context) -> { @@ -532,4 +581,136 @@ class RedisAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class ConnectionDetailsStandaloneConfiguration { + + @Bean + RedisConnectionDetails redisConnectionDetails() { + return new RedisConnectionDetails() { + + @Override + public String getUsername() { + return "user-1"; + } + + @Override + public String getPassword() { + return "password-1"; + } + + @Override + public Standalone getStandalone() { + return new Standalone() { + + @Override + public int getDatabase() { + return 1; + } + + @Override + public String getHost() { + return "redis.example.com"; + } + + @Override + public int getPort() { + return 16379; + } + + }; + } + + }; + } + + } + + @Configuration(proxyBeanMethods = false) + static class ConnectionDetailsSentinelConfiguration { + + @Bean + RedisConnectionDetails redisConnectionDetails() { + return new RedisConnectionDetails() { + + @Override + public String getUsername() { + return "user-1"; + } + + @Override + public String getPassword() { + return "password-1"; + } + + @Override + public Sentinel getSentinel() { + return new Sentinel() { + + @Override + public int getDatabase() { + return 1; + } + + @Override + public String getMaster() { + return "master.redis.example.com"; + } + + @Override + public List getNodes() { + return List.of(new Node("node-1", 12345)); + } + + @Override + public String getUsername() { + return "sentinel-1"; + } + + @Override + public String getPassword() { + return "secret-1"; + } + + }; + } + + }; + } + + } + + @Configuration(proxyBeanMethods = false) + static class ConnectionDetailsClusterConfiguration { + + @Bean + RedisConnectionDetails redisConnectionDetails() { + return new RedisConnectionDetails() { + + @Override + public String getUsername() { + return "user-1"; + } + + @Override + public String getPassword() { + return "password-1"; + } + + @Override + public Cluster getCluster() { + return new Cluster() { + + @Override + public List getNodes() { + return List.of(new Node("node-1", 12345), new Node("node-2", 23456)); + } + + }; + } + + }; + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfigurationTests.java index 79b9c59e90..6e57d8112b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfigurationTests.java @@ -17,6 +17,7 @@ package org.springframework.boot.autoconfigure.elasticsearch; import java.time.Duration; +import java.util.List; import org.apache.http.HttpHost; import org.apache.http.auth.AuthScope; @@ -32,6 +33,7 @@ import org.elasticsearch.client.sniff.Sniffer; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchConnectionDetails.Node.Protocol; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; @@ -49,6 +51,8 @@ import static org.mockito.Mockito.mock; * @author Evgeniy Cheban * @author Filip Hrisafov * @author Andy Wilkinson + * @author Moritz Halbritter + * @author Phillip Webb */ class ElasticsearchRestClientAutoConfigurationTests { @@ -243,6 +247,65 @@ class ElasticsearchRestClientAutoConfigurationTests { }); } + @Test + void connectionDetailsAreUsedIfAvailable() { + this.contextRunner.withUserConfiguration(ConnectionDetailsConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(RestClient.class); + RestClient restClient = context.getBean(RestClient.class); + assertThat(restClient).hasFieldOrPropertyWithValue("pathPrefix", "/some-path"); + assertThat(restClient.getNodes().stream().map(Node::getHost).map(HttpHost::toString)) + .containsExactly("http://elastic.example.com:9200"); + assertThat(restClient) + .extracting("client.credentialsProvider", InstanceOfAssertFactories.type(CredentialsProvider.class)) + .satisfies((credentialsProvider) -> { + Credentials uriCredentials = credentialsProvider + .getCredentials(new AuthScope("any.elastic.example.com", 80)); + assertThat(uriCredentials.getUserPrincipal().getName()).isEqualTo("user-1"); + assertThat(uriCredentials.getPassword()).isEqualTo("password-1"); + }) + .satisfies((credentialsProvider) -> { + Credentials uriCredentials = credentialsProvider + .getCredentials(new AuthScope("elastic.example.com", 9200)); + assertThat(uriCredentials.getUserPrincipal().getName()).isEqualTo("node-user-1"); + assertThat(uriCredentials.getPassword()).isEqualTo("node-password-1"); + }); + + }); + } + + @Configuration(proxyBeanMethods = false) + static class ConnectionDetailsConfiguration { + + @Bean + ElasticsearchConnectionDetails elasticsearchConnectionDetails() { + return new ElasticsearchConnectionDetails() { + + @Override + public List getNodes() { + return List + .of(new Node("elastic.example.com", 9200, Protocol.HTTP, "node-user-1", "node-password-1")); + } + + @Override + public String getUsername() { + return "user-1"; + } + + @Override + public String getPassword() { + return "password-1"; + } + + @Override + public String getPathPrefix() { + return "/some-path"; + } + + }; + } + + } + @Configuration(proxyBeanMethods = false) static class BuilderCustomizerConfiguration { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java index 6ac6211f37..d904bc9be6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java @@ -49,6 +49,7 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration.FlywayAutoConfigurationRuntimeHints; import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration; +import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.boot.jdbc.SchemaManagement; @@ -120,6 +121,16 @@ class FlywayAutoConfigurationTests { }); } + @Test + void createsDataSourceWithNoDataSourceBeanAndJdbcConnectionDetails() { + this.contextRunner + .withUserConfiguration(JdbcConnectionDetailsConfiguration.class, MockFlywayMigrationStrategy.class) + .run((context) -> { + assertThat(context).hasSingleBean(Flyway.class); + assertThat(context.getBean(Flyway.class).getConfiguration().getDataSource()).isNotNull(); + }); + } + @Test void backsOffWithFlywayUrlAndNoSpringJdbc() { this.contextRunner.withPropertyValues("spring.flyway.url:jdbc:hsqldb:mem:" + UUID.randomUUID()) @@ -137,6 +148,28 @@ class FlywayAutoConfigurationTests { }); } + @Test + void createDataSourceWithJdbcConnectionDetails() { + this.contextRunner + .withUserConfiguration(EmbeddedDataSourceConfiguration.class, JdbcConnectionDetailsConfiguration.class, + MockFlywayMigrationStrategy.class) + .withPropertyValues("spring.flyway.url=jdbc:hsqldb:mem:flywaytest", "spring.flyway.user=some-user", + "spring.flyway.password=some-password", + "spring.flyway.driver-class-name=org.hsqldb.jdbc.JDBCDriver") + .run((context) -> { + assertThat(context).hasSingleBean(Flyway.class); + Flyway flyway = context.getBean(Flyway.class); + DataSource dataSource = flyway.getConfiguration().getDataSource(); + assertThat(dataSource).isInstanceOf(SimpleDriverDataSource.class); + SimpleDriverDataSource simpleDriverDataSource = (SimpleDriverDataSource) dataSource; + assertThat(simpleDriverDataSource.getUrl()) + .isEqualTo("jdbc:postgresql://database.example.com:12345/database-1"); + assertThat(simpleDriverDataSource.getUsername()).isEqualTo("user-1"); + assertThat(simpleDriverDataSource.getPassword()).isEqualTo("secret-1"); + assertThat(simpleDriverDataSource.getDriver()).isInstanceOf(org.postgresql.Driver.class); + }); + } + @Test void createDataSourceWithUser() { this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) @@ -200,6 +233,19 @@ class FlywayAutoConfigurationTests { }); } + @Test + void flywayDataSourceIsUsedWhenJdbcConnectionDetailsIsAvailable() { + this.contextRunner + .withUserConfiguration(FlywayDataSourceConfiguration.class, EmbeddedDataSourceConfiguration.class, + JdbcConnectionDetailsConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(JdbcConnectionDetails.class); + assertThat(context).hasSingleBean(Flyway.class); + assertThat(context.getBean(Flyway.class).getConfiguration().getDataSource()) + .isEqualTo(context.getBean("flywayDataSource")); + }); + } + @Test void flywayDataSourceWithoutDataSourceAutoConfiguration() { this.contextRunner.withUserConfiguration(FlywayDataSourceConfiguration.class).run((context) -> { @@ -1036,4 +1082,31 @@ class FlywayAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class JdbcConnectionDetailsConfiguration { + + @Bean + JdbcConnectionDetails jdbcConnectionDetails() { + return new JdbcConnectionDetails() { + + @Override + public String getJdbcUrl() { + return "jdbc:postgresql://database.example.com:12345/database-1"; + } + + @Override + public String getUsername() { + return "user-1"; + } + + @Override + public String getPassword() { + return "secret-1"; + } + + }; + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/influx/InfluxDbAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/influx/InfluxDbAutoConfigurationTests.java index 2bed18e3f7..5ce38d8377 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/influx/InfluxDbAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/influx/InfluxDbAutoConfigurationTests.java @@ -16,6 +16,7 @@ package org.springframework.boot.autoconfigure.influx; +import java.net.URI; import java.util.concurrent.TimeUnit; import okhttp3.OkHttpClient; @@ -38,6 +39,9 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Sergey Kuptsov * @author Stephane Nicoll * @author Eddú Meléndez + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb */ class InfluxDbAutoConfigurationTests { @@ -49,6 +53,27 @@ class InfluxDbAutoConfigurationTests { this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(InfluxDB.class)); } + @Test + void shouldUseConnectionDetails() { + this.contextRunner.withBean(InfluxDbConnectionDetails.class, this::influxDbConnectionDetails).run((context) -> { + assertThat(context).hasSingleBean(InfluxDB.class); + InfluxDB influxDb = context.getBean(InfluxDB.class); + assertThat(influxDb).hasFieldOrPropertyWithValue("hostName", "localhost"); + }); + } + + @Test + void connectionDetailsOverwriteProperties() { + this.contextRunner.withBean(InfluxDbConnectionDetails.class, this::influxDbConnectionDetails) + .withPropertyValues("spring.influx.url=http://some-other-host", "spring.influx.user=user", + "spring.influx.password=password") + .run((context) -> { + assertThat(context).hasSingleBean(InfluxDB.class); + InfluxDB influxDb = context.getBean(InfluxDB.class); + assertThat(influxDb).hasFieldOrPropertyWithValue("hostName", "localhost"); + }); + } + @Test void influxDbCanBeCustomized() { this.contextRunner @@ -95,6 +120,27 @@ class InfluxDbAutoConfigurationTests { return callFactory.readTimeoutMillis(); } + private InfluxDbConnectionDetails influxDbConnectionDetails() { + return new InfluxDbConnectionDetails() { + + @Override + public URI getUrl() { + return URI.create("http://localhost"); + } + + @Override + public String getUsername() { + return "user-1"; + } + + @Override + public String getPassword() { + return "password-1"; + } + + }; + } + @Configuration(proxyBeanMethods = false) static class CustomOkHttpClientBuilderProviderConfig { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfigurationTests.java index 181eab518d..7f4542a343 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfigurationTests.java @@ -36,6 +36,7 @@ import com.zaxxer.hikari.HikariDataSource; import io.r2dbc.spi.ConnectionFactory; import oracle.ucp.jdbc.PoolDataSourceImpl; import org.apache.commons.dbcp2.BasicDataSource; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.BeanCreationException; @@ -60,6 +61,9 @@ import static org.mockito.Mockito.mock; * * @author Dave Syer * @author Stephane Nicoll + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb */ class DataSourceAutoConfigurationTests { @@ -244,6 +248,41 @@ class DataSourceAutoConfigurationTests { .run((context) -> assertThat(context).doesNotHaveBean(DataSourceScriptDatabaseInitializer.class)); } + @Test + void dbcp2UsesJdbcConnectionDetailsIfAvailable() { + ApplicationContextRunner runner = new ApplicationContextRunner() + .withPropertyValues("spring.datasource.type=org.apache.commons.dbcp2.BasicDataSource", + "spring.datasource.dbcp2.url=jdbc:broken", "spring.datasource.dbcp2.username=alice", + "spring.datasource.dbcp2.password=secret") + .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class)); + runner.withUserConfiguration(JdbcConnectionDetailsConfiguration.class).run((context) -> { + DataSource dataSource = context.getBean(DataSource.class); + assertThat(dataSource).asInstanceOf(InstanceOfAssertFactories.type(BasicDataSource.class)) + .satisfies((dbcp2) -> { + assertThat(dbcp2.getUsername()).isEqualTo("user-1"); + assertThat(dbcp2.getPassword()).isEqualTo("password-1"); + assertThat(dbcp2.getDriverClassName()).isEqualTo("org.postgresql.Driver"); + assertThat(dbcp2.getUrl()).isEqualTo("jdbc:postgresql://postgres.example.com:12345/database-1"); + }); + }); + } + + @Test + void genericUsesJdbcConnectionDetailsIfAvailable() { + ApplicationContextRunner runner = new ApplicationContextRunner() + .withPropertyValues("spring.datasource.type=" + TestDataSource.class.getName()) + .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class)); + runner.withUserConfiguration(JdbcConnectionDetailsConfiguration.class).run((context) -> { + DataSource dataSource = context.getBean(DataSource.class); + assertThat(dataSource).isInstanceOf(TestDataSource.class); + TestDataSource source = (TestDataSource) dataSource; + assertThat(source.getUsername()).isEqualTo("user-1"); + assertThat(source.getPassword()).isEqualTo("password-1"); + assertThat(source.getDriver().getClass().getName()).isEqualTo("org.postgresql.Driver"); + assertThat(source.getUrl()).isEqualTo("jdbc:postgresql://postgres.example.com:12345/database-1"); + }); + } + private static Function hideConnectionPools() { return (runner) -> runner.withClassLoader(new FilteredClassLoader("org.apache.tomcat", "com.zaxxer.hikari", "org.apache.commons.dbcp2", "oracle.ucp.jdbc", "com.mchange")); @@ -259,6 +298,16 @@ class DataSourceAutoConfigurationTests { }); } + @Configuration(proxyBeanMethods = false) + static class JdbcConnectionDetailsConfiguration { + + @Bean + JdbcConnectionDetails sqlJdbcConnectionDetails() { + return new TestJdbcConnectionDetails(); + } + + } + @Configuration(proxyBeanMethods = false) static class TestDataSourceConfiguration { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/Dbcp2JdbcConnectionDetailsBeanPostProcessorTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/Dbcp2JdbcConnectionDetailsBeanPostProcessorTests.java new file mode 100644 index 0000000000..996e6bd416 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/Dbcp2JdbcConnectionDetailsBeanPostProcessorTests.java @@ -0,0 +1,50 @@ +/* + * 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.autoconfigure.jdbc; + +import org.apache.commons.dbcp2.BasicDataSource; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.jdbc.DatabaseDriver; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link Dbcp2JdbcConnectionDetailsBeanPostProcessor}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class Dbcp2JdbcConnectionDetailsBeanPostProcessorTests { + + @Test + void setUsernamePasswordAndUrl() { + BasicDataSource dataSource = new BasicDataSource(); + dataSource.setUrl("will-be-overwritten"); + dataSource.setUsername("will-be-overwritten"); + dataSource.setPassword("will-be-overwritten"); + dataSource.setDriverClassName("will-be-overwritten"); + new Dbcp2JdbcConnectionDetailsBeanPostProcessor(null).processDataSource(dataSource, + new TestJdbcConnectionDetails()); + assertThat(dataSource.getUrl()).isEqualTo("jdbc:postgresql://postgres.example.com:12345/database-1"); + assertThat(dataSource.getUsername()).isEqualTo("user-1"); + assertThat(dataSource.getPassword()).isEqualTo("password-1"); + assertThat(dataSource.getDriverClassName()).isEqualTo(DatabaseDriver.POSTGRESQL.getDriverClassName()); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariDataSourceConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariDataSourceConfigurationTests.java index e308663eb3..35a33edffe 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariDataSourceConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariDataSourceConfigurationTests.java @@ -19,10 +19,13 @@ package org.springframework.boot.autoconfigure.jdbc; import javax.sql.DataSource; import com.zaxxer.hikari.HikariDataSource; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import static org.assertj.core.api.Assertions.assertThat; @@ -31,9 +34,14 @@ import static org.assertj.core.api.Assertions.assertThat; * * @author Dave Syer * @author Stephane Nicoll + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb */ class HikariDataSourceConfigurationTests { + private static final String PREFIX = "spring.datasource.hikari."; + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class)) .withPropertyValues("spring.datasource.type=" + HikariDataSource.class.getName()); @@ -49,8 +57,7 @@ class HikariDataSourceConfigurationTests { @Test void testDataSourcePropertiesOverridden() { this.contextRunner - .withPropertyValues("spring.datasource.hikari.jdbc-url=jdbc:foo//bar/spam", - "spring.datasource.hikari.max-lifetime=1234") + .withPropertyValues(PREFIX + "jdbc-url=jdbc:foo//bar/spam", "spring.datasource.hikari.max-lifetime=1234") .run((context) -> { HikariDataSource ds = context.getBean(HikariDataSource.class); assertThat(ds.getJdbcUrl()).isEqualTo("jdbc:foo//bar/spam"); @@ -61,8 +68,7 @@ class HikariDataSourceConfigurationTests { @Test void testDataSourceGenericPropertiesOverridden() { this.contextRunner - .withPropertyValues( - "spring.datasource.hikari.data-source-properties.dataSourceClassName=org.h2.JDBCDataSource") + .withPropertyValues(PREFIX + "data-source-properties.dataSourceClassName=org.h2.JDBCDataSource") .run((context) -> { HikariDataSource ds = context.getBean(HikariDataSource.class); assertThat(ds.getDataSourceProperties().getProperty("dataSourceClassName")) @@ -90,12 +96,38 @@ class HikariDataSourceConfigurationTests { @Test void poolNameTakesPrecedenceOverName() { - this.contextRunner - .withPropertyValues("spring.datasource.name=myDS", "spring.datasource.hikari.pool-name=myHikariDS") + this.contextRunner.withPropertyValues("spring.datasource.name=myDS", PREFIX + "pool-name=myHikariDS") .run((context) -> { HikariDataSource ds = context.getBean(HikariDataSource.class); assertThat(ds.getPoolName()).isEqualTo("myHikariDS"); }); } + @Test + void usesConnectionDetailsIfAvailable() { + this.contextRunner.withUserConfiguration(ConnectionDetailsConfiguration.class) + .withPropertyValues(PREFIX + "url=jdbc:broken", PREFIX + "username=alice", PREFIX + "password=secret") + .run((context) -> { + DataSource dataSource = context.getBean(DataSource.class); + assertThat(dataSource).asInstanceOf(InstanceOfAssertFactories.type(HikariDataSource.class)) + .satisfies((hikari) -> { + assertThat(hikari.getUsername()).isEqualTo("user-1"); + assertThat(hikari.getPassword()).isEqualTo("password-1"); + assertThat(hikari.getDriverClassName()).isEqualTo("org.postgresql.Driver"); + assertThat(hikari.getJdbcUrl()) + .isEqualTo("jdbc:postgresql://postgres.example.com:12345/database-1"); + }); + }); + } + + @Configuration(proxyBeanMethods = false) + static class ConnectionDetailsConfiguration { + + @Bean + JdbcConnectionDetails sqlConnectionDetails() { + return new TestJdbcConnectionDetails(); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariJdbcConnectionDetailsBeanPostProcessorTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariJdbcConnectionDetailsBeanPostProcessorTests.java new file mode 100644 index 0000000000..8031bea661 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariJdbcConnectionDetailsBeanPostProcessorTests.java @@ -0,0 +1,50 @@ +/* + * 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.autoconfigure.jdbc; + +import com.zaxxer.hikari.HikariDataSource; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.jdbc.DatabaseDriver; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link HikariJdbcConnectionDetailsBeanPostProcessor}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class HikariJdbcConnectionDetailsBeanPostProcessorTests { + + @Test + void setUsernamePasswordAndUrl() { + HikariDataSource dataSource = new HikariDataSource(); + dataSource.setJdbcUrl("will-be-overwritten"); + dataSource.setUsername("will-be-overwritten"); + dataSource.setPassword("will-be-overwritten"); + dataSource.setDriverClassName(DatabaseDriver.H2.getDriverClassName()); + new HikariJdbcConnectionDetailsBeanPostProcessor(null).processDataSource(dataSource, + new TestJdbcConnectionDetails()); + assertThat(dataSource.getJdbcUrl()).isEqualTo("jdbc:postgresql://postgres.example.com:12345/database-1"); + assertThat(dataSource.getUsername()).isEqualTo("user-1"); + assertThat(dataSource.getPassword()).isEqualTo("password-1"); + assertThat(dataSource.getDriverClassName()).isEqualTo(DatabaseDriver.POSTGRESQL.getDriverClassName()); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/OracleUcpDataSourceConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/OracleUcpDataSourceConfigurationTests.java index 856fe0b408..c8905b9026 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/OracleUcpDataSourceConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/OracleUcpDataSourceConfigurationTests.java @@ -22,10 +22,13 @@ import javax.sql.DataSource; import oracle.ucp.jdbc.PoolDataSource; import oracle.ucp.jdbc.PoolDataSourceImpl; +import oracle.ucp.util.OpaqueString; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import static org.assertj.core.api.Assertions.assertThat; @@ -34,9 +37,14 @@ import static org.assertj.core.api.Assertions.assertThat; * * @author Fabio Grassi * @author Stephane Nicoll + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb */ class OracleUcpDataSourceConfigurationTests { + private static final String PREFIX = "spring.datasource.oracleucp."; + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class)) .withPropertyValues("spring.datasource.type=" + PoolDataSource.class.getName()); @@ -54,9 +62,7 @@ class OracleUcpDataSourceConfigurationTests { @Test void testDataSourcePropertiesOverridden() { - this.contextRunner - .withPropertyValues("spring.datasource.oracleucp.url=jdbc:foo//bar/spam", - "spring.datasource.oracleucp.max-idle-time=1234") + this.contextRunner.withPropertyValues(PREFIX + "url=jdbc:foo//bar/spam", PREFIX + "max-idle-time=1234") .run((context) -> { PoolDataSourceImpl ds = context.getBean(PoolDataSourceImpl.class); assertThat(ds.getURL()).isEqualTo("jdbc:foo//bar/spam"); @@ -66,11 +72,10 @@ class OracleUcpDataSourceConfigurationTests { @Test void testDataSourceConnectionPropertiesOverridden() { - this.contextRunner.withPropertyValues("spring.datasource.oracleucp.connection-properties.autoCommit=false") - .run((context) -> { - PoolDataSourceImpl ds = context.getBean(PoolDataSourceImpl.class); - assertThat(ds.getConnectionProperty("autoCommit")).isEqualTo("false"); - }); + this.contextRunner.withPropertyValues(PREFIX + "connection-properties.autoCommit=false").run((context) -> { + PoolDataSourceImpl ds = context.getBean(PoolDataSourceImpl.class); + assertThat(ds.getConnectionProperty("autoCommit")).isEqualTo("false"); + }); } @Test @@ -100,12 +105,38 @@ class OracleUcpDataSourceConfigurationTests { @Test void poolNameTakesPrecedenceOverName() { this.contextRunner - .withPropertyValues("spring.datasource.name=myDS", - "spring.datasource.oracleucp.connection-pool-name=myOracleUcpDS") + .withPropertyValues("spring.datasource.name=myDS", PREFIX + "connection-pool-name=myOracleUcpDS") .run((context) -> { PoolDataSourceImpl ds = context.getBean(PoolDataSourceImpl.class); assertThat(ds.getConnectionPoolName()).isEqualTo("myOracleUcpDS"); }); } + @Test + void usesJdbcConnectionDetailsIfAvailable() { + this.contextRunner.withUserConfiguration(ConnectionDetailsConfiguration.class) + .withPropertyValues(PREFIX + "url=jdbc:broken", PREFIX + "username=alice", PREFIX + "password=secret") + .run((context) -> { + DataSource dataSource = context.getBean(DataSource.class); + assertThat(dataSource).isInstanceOf(PoolDataSourceImpl.class); + PoolDataSourceImpl oracleUcp = (PoolDataSourceImpl) dataSource; + assertThat(oracleUcp.getUser()).isEqualTo("user-1"); + assertThat(oracleUcp).extracting("password") + .extracting((o) -> ((OpaqueString) o).get()) + .isEqualTo("password-1"); + assertThat(oracleUcp.getConnectionFactoryClassName()).isEqualTo("org.postgresql.Driver"); + assertThat(oracleUcp.getURL()).isEqualTo("jdbc:postgresql://postgres.example.com:12345/database-1"); + }); + } + + @Configuration(proxyBeanMethods = false) + static class ConnectionDetailsConfiguration { + + @Bean + JdbcConnectionDetails jdbcConnectionDetails() { + return new TestJdbcConnectionDetails(); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/OracleUcpJdbcConnectionDetailsBeanPostProcessorTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/OracleUcpJdbcConnectionDetailsBeanPostProcessorTests.java new file mode 100644 index 0000000000..8848bffcd2 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/OracleUcpJdbcConnectionDetailsBeanPostProcessorTests.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.autoconfigure.jdbc; + +import java.sql.SQLException; + +import oracle.ucp.jdbc.PoolDataSourceImpl; +import oracle.ucp.util.OpaqueString; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.jdbc.DatabaseDriver; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link OracleUcpJdbcConnectionDetailsBeanPostProcessor}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class OracleUcpJdbcConnectionDetailsBeanPostProcessorTests { + + @Test + void setUsernamePasswordAndUrl() throws SQLException { + PoolDataSourceImpl dataSource = new PoolDataSourceImpl(); + dataSource.setURL("will-be-overwritten"); + dataSource.setUser("will-be-overwritten"); + dataSource.setPassword("will-be-overwritten"); + dataSource.setConnectionFactoryClassName("will-be-overwritten"); + new OracleUcpJdbcConnectionDetailsBeanPostProcessor(null).processDataSource(dataSource, + new TestJdbcConnectionDetails()); + assertThat(dataSource.getURL()).isEqualTo("jdbc:postgresql://postgres.example.com:12345/database-1"); + assertThat(dataSource.getUser()).isEqualTo("user-1"); + assertThat(dataSource).extracting("password") + .extracting((password) -> ((OpaqueString) password).get()) + .isEqualTo("password-1"); + assertThat(dataSource.getConnectionFactoryClassName()) + .isEqualTo(DatabaseDriver.POSTGRESQL.getDriverClassName()); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/TestJdbcConnectionDetails.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/TestJdbcConnectionDetails.java new file mode 100644 index 0000000000..3f277b01e0 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/TestJdbcConnectionDetails.java @@ -0,0 +1,41 @@ +/* + * 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.autoconfigure.jdbc; + +/** + * {@link JdbcConnectionDetails} used in tests. + * + * @author Moritz Halbritter + */ +class TestJdbcConnectionDetails implements JdbcConnectionDetails { + + @Override + public String getJdbcUrl() { + return "jdbc:postgresql://postgres.example.com:12345/database-1"; + } + + @Override + public String getUsername() { + return "user-1"; + } + + @Override + public String getPassword() { + return "password-1"; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/TomcatDataSourceConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/TomcatDataSourceConfigurationTests.java index ff19386891..5bfc55a32e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/TomcatDataSourceConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/TomcatDataSourceConfigurationTests.java @@ -24,9 +24,11 @@ import org.apache.tomcat.jdbc.pool.interceptor.SlowQueryReport; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.util.TestPropertyValues; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; @@ -41,6 +43,9 @@ import static org.junit.jupiter.api.Assertions.fail; * * @author Dave Syer * @author Stephane Nicoll + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb */ class TomcatDataSourceConfigurationTests { @@ -48,6 +53,10 @@ class TomcatDataSourceConfigurationTests { private final AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class)) + .withPropertyValues("spring.datasource.type=" + org.apache.tomcat.jdbc.pool.DataSource.class.getName()); + @BeforeEach void init() { TestPropertyValues.of(PREFIX + "initialize:false").applyTo(this.context); @@ -106,6 +115,32 @@ class TomcatDataSourceConfigurationTests { assertThat(ds.getValidationInterval()).isEqualTo(3000L); } + @Test + void usesJdbcConnectionDetailsIfAvailable() { + this.contextRunner.withUserConfiguration(ConnectionDetailsConfiguration.class) + .withPropertyValues(PREFIX + "url=jdbc:broken", PREFIX + "username=alice", PREFIX + "password=secret") + .run((context) -> { + DataSource dataSource = context.getBean(DataSource.class); + assertThat(dataSource).isInstanceOf(org.apache.tomcat.jdbc.pool.DataSource.class); + org.apache.tomcat.jdbc.pool.DataSource tomcat = (org.apache.tomcat.jdbc.pool.DataSource) dataSource; + assertThat(tomcat.getPoolProperties().getUsername()).isEqualTo("user-1"); + assertThat(tomcat.getPoolProperties().getPassword()).isEqualTo("password-1"); + assertThat(tomcat.getPoolProperties().getDriverClassName()).isEqualTo("org.postgresql.Driver"); + assertThat(tomcat.getPoolProperties().getUrl()) + .isEqualTo("jdbc:postgresql://postgres.example.com:12345/database-1"); + }); + } + + @Configuration(proxyBeanMethods = false) + static class ConnectionDetailsConfiguration { + + @Bean + JdbcConnectionDetails jdbcConnectionDetails() { + return new TestJdbcConnectionDetails(); + } + + } + @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties @EnableMBeanExport diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/TomcatJdbcConnectionDetailsBeanPostProcessorTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/TomcatJdbcConnectionDetailsBeanPostProcessorTests.java new file mode 100644 index 0000000000..5906c6dfde --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/TomcatJdbcConnectionDetailsBeanPostProcessorTests.java @@ -0,0 +1,51 @@ +/* + * 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.autoconfigure.jdbc; + +import org.apache.tomcat.jdbc.pool.DataSource; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.jdbc.DatabaseDriver; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link TomcatJdbcConnectionDetailsBeanPostProcessor}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class TomcatJdbcConnectionDetailsBeanPostProcessorTests { + + @Test + void setUsernamePasswordAndUrl() { + DataSource dataSource = new DataSource(); + dataSource.setUrl("will-be-overwritten"); + dataSource.setUsername("will-be-overwritten"); + dataSource.setPassword("will-be-overwritten"); + dataSource.setDriverClassName("will-be-overwritten"); + new TomcatJdbcConnectionDetailsBeanPostProcessor(null).processDataSource(dataSource, + new TestJdbcConnectionDetails()); + assertThat(dataSource.getUrl()).isEqualTo("jdbc:postgresql://postgres.example.com:12345/database-1"); + assertThat(dataSource.getUsername()).isEqualTo("user-1"); + assertThat(dataSource.getPoolProperties().getPassword()).isEqualTo("password-1"); + assertThat(dataSource.getPoolProperties().getDriverClassName()) + .isEqualTo(DatabaseDriver.POSTGRESQL.getDriverClassName()); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfigurationTests.java index 54ff0354f3..96cb982fa5 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfigurationTests.java @@ -22,6 +22,7 @@ import javax.sql.XADataSource; import com.ibm.db2.jcc.DB2XADataSource; import org.hsqldb.jdbc.pool.JDBCXADataSource; import org.junit.jupiter.api.Test; +import org.postgresql.xa.PGXADataSource; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.jdbc.XADataSourceWrapper; @@ -40,6 +41,8 @@ import static org.mockito.Mockito.mock; * Tests for {@link XADataSourceAutoConfiguration}. * * @author Phillip Webb + * @author Moritz Halbritter + * @author Andy Wilkinson */ class XADataSourceAutoConfigurationTests { @@ -90,6 +93,20 @@ class XADataSourceAutoConfigurationTests { assertThat(dataSource.getLoginTimeout()).isEqualTo(123); } + @Test + void shouldUseConnectionDetailsIfAvailable() { + new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(XADataSourceAutoConfiguration.class)) + .withUserConfiguration(FromProperties.class, JdbcConnectionDetailsConfiguration.class) + .run((context) -> { + MockXADataSourceWrapper wrapper = context.getBean(MockXADataSourceWrapper.class); + PGXADataSource dataSource = (PGXADataSource) wrapper.getXaDataSource(); + assertThat(dataSource).isNotNull(); + assertThat(dataSource.getUrl()).startsWith("jdbc:postgresql://postgres.example.com:12345/database-1"); + assertThat(dataSource.getUser()).isEqualTo("user-1"); + assertThat(dataSource.getPassword()).isEqualTo("password-1"); + }); + } + private ApplicationContext createContext(Class configuration, String... env) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); TestPropertyValues.of(env).applyTo(context); @@ -98,6 +115,16 @@ class XADataSourceAutoConfigurationTests { return context; } + @Configuration(proxyBeanMethods = false) + static class JdbcConnectionDetailsConfiguration { + + @Bean + JdbcConnectionDetails jdbcConnectionDetails() { + return new TestJdbcConnectionDetails(); + } + + } + @Configuration(proxyBeanMethods = false) static class WrapExisting { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfigurationTests.java index 22cb10dcc1..013f574aeb 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfigurationTests.java @@ -99,6 +99,9 @@ import static org.mockito.Mockito.never; * @author Nakul Mishra * @author Tomaz Fernandes * @author Thomas Kåsene + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb */ class KafkaAutoConfigurationTests { @@ -161,6 +164,24 @@ class KafkaAutoConfigurationTests { }); } + @Test + void connectionDetailsAreAppliedToConsumer() { + this.contextRunner + .withPropertyValues("spring.kafka.bootstrap-servers=foo:1234", + "spring.kafka.consumer.bootstrap-servers=foo:1234", "spring.kafka.security.protocol=SSL", + "spring.kafka.consumer.security.protocol=SSL") + .withBean(KafkaConnectionDetails.class, this::kafkaConnectionDetails) + .run((context) -> { + DefaultKafkaConsumerFactory consumerFactory = context.getBean(DefaultKafkaConsumerFactory.class); + Map configs = consumerFactory.getConfigurationProperties(); + assertThat(configs).containsEntry(CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG, + Collections.singletonList("kafka.example.com:12345")); + assertThat(configs).containsEntry(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, + Collections.singletonList("kafka.example.com:12345")); + assertThat(configs).containsEntry(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, "PLAINTEXT"); + }); + } + @Test void producerProperties() { this.contextRunner.withPropertyValues("spring.kafka.clientId=cid", @@ -211,6 +232,24 @@ class KafkaAutoConfigurationTests { }); } + @Test + void connectionDetailsAreAppliedToProducer() { + this.contextRunner + .withPropertyValues("spring.kafka.bootstrap-servers=foo:1234", + "spring.kafka.producer.bootstrap-servers=foo:1234", "spring.kafka.security.protocol=SSL", + "spring.kafka.producer.security.protocol=SSL") + .withBean(KafkaConnectionDetails.class, this::kafkaConnectionDetails) + .run((context) -> { + DefaultKafkaProducerFactory producerFactory = context.getBean(DefaultKafkaProducerFactory.class); + Map configs = producerFactory.getConfigurationProperties(); + assertThat(configs).containsEntry(CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG, + Collections.singletonList("kafka.example.com:12345")); + assertThat(configs).containsEntry(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, + Collections.singletonList("kafka.example.com:12345")); + assertThat(configs).containsEntry(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, "PLAINTEXT"); + }); + } + @Test void adminProperties() { this.contextRunner @@ -252,6 +291,24 @@ class KafkaAutoConfigurationTests { }); } + @Test + void connectionDetailsAreAppliedToAdmin() { + this.contextRunner + .withPropertyValues("spring.kafka.bootstrap-servers=foo:1234", "spring.kafka.security.protocol=SSL", + "spring.kafka.admin.security.protocol=SSL") + .withBean(KafkaConnectionDetails.class, this::kafkaConnectionDetails) + .run((context) -> { + KafkaAdmin admin = context.getBean(KafkaAdmin.class); + Map configs = admin.getConfigurationProperties(); + assertThat(configs).containsEntry(CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG, + Collections.singletonList("kafka.example.com:12345")); + assertThat(configs).containsEntry(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, + Collections.singletonList("kafka.example.com:12345")); + assertThat(configs).containsEntry(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, "PLAINTEXT"); + assertThat(configs).containsEntry(AdminClientConfig.SECURITY_PROTOCOL_CONFIG, "PLAINTEXT"); + }); + } + @SuppressWarnings("unchecked") @Test void streamsProperties() { @@ -298,6 +355,27 @@ class KafkaAutoConfigurationTests { }); } + @Test + void connectionDetailsAreAppliedToStreams() { + this.contextRunner.withUserConfiguration(EnableKafkaStreamsConfiguration.class) + .withPropertyValues("spring.kafka.streams.auto-startup=false", "spring.kafka.streams.application-id=test", + "spring.kafka.bootstrap-servers=foo:1234", "spring.kafka.streams.bootstrap-servers=foo:1234", + "spring.kafka.security.protocol=SSL", "spring.kafka.streams.security.protocol=SSL") + .withBean(KafkaConnectionDetails.class, this::kafkaConnectionDetails) + .run((context) -> { + Properties configs = context + .getBean(KafkaStreamsDefaultConfiguration.DEFAULT_STREAMS_CONFIG_BEAN_NAME, + KafkaStreamsConfiguration.class) + .asProperties(); + assertThat(configs).containsEntry(CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG, + Collections.singletonList("kafka.example.com:12345")); + assertThat(configs).containsEntry(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, + Collections.singletonList("kafka.example.com:12345")); + assertThat(configs).containsEntry(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, "PLAINTEXT"); + assertThat(configs).containsEntry(StreamsConfig.SECURITY_PROTOCOL_CONFIG, "PLAINTEXT"); + }); + } + @SuppressWarnings("deprecation") @Deprecated(since = "3.1.0", forRemoval = true) void streamsCacheMaxSizeBuffering() { @@ -744,6 +822,17 @@ class KafkaAutoConfigurationTests { }); } + private KafkaConnectionDetails kafkaConnectionDetails() { + return new KafkaConnectionDetails() { + + @Override + public List getBootstrapNodes() { + return List.of(new Node("kafka.example.com", 12345)); + } + + }; + } + @Configuration(proxyBeanMethods = false) static class MessageConverterConfiguration { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java index f61a139983..39d6453692 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java @@ -28,6 +28,7 @@ import java.util.function.Consumer; import javax.sql.DataSource; +import com.zaxxer.hikari.HikariDataSource; import liquibase.integration.spring.SpringLiquibase; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -39,6 +40,7 @@ import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration; +import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails; import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration; import org.springframework.boot.autoconfigure.jooq.JooqAutoConfiguration; import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration.LiquibaseAutoConfigurationRuntimeHints; @@ -75,6 +77,8 @@ import static org.assertj.core.api.Assertions.contentOf; * @author Andrii Hrytsiuk * @author Ferenc Gratzer * @author Evgeniy Cheban + * @author Moritz Halbritter + * @author Phillip Webb */ @ExtendWith(OutputCaptureExtension.class) class LiquibaseAutoConfigurationTests { @@ -97,6 +101,18 @@ class LiquibaseAutoConfigurationTests { })); } + @Test + void createsDataSourceWithNoDataSourceBeanAndJdbcConnectionDetails() { + this.contextRunner.withSystemProperties("shouldRun=false") + .withUserConfiguration(JdbcConnectionDetailsConfiguration.class) + .run(assertLiquibase((liquibase) -> { + SimpleDriverDataSource dataSource = (SimpleDriverDataSource) liquibase.getDataSource(); + assertThat(dataSource.getUrl()).isEqualTo("jdbc:postgresql://database.example.com:12345/database-1"); + assertThat(dataSource.getUsername()).isEqualTo("user-1"); + assertThat(dataSource.getPassword()).isEqualTo("secret-1"); + })); + } + @Test void backsOffWithLiquibaseUrlAndNoSpringJdbc() { this.contextRunner.withPropertyValues("spring.liquibase.url:jdbc:hsqldb:mem:" + UUID.randomUUID()) @@ -116,6 +132,30 @@ class LiquibaseAutoConfigurationTests { })); } + @Test + void jdbcConnectionDetailsAreUsedIfAvailable() { + this.contextRunner.withSystemProperties("shouldRun=false") + .withUserConfiguration(EmbeddedDataSourceConfiguration.class, JdbcConnectionDetailsConfiguration.class) + .run(assertLiquibase((liquibase) -> { + SimpleDriverDataSource dataSource = (SimpleDriverDataSource) liquibase.getDataSource(); + assertThat(dataSource.getUrl()).isEqualTo("jdbc:postgresql://database.example.com:12345/database-1"); + assertThat(dataSource.getUsername()).isEqualTo("user-1"); + assertThat(dataSource.getPassword()).isEqualTo("secret-1"); + })); + } + + @Test + void liquibaseDataSourceIsUsedOverJdbcConnectionDetails() { + this.contextRunner + .withUserConfiguration(LiquibaseDataSourceConfiguration.class, JdbcConnectionDetailsConfiguration.class) + .run(assertLiquibase((liquibase) -> { + HikariDataSource dataSource = (HikariDataSource) liquibase.getDataSource(); + assertThat(dataSource.getJdbcUrl()).startsWith("jdbc:hsqldb:mem:liquibasetest"); + assertThat(dataSource.getUsername()).isEqualTo("sa"); + assertThat(dataSource.getPassword()).isNull(); + })); + } + @Test void changelogXml() { this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) @@ -509,6 +549,33 @@ class LiquibaseAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class JdbcConnectionDetailsConfiguration { + + @Bean + JdbcConnectionDetails jdbcConnectionDetails() { + return new JdbcConnectionDetails() { + + @Override + public String getJdbcUrl() { + return "jdbc:postgresql://database.example.com:12345/database-1"; + } + + @Override + public String getUsername() { + return "user-1"; + } + + @Override + public String getPassword() { + return "secret-1"; + } + + }; + } + + } + static class CustomH2Driver extends org.h2.Driver { } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoClientFactorySupportTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoClientFactorySupportTests.java index 984360a68e..87861cf2d0 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoClientFactorySupportTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoClientFactorySupportTests.java @@ -43,6 +43,7 @@ import static org.mockito.Mockito.mock; * @author Mark Paluch * @author Artsiom Yudovin * @author Scott Frederick + * @author Mortiz Halbritter */ abstract class MongoClientFactorySupportTests { @@ -110,10 +111,6 @@ abstract class MongoClientFactorySupportTests { assertThat(properties.isAutoIndexCreation()).isTrue(); } - protected T createMongoClient() { - return createMongoClient(null, MongoClientSettings.builder().build()); - } - protected T createMongoClient(MongoClientSettings settings) { return createMongoClient(null, settings); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoPropertiesClientSettingsBuilderCustomizerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoPropertiesClientSettingsBuilderCustomizerTests.java index 9e7199ae1e..021d298523 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoPropertiesClientSettingsBuilderCustomizerTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoPropertiesClientSettingsBuilderCustomizerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * 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. @@ -32,6 +32,7 @@ import static org.assertj.core.api.Assertions.assertThat; * * @author Scott Frederick */ +@Deprecated(since = "3.1.0", forRemoval = true) class MongoPropertiesClientSettingsBuilderCustomizerTests { private final MongoProperties properties = new MongoProperties(); @@ -188,6 +189,7 @@ class MongoPropertiesClientSettingsBuilderCustomizerTests { assertThat(settings.getRetryWrites()).isFalse(); } + @SuppressWarnings("removal") private MongoClientSettings customizeSettings() { MongoClientSettings.Builder settings = MongoClientSettings.builder(); new MongoPropertiesClientSettingsBuilderCustomizer(this.properties).customize(settings); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfigurationTests.java index 81cb723237..d9b73ce74b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfigurationTests.java @@ -26,20 +26,19 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import org.neo4j.driver.AuthToken; import org.neo4j.driver.AuthTokens; import org.neo4j.driver.Config; import org.neo4j.driver.Config.ConfigBuilder; import org.neo4j.driver.Driver; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.neo4j.Neo4jAutoConfiguration.PropertiesNeo4jConnectionDetails; import org.springframework.boot.autoconfigure.neo4j.Neo4jProperties.Authentication; import org.springframework.boot.autoconfigure.neo4j.Neo4jProperties.Security.TrustStrategy; import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.core.env.Environment; -import org.springframework.mock.env.MockEnvironment; +import org.springframework.context.annotation.Bean; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -50,6 +49,9 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException; * * @author Michael J. Simons * @author Stephane Nicoll + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb */ class Neo4jAutoConfigurationTests { @@ -103,6 +105,22 @@ class Neo4jAutoConfigurationTests { .hasMessageContaining("'%s' is not a supported scheme.", invalidScheme)); } + @Bean + void usesCustomConnectionDetails() { + this.contextRunner.withBean(Neo4jConnectionDetails.class, () -> new Neo4jConnectionDetails() { + + @Override + public URI getUri() { + return URI.create("bolt+ssc://localhost:12345"); + } + + }).run((context) -> { + assertThat(context).hasSingleBean(Driver.class); + Driver driver = context.getBean(Driver.class); + assertThat(driver.isEncrypted()).isTrue(); + }); + } + @Test void connectionTimeout() { Neo4jProperties properties = new Neo4jProperties(); @@ -118,8 +136,8 @@ class Neo4jAutoConfigurationTests { } @Test - void determineServerUriShouldDefaultToLocalhost() { - assertThat(determineServerUri(new Neo4jProperties(), new MockEnvironment())) + void uriShouldDefaultToLocalhost() { + assertThat(new PropertiesNeo4jConnectionDetails(new Neo4jProperties()).getUri()) .isEqualTo(URI.create("bolt://localhost:7687")); } @@ -128,44 +146,52 @@ class Neo4jAutoConfigurationTests { URI customUri = URI.create("bolt://localhost:4242"); Neo4jProperties properties = new Neo4jProperties(); properties.setUri(customUri); - assertThat(determineServerUri(properties, new MockEnvironment())).isEqualTo(customUri); + assertThat(new PropertiesNeo4jConnectionDetails(properties).getUri()).isEqualTo(customUri); } @Test void authenticationShouldDefaultToNone() { - assertThat(mapAuthToken(new Authentication())).isEqualTo(AuthTokens.none()); + assertThat(new PropertiesNeo4jConnectionDetails(new Neo4jProperties()).getAuthToken()) + .isEqualTo(AuthTokens.none()); } @Test void authenticationWithUsernameShouldEnableBasicAuth() { - Authentication authentication = new Authentication(); - authentication.setUsername("Farin"); - authentication.setPassword("Urlaub"); - assertThat(mapAuthToken(authentication)).isEqualTo(AuthTokens.basic("Farin", "Urlaub")); + Neo4jProperties properties = new Neo4jProperties(); + properties.getAuthentication().setUsername("Farin"); + properties.getAuthentication().setPassword("Urlaub"); + assertThat(new PropertiesNeo4jConnectionDetails(properties).getAuthToken()) + .isEqualTo(AuthTokens.basic("Farin", "Urlaub")); } @Test void authenticationWithUsernameAndRealmShouldEnableBasicAuth() { - Authentication authentication = new Authentication(); + Neo4jProperties properties = new Neo4jProperties(); + Authentication authentication = properties.getAuthentication(); authentication.setUsername("Farin"); authentication.setPassword("Urlaub"); authentication.setRealm("Test Realm"); - assertThat(mapAuthToken(authentication)).isEqualTo(AuthTokens.basic("Farin", "Urlaub", "Test Realm")); + assertThat(new PropertiesNeo4jConnectionDetails(properties).getAuthToken()) + .isEqualTo(AuthTokens.basic("Farin", "Urlaub", "Test Realm")); } @Test void authenticationWithKerberosTicketShouldEnableKerberos() { - Authentication authentication = new Authentication(); + Neo4jProperties properties = new Neo4jProperties(); + Authentication authentication = properties.getAuthentication(); authentication.setKerberosTicket("AABBCCDDEE"); - assertThat(mapAuthToken(authentication)).isEqualTo(AuthTokens.kerberos("AABBCCDDEE")); + assertThat(new PropertiesNeo4jConnectionDetails(properties).getAuthToken()) + .isEqualTo(AuthTokens.kerberos("AABBCCDDEE")); } @Test void authenticationWithBothUsernameAndKerberosShouldNotBeAllowed() { - Authentication authentication = new Authentication(); + Neo4jProperties properties = new Neo4jProperties(); + Authentication authentication = properties.getAuthentication(); authentication.setUsername("Farin"); authentication.setKerberosTicket("AABBCCDDEE"); - assertThatIllegalStateException().isThrownBy(() -> mapAuthToken(authentication)) + assertThatIllegalStateException() + .isThrownBy(() -> new PropertiesNeo4jConnectionDetails(properties).getAuthToken()) .withMessage("Cannot specify both username ('Farin') and kerberos ticket ('AABBCCDDEE')"); } @@ -279,20 +305,9 @@ class Neo4jAutoConfigurationTests { assertThat(mapDriverConfig(new Neo4jProperties()).logging()).isInstanceOf(Neo4jSpringJclLogging.class); } - private URI determineServerUri(Neo4jProperties properties, Environment environment) { - return new Neo4jAutoConfiguration().determineServerUri(properties, environment); - } - - private AuthToken mapAuthToken(Authentication authentication, Environment environment) { - return new Neo4jAutoConfiguration().mapAuthToken(authentication, environment); - } - - private AuthToken mapAuthToken(Authentication authentication) { - return mapAuthToken(authentication, new MockEnvironment()); - } - private Config mapDriverConfig(Neo4jProperties properties, ConfigBuilderCustomizer... customizers) { - return new Neo4jAutoConfiguration().mapDriverConfig(properties, Arrays.asList(customizers)); + return new Neo4jAutoConfiguration().mapDriverConfig(properties, + new PropertiesNeo4jConnectionDetails(properties), Arrays.asList(customizers)); } } 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 ee564a8954..43aad72589 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 @@ -27,6 +27,7 @@ import io.r2dbc.h2.H2ConnectionFactory; import io.r2dbc.pool.ConnectionPool; import io.r2dbc.pool.PoolMetrics; import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.ConnectionFactoryOptions; import io.r2dbc.spi.ConnectionFactoryProvider; import io.r2dbc.spi.Option; import io.r2dbc.spi.Wrapped; @@ -54,6 +55,9 @@ import static org.assertj.core.api.Assertions.assertThat; * * @author Mark Paluch * @author Stephane Nicoll + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb */ class R2dbcAutoConfigurationTests { @@ -68,7 +72,7 @@ class R2dbcAutoConfigurationTests { assertThat(context.getBean(ConnectionPool.class)).extracting(ConnectionPool::unwrap) .satisfies((connectionFactory) -> assertThat(connectionFactory) .asInstanceOf(type(OptionsCapableConnectionFactory.class)) - .extracting(Wrapped::unwrap) + .extracting(Wrapped::unwrap) .isExactlyInstanceOf(H2ConnectionFactory.class)); }); } @@ -306,6 +310,64 @@ class R2dbcAutoConfigurationTests { .doesNotHaveBean(DatabaseClient.class)); } + @Test + void shouldUseCustomConnectionDetailsIfAvailable() { + this.contextRunner.withPropertyValues("spring.r2dbc.pool.enabled=false") + .withUserConfiguration(ConnectionDetailsConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(ConnectionFactory.class); + OptionsCapableConnectionFactory connectionFactory = context + .getBean(OptionsCapableConnectionFactory.class); + ConnectionFactoryOptions options = connectionFactory.getOptions(); + assertThat(options.getValue(ConnectionFactoryOptions.DRIVER)).isEqualTo("postgresql"); + assertThat(options.getValue(ConnectionFactoryOptions.HOST)).isEqualTo("postgres.example.com"); + assertThat(options.getValue(ConnectionFactoryOptions.PORT)).isEqualTo(12345); + assertThat(options.getValue(ConnectionFactoryOptions.DATABASE)).isEqualTo("database-1"); + assertThat(options.getValue(ConnectionFactoryOptions.USER)).isEqualTo("user-1"); + assertThat(options.getValue(ConnectionFactoryOptions.PASSWORD)).isEqualTo("password-1"); + }); + } + + @Test + void configureWithUsernamePasswordAndUrlWithoutUserInfoUsesUsernameAndPassword() { + this.contextRunner + .withPropertyValues("spring.r2dbc.pool.enabled=false", + "spring.r2dbc.url:r2dbc:postgresql://postgres.example.com:4321/db", "spring.r2dbc.username=alice", + "spring.r2dbc.password=secret") + .run((context) -> { + assertThat(context).hasSingleBean(ConnectionFactory.class); + OptionsCapableConnectionFactory connectionFactory = context + .getBean(OptionsCapableConnectionFactory.class); + ConnectionFactoryOptions options = connectionFactory.getOptions(); + assertThat(options.getValue(ConnectionFactoryOptions.DRIVER)).isEqualTo("postgresql"); + assertThat(options.getValue(ConnectionFactoryOptions.HOST)).isEqualTo("postgres.example.com"); + assertThat(options.getValue(ConnectionFactoryOptions.PORT)).isEqualTo(4321); + assertThat(options.getValue(ConnectionFactoryOptions.DATABASE)).isEqualTo("db"); + assertThat(options.getValue(ConnectionFactoryOptions.USER)).isEqualTo("alice"); + assertThat(options.getValue(ConnectionFactoryOptions.PASSWORD)).isEqualTo("secret"); + }); + } + + @Test + void configureWithUsernamePasswordAndUrlWithUserInfoUsesUserInfo() { + this.contextRunner + .withPropertyValues("spring.r2dbc.pool.enabled=false", + "spring.r2dbc.url:r2dbc:postgresql://bob:password@postgres.example.com:9876/db", + "spring.r2dbc.username=alice", "spring.r2dbc.password=secret") + .run((context) -> { + assertThat(context).hasSingleBean(ConnectionFactory.class); + OptionsCapableConnectionFactory connectionFactory = context + .getBean(OptionsCapableConnectionFactory.class); + ConnectionFactoryOptions options = connectionFactory.getOptions(); + assertThat(options.getValue(ConnectionFactoryOptions.DRIVER)).isEqualTo("postgresql"); + assertThat(options.getValue(ConnectionFactoryOptions.HOST)).isEqualTo("postgres.example.com"); + assertThat(options.getValue(ConnectionFactoryOptions.PORT)).isEqualTo(9876); + assertThat(options.getValue(ConnectionFactoryOptions.DATABASE)).isEqualTo("db"); + assertThat(options.getValue(ConnectionFactoryOptions.USER)).isEqualTo("bob"); + assertThat(options.getValue(ConnectionFactoryOptions.PASSWORD)).isEqualTo("password"); + }); + } + private InstanceOfAssertFactory> type(Class type) { return InstanceOfAssertFactories.type(type); } @@ -342,4 +404,22 @@ class R2dbcAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class ConnectionDetailsConfiguration { + + @Bean + R2dbcConnectionDetails r2dbcConnectionDetails() { + return new R2dbcConnectionDetails() { + + @Override + public ConnectionFactoryOptions getConnectionFactoryOptions() { + return ConnectionFactoryOptions + .parse("r2dbc:postgresql://user-1:password-1@postgres.example.com:12345/database-1"); + } + + }; + } + + } + }