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 11d8575e441..2a4d5fe7f9d 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 00000000000..f4ca1c76ee4 --- /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/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 2edf5a8341d..b02e6ffb4a3 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 {