Create spring-boot-couchbase module
This commit is contained in:
committed by
Phillip Webb
parent
abfd521363
commit
42f12380da
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.couchbase.autoconfigure;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import com.couchbase.client.core.diagnostics.ClusterState;
|
||||
import com.couchbase.client.core.diagnostics.DiagnosticsResult;
|
||||
import com.couchbase.client.java.Bucket;
|
||||
import com.couchbase.client.java.Cluster;
|
||||
import com.couchbase.client.java.Collection;
|
||||
import com.couchbase.client.java.env.ClusterEnvironment;
|
||||
import com.couchbase.client.java.json.JsonObject;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.testcontainers.couchbase.BucketDefinition;
|
||||
import org.testcontainers.couchbase.CouchbaseContainer;
|
||||
import org.testcontainers.couchbase.CouchbaseService;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.boot.testsupport.container.TestImage;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link CouchbaseAutoConfiguration}.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
@Testcontainers(disabledWithoutDocker = true)
|
||||
class CouchbaseAutoConfigurationIntegrationTests {
|
||||
|
||||
private static final String BUCKET_NAME = "cbbucket";
|
||||
|
||||
@Container
|
||||
static final CouchbaseContainer couchbase = TestImage.container(CouchbaseContainer.class)
|
||||
.withEnabledServices(CouchbaseService.KV)
|
||||
.withCredentials("spring", "password")
|
||||
.withBucket(new BucketDefinition(BUCKET_NAME).withPrimaryIndex(false));
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(CouchbaseAutoConfiguration.class))
|
||||
.withPropertyValues("spring.couchbase.connection-string: " + couchbase.getConnectionString(),
|
||||
"spring.couchbase.username:spring", "spring.couchbase.password:password",
|
||||
"spring.couchbase.bucket.name:" + BUCKET_NAME, "spring.couchbase.env.timeouts.connect=2m",
|
||||
"spring.couchbase.env.timeouts.key-value=1m");
|
||||
|
||||
@Test
|
||||
void defaultConfiguration() {
|
||||
this.contextRunner.run((context) -> {
|
||||
assertThat(context).hasSingleBean(Cluster.class).hasSingleBean(ClusterEnvironment.class);
|
||||
Cluster cluster = context.getBean(Cluster.class);
|
||||
Bucket bucket = cluster.bucket(BUCKET_NAME);
|
||||
bucket.waitUntilReady(Duration.ofMinutes(5));
|
||||
DiagnosticsResult diagnostics = cluster.diagnostics();
|
||||
assertThat(diagnostics.state()).isEqualTo(ClusterState.ONLINE);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenCouchbaseIsUsingCustomObjectMapperThenJsonCanBeRoundTripped() {
|
||||
this.contextRunner.withBean(ObjectMapper.class, ObjectMapper::new).run((context) -> {
|
||||
Cluster cluster = context.getBean(Cluster.class);
|
||||
Bucket bucket = cluster.bucket(BUCKET_NAME);
|
||||
bucket.waitUntilReady(Duration.ofMinutes(5));
|
||||
Collection collection = bucket.defaultCollection();
|
||||
collection.insert("test-document", JsonObject.create().put("a", "alpha"));
|
||||
assertThat(collection.get("test-document").contentAsObject().get("a")).isEqualTo("alpha");
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.couchbase.autoconfigure;
|
||||
|
||||
import com.couchbase.client.java.env.ClusterEnvironment;
|
||||
import com.couchbase.client.java.env.ClusterEnvironment.Builder;
|
||||
|
||||
/**
|
||||
* Callback interface that can be implemented by beans wishing to customize the
|
||||
* {@link ClusterEnvironment} through a {@link Builder ClusterEnvironment.Builder} whilst
|
||||
* retaining default auto-configuration.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ClusterEnvironmentBuilderCustomizer {
|
||||
|
||||
/**
|
||||
* Customize the {@link Builder ClusterEnvironment.Builder}.
|
||||
* @param builder the builder to customize
|
||||
*/
|
||||
void customize(ClusterEnvironment.Builder builder);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.couchbase.autoconfigure;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyStore;
|
||||
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
|
||||
import com.couchbase.client.core.env.Authenticator;
|
||||
import com.couchbase.client.core.env.CertificateAuthenticator;
|
||||
import com.couchbase.client.core.env.PasswordAuthenticator;
|
||||
import com.couchbase.client.java.Cluster;
|
||||
import com.couchbase.client.java.ClusterOptions;
|
||||
import com.couchbase.client.java.codec.JacksonJsonSerializer;
|
||||
import com.couchbase.client.java.env.ClusterEnvironment;
|
||||
import com.couchbase.client.java.env.ClusterEnvironment.Builder;
|
||||
import com.couchbase.client.java.json.JsonValueModule;
|
||||
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.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.couchbase.autoconfigure.CouchbaseAutoConfiguration.CouchbaseCondition;
|
||||
import org.springframework.boot.couchbase.autoconfigure.CouchbaseProperties.Authentication.Jks;
|
||||
import org.springframework.boot.couchbase.autoconfigure.CouchbaseProperties.Authentication.Pem;
|
||||
import org.springframework.boot.couchbase.autoconfigure.CouchbaseProperties.Ssl;
|
||||
import org.springframework.boot.couchbase.autoconfigure.CouchbaseProperties.Timeouts;
|
||||
import org.springframework.boot.io.ApplicationResourceLoader;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.boot.ssl.pem.PemSslStore;
|
||||
import org.springframework.boot.ssl.pem.PemSslStoreDetails;
|
||||
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.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for Couchbase.
|
||||
*
|
||||
* @author Eddú Meléndez
|
||||
* @author Stephane Nicoll
|
||||
* @author Yulin Qin
|
||||
* @author Moritz Halbritter
|
||||
* @author Andy Wilkinson
|
||||
* @author Phillip Webb
|
||||
* @author Scott Frederick
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@AutoConfiguration(afterName = "org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration")
|
||||
@ConditionalOnClass(Cluster.class)
|
||||
@Conditional(CouchbaseCondition.class)
|
||||
@EnableConfigurationProperties(CouchbaseProperties.class)
|
||||
public class CouchbaseAutoConfiguration {
|
||||
|
||||
private final ResourceLoader resourceLoader;
|
||||
|
||||
private final CouchbaseProperties properties;
|
||||
|
||||
CouchbaseAutoConfiguration(ResourceLoader resourceLoader, CouchbaseProperties properties) {
|
||||
this.resourceLoader = ApplicationResourceLoader.get(resourceLoader);
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(CouchbaseConnectionDetails.class)
|
||||
PropertiesCouchbaseConnectionDetails couchbaseConnectionDetails(ObjectProvider<SslBundles> sslBundles) {
|
||||
return new PropertiesCouchbaseConnectionDetails(this.properties, sslBundles.getIfAvailable());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public ClusterEnvironment couchbaseClusterEnvironment(
|
||||
ObjectProvider<ClusterEnvironmentBuilderCustomizer> customizers,
|
||||
CouchbaseConnectionDetails connectionDetails) {
|
||||
Builder builder = initializeEnvironmentBuilder(connectionDetails);
|
||||
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public Authenticator couchbaseAuthenticator(CouchbaseConnectionDetails connectionDetails) throws IOException {
|
||||
if (connectionDetails.getUsername() != null && connectionDetails.getPassword() != null) {
|
||||
return PasswordAuthenticator.create(connectionDetails.getUsername(), connectionDetails.getPassword());
|
||||
}
|
||||
Pem pem = this.properties.getAuthentication().getPem();
|
||||
if (pem.getCertificates() != null) {
|
||||
PemSslStoreDetails details = new PemSslStoreDetails(null, pem.getCertificates(), pem.getPrivateKey());
|
||||
PemSslStore store = PemSslStore.load(details);
|
||||
return CertificateAuthenticator.fromKey(store.privateKey(), pem.getPrivateKeyPassword(),
|
||||
store.certificates());
|
||||
}
|
||||
Jks jks = this.properties.getAuthentication().getJks();
|
||||
if (jks.getLocation() != null) {
|
||||
Resource resource = this.resourceLoader.getResource(jks.getLocation());
|
||||
String keystorePassword = jks.getPassword();
|
||||
try (InputStream inputStream = resource.getInputStream()) {
|
||||
KeyStore store = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
store.load(inputStream, (keystorePassword != null) ? keystorePassword.toCharArray() : null);
|
||||
return CertificateAuthenticator.fromKeyStore(store, keystorePassword);
|
||||
}
|
||||
catch (GeneralSecurityException ex) {
|
||||
throw new IllegalStateException("Error reading Couchbase certificate store", ex);
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("Couchbase authentication requires username and password, or certificates");
|
||||
}
|
||||
|
||||
@Bean(destroyMethod = "disconnect")
|
||||
@ConditionalOnMissingBean
|
||||
public Cluster couchbaseCluster(ClusterEnvironment couchbaseClusterEnvironment, Authenticator authenticator,
|
||||
CouchbaseConnectionDetails connectionDetails) {
|
||||
ClusterOptions options = ClusterOptions.clusterOptions(authenticator).environment(couchbaseClusterEnvironment);
|
||||
return Cluster.connect(connectionDetails.getConnectionString(), options);
|
||||
}
|
||||
|
||||
private ClusterEnvironment.Builder initializeEnvironmentBuilder(CouchbaseConnectionDetails connectionDetails) {
|
||||
ClusterEnvironment.Builder builder = ClusterEnvironment.builder();
|
||||
Timeouts timeouts = this.properties.getEnv().getTimeouts();
|
||||
builder.timeoutConfig((config) -> config.kvTimeout(timeouts.getKeyValue())
|
||||
.analyticsTimeout(timeouts.getAnalytics())
|
||||
.kvDurableTimeout(timeouts.getKeyValueDurable())
|
||||
.queryTimeout(timeouts.getQuery())
|
||||
.viewTimeout(timeouts.getView())
|
||||
.searchTimeout(timeouts.getSearch())
|
||||
.managementTimeout(timeouts.getManagement())
|
||||
.connectTimeout(timeouts.getConnect())
|
||||
.disconnectTimeout(timeouts.getDisconnect()));
|
||||
CouchbaseProperties.Io io = this.properties.getEnv().getIo();
|
||||
builder.ioConfig((config) -> config.maxHttpConnections(io.getMaxEndpoints())
|
||||
.numKvConnections(io.getMinEndpoints())
|
||||
.idleHttpConnectionTimeout(io.getIdleHttpConnectionTimeout()));
|
||||
SslBundle sslBundle = connectionDetails.getSslBundle();
|
||||
if (sslBundle != null) {
|
||||
configureSsl(builder, sslBundle);
|
||||
}
|
||||
return builder;
|
||||
}
|
||||
|
||||
private void configureSsl(Builder builder, SslBundle sslBundle) {
|
||||
Assert.state(!sslBundle.getOptions().isSpecified(), "SSL Options cannot be specified with Couchbase");
|
||||
builder.securityConfig((config) -> {
|
||||
config.enableTls(true);
|
||||
TrustManagerFactory trustManagerFactory = sslBundle.getManagers().getTrustManagerFactory();
|
||||
if (trustManagerFactory != null) {
|
||||
config.trustManagerFactory(trustManagerFactory);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(ObjectMapper.class)
|
||||
static class JacksonConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnSingleCandidate(ObjectMapper.class)
|
||||
ClusterEnvironmentBuilderCustomizer jacksonClusterEnvironmentBuilderCustomizer(ObjectMapper objectMapper) {
|
||||
return new JacksonClusterEnvironmentBuilderCustomizer(
|
||||
objectMapper.copy().registerModule(new JsonValueModule()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class JacksonClusterEnvironmentBuilderCustomizer
|
||||
implements ClusterEnvironmentBuilderCustomizer, Ordered {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
private JacksonClusterEnvironmentBuilderCustomizer(ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void customize(Builder builder) {
|
||||
builder.jsonSerializer(JacksonJsonSerializer.create(this.objectMapper));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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("spring.couchbase.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;
|
||||
|
||||
private final SslBundles sslBundles;
|
||||
|
||||
PropertiesCouchbaseConnectionDetails(CouchbaseProperties properties, SslBundles sslBundles) {
|
||||
this.properties = properties;
|
||||
this.sslBundles = sslBundles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getConnectionString() {
|
||||
return this.properties.getConnectionString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsername() {
|
||||
return this.properties.getUsername();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPassword() {
|
||||
return this.properties.getPassword();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SslBundle getSslBundle() {
|
||||
Ssl ssl = this.properties.getEnv().getSsl();
|
||||
if (!ssl.getEnabled()) {
|
||||
return null;
|
||||
}
|
||||
if (StringUtils.hasLength(ssl.getBundle())) {
|
||||
Assert.notNull(this.sslBundles, "SSL bundle name has been set but no SSL bundles found in context");
|
||||
return this.sslBundles.getBundle(ssl.getBundle());
|
||||
}
|
||||
return SslBundle.systemDefault();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.couchbase.autoconfigure;
|
||||
|
||||
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
|
||||
/**
|
||||
* Details required to establish a connection to a Couchbase service.
|
||||
*
|
||||
* @author Moritz Halbritter
|
||||
* @author Andy Wilkinson
|
||||
* @author Phillip Webb
|
||||
* @since 4.0.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();
|
||||
|
||||
/**
|
||||
* SSL bundle to use.
|
||||
* @return the SSL bundle to use
|
||||
* @since 3.5.0
|
||||
*/
|
||||
default SslBundle getSslBundle() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,409 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.couchbase.autoconfigure;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Configuration properties for Couchbase.
|
||||
*
|
||||
* @author Eddú Meléndez
|
||||
* @author Stephane Nicoll
|
||||
* @author Yulin Qin
|
||||
* @author Brian Clozel
|
||||
* @author Michael Nitschinger
|
||||
* @author Scott Frederick
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@ConfigurationProperties("spring.couchbase")
|
||||
public class CouchbaseProperties {
|
||||
|
||||
/**
|
||||
* Connection string used to locate the Couchbase cluster.
|
||||
*/
|
||||
private String connectionString;
|
||||
|
||||
/**
|
||||
* Cluster username.
|
||||
*/
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* Cluster password.
|
||||
*/
|
||||
private String password;
|
||||
|
||||
private final Authentication authentication = new Authentication();
|
||||
|
||||
private final Env env = new Env();
|
||||
|
||||
public String getConnectionString() {
|
||||
return this.connectionString;
|
||||
}
|
||||
|
||||
public void setConnectionString(String connectionString) {
|
||||
this.connectionString = connectionString;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return this.username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return this.password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public Authentication getAuthentication() {
|
||||
return this.authentication;
|
||||
}
|
||||
|
||||
public Env getEnv() {
|
||||
return this.env;
|
||||
}
|
||||
|
||||
public static class Authentication {
|
||||
|
||||
private final Pem pem = new Pem();
|
||||
|
||||
private final Jks jks = new Jks();
|
||||
|
||||
public Pem getPem() {
|
||||
return this.pem;
|
||||
}
|
||||
|
||||
public Jks getJks() {
|
||||
return this.jks;
|
||||
}
|
||||
|
||||
public static class Pem {
|
||||
|
||||
/**
|
||||
* PEM-formatted certificates for certificate-based cluster authentication.
|
||||
*/
|
||||
private String certificates;
|
||||
|
||||
/**
|
||||
* PEM-formatted private key for certificate-based cluster authentication.
|
||||
*/
|
||||
private String privateKey;
|
||||
|
||||
/**
|
||||
* Private key password for certificate-based cluster authentication.
|
||||
*/
|
||||
private String privateKeyPassword;
|
||||
|
||||
public String getCertificates() {
|
||||
return this.certificates;
|
||||
}
|
||||
|
||||
public void setCertificates(String certificates) {
|
||||
this.certificates = certificates;
|
||||
}
|
||||
|
||||
public String getPrivateKey() {
|
||||
return this.privateKey;
|
||||
}
|
||||
|
||||
public void setPrivateKey(String privateKey) {
|
||||
this.privateKey = privateKey;
|
||||
}
|
||||
|
||||
public String getPrivateKeyPassword() {
|
||||
return this.privateKeyPassword;
|
||||
}
|
||||
|
||||
public void setPrivateKeyPassword(String privateKeyPassword) {
|
||||
this.privateKeyPassword = privateKeyPassword;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Jks {
|
||||
|
||||
/**
|
||||
* Java KeyStore location for certificate-based cluster authentication.
|
||||
*/
|
||||
private String location;
|
||||
|
||||
/**
|
||||
* Java KeyStore password for certificate-based cluster authentication.
|
||||
*/
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* Private key password for certificate-based cluster authentication.
|
||||
*/
|
||||
private String privateKeyPassword;
|
||||
|
||||
public String getLocation() {
|
||||
return this.location;
|
||||
}
|
||||
|
||||
public void setLocation(String location) {
|
||||
this.location = location;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return this.password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public String getPrivateKeyPassword() {
|
||||
return this.privateKeyPassword;
|
||||
}
|
||||
|
||||
public void setPrivateKeyPassword(String privateKeyPassword) {
|
||||
this.privateKeyPassword = privateKeyPassword;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Env {
|
||||
|
||||
private final Io io = new Io();
|
||||
|
||||
private final Ssl ssl = new Ssl();
|
||||
|
||||
private final Timeouts timeouts = new Timeouts();
|
||||
|
||||
public Io getIo() {
|
||||
return this.io;
|
||||
}
|
||||
|
||||
public Ssl getSsl() {
|
||||
return this.ssl;
|
||||
}
|
||||
|
||||
public Timeouts getTimeouts() {
|
||||
return this.timeouts;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Io {
|
||||
|
||||
/**
|
||||
* Minimum number of sockets per node.
|
||||
*/
|
||||
private int minEndpoints = 1;
|
||||
|
||||
/**
|
||||
* Maximum number of sockets per node.
|
||||
*/
|
||||
private int maxEndpoints = 12;
|
||||
|
||||
/**
|
||||
* Length of time an HTTP connection may remain idle before it is closed and
|
||||
* removed from the pool.
|
||||
*/
|
||||
private Duration idleHttpConnectionTimeout = Duration.ofSeconds(1);
|
||||
|
||||
public int getMinEndpoints() {
|
||||
return this.minEndpoints;
|
||||
}
|
||||
|
||||
public void setMinEndpoints(int minEndpoints) {
|
||||
this.minEndpoints = minEndpoints;
|
||||
}
|
||||
|
||||
public int getMaxEndpoints() {
|
||||
return this.maxEndpoints;
|
||||
}
|
||||
|
||||
public void setMaxEndpoints(int maxEndpoints) {
|
||||
this.maxEndpoints = maxEndpoints;
|
||||
}
|
||||
|
||||
public Duration getIdleHttpConnectionTimeout() {
|
||||
return this.idleHttpConnectionTimeout;
|
||||
}
|
||||
|
||||
public void setIdleHttpConnectionTimeout(Duration idleHttpConnectionTimeout) {
|
||||
this.idleHttpConnectionTimeout = idleHttpConnectionTimeout;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Ssl {
|
||||
|
||||
/**
|
||||
* Whether to enable SSL support. Enabled automatically if a "bundle" is provided
|
||||
* unless specified otherwise.
|
||||
*/
|
||||
private Boolean enabled;
|
||||
|
||||
/**
|
||||
* SSL bundle name.
|
||||
*/
|
||||
private String bundle;
|
||||
|
||||
public Boolean getEnabled() {
|
||||
return (this.enabled != null) ? this.enabled : StringUtils.hasText(this.bundle);
|
||||
}
|
||||
|
||||
public void setEnabled(Boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public String getBundle() {
|
||||
return this.bundle;
|
||||
}
|
||||
|
||||
public void setBundle(String bundle) {
|
||||
this.bundle = bundle;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Timeouts {
|
||||
|
||||
/**
|
||||
* Bucket connect timeout.
|
||||
*/
|
||||
private Duration connect = Duration.ofSeconds(10);
|
||||
|
||||
/**
|
||||
* Bucket disconnect timeout.
|
||||
*/
|
||||
private Duration disconnect = Duration.ofSeconds(10);
|
||||
|
||||
/**
|
||||
* Timeout for operations on a specific key-value.
|
||||
*/
|
||||
private Duration keyValue = Duration.ofMillis(2500);
|
||||
|
||||
/**
|
||||
* Timeout for operations on a specific key-value with a durability level.
|
||||
*/
|
||||
private Duration keyValueDurable = Duration.ofSeconds(10);
|
||||
|
||||
/**
|
||||
* N1QL query operations timeout.
|
||||
*/
|
||||
private Duration query = Duration.ofSeconds(75);
|
||||
|
||||
/**
|
||||
* Regular and geospatial view operations timeout.
|
||||
*/
|
||||
private Duration view = Duration.ofSeconds(75);
|
||||
|
||||
/**
|
||||
* Timeout for the search service.
|
||||
*/
|
||||
private Duration search = Duration.ofSeconds(75);
|
||||
|
||||
/**
|
||||
* Timeout for the analytics service.
|
||||
*/
|
||||
private Duration analytics = Duration.ofSeconds(75);
|
||||
|
||||
/**
|
||||
* Timeout for the management operations.
|
||||
*/
|
||||
private Duration management = Duration.ofSeconds(75);
|
||||
|
||||
public Duration getConnect() {
|
||||
return this.connect;
|
||||
}
|
||||
|
||||
public void setConnect(Duration connect) {
|
||||
this.connect = connect;
|
||||
}
|
||||
|
||||
public Duration getDisconnect() {
|
||||
return this.disconnect;
|
||||
}
|
||||
|
||||
public void setDisconnect(Duration disconnect) {
|
||||
this.disconnect = disconnect;
|
||||
}
|
||||
|
||||
public Duration getKeyValue() {
|
||||
return this.keyValue;
|
||||
}
|
||||
|
||||
public void setKeyValue(Duration keyValue) {
|
||||
this.keyValue = keyValue;
|
||||
}
|
||||
|
||||
public Duration getKeyValueDurable() {
|
||||
return this.keyValueDurable;
|
||||
}
|
||||
|
||||
public void setKeyValueDurable(Duration keyValueDurable) {
|
||||
this.keyValueDurable = keyValueDurable;
|
||||
}
|
||||
|
||||
public Duration getQuery() {
|
||||
return this.query;
|
||||
}
|
||||
|
||||
public void setQuery(Duration query) {
|
||||
this.query = query;
|
||||
}
|
||||
|
||||
public Duration getView() {
|
||||
return this.view;
|
||||
}
|
||||
|
||||
public void setView(Duration view) {
|
||||
this.view = view;
|
||||
}
|
||||
|
||||
public Duration getSearch() {
|
||||
return this.search;
|
||||
}
|
||||
|
||||
public void setSearch(Duration search) {
|
||||
this.search = search;
|
||||
}
|
||||
|
||||
public Duration getAnalytics() {
|
||||
return this.analytics;
|
||||
}
|
||||
|
||||
public void setAnalytics(Duration analytics) {
|
||||
this.analytics = analytics;
|
||||
}
|
||||
|
||||
public Duration getManagement() {
|
||||
return this.management;
|
||||
}
|
||||
|
||||
public void setManagement(Duration management) {
|
||||
this.management = management;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Auto-configuration for Couchbase.
|
||||
*/
|
||||
package org.springframework.boot.couchbase.autoconfigure;
|
||||
@@ -0,0 +1,136 @@
|
||||
{
|
||||
"groups": [],
|
||||
"properties": [
|
||||
{
|
||||
"name": "spring.couchbase.bootstrap-hosts",
|
||||
"type": "java.util.List<java.lang.String>",
|
||||
"description": "Couchbase nodes (host or IP address) to bootstrap from.",
|
||||
"deprecation": {
|
||||
"replacement": "spring.couchbase.connection-string",
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.couchbase.bucket.name",
|
||||
"type": "java.lang.String",
|
||||
"description": "Name of the bucket to connect to.",
|
||||
"deprecation": {
|
||||
"reason": "A bucket is no longer auto-configured.",
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.couchbase.bucket.password",
|
||||
"type": "java.lang.String",
|
||||
"description": "Password of the bucket.",
|
||||
"deprecation": {
|
||||
"reason": "A bucket is no longer auto-configured.",
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.couchbase.env.bootstrap.http-direct-port",
|
||||
"type": "java.lang.Integer",
|
||||
"description": "Port for the HTTP bootstrap.",
|
||||
"deprecation": {
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.couchbase.env.bootstrap.http-ssl-port",
|
||||
"type": "java.lang.Integer",
|
||||
"description": "Port for the HTTPS bootstrap.",
|
||||
"deprecation": {
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.couchbase.env.endpoints.key-value",
|
||||
"type": "java.lang.Integer",
|
||||
"description": "Number of sockets per node against the key/value service.",
|
||||
"deprecation": {
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.couchbase.env.endpoints.query",
|
||||
"type": "java.lang.Integer",
|
||||
"description": "Number of sockets per node against the query (N1QL) service.",
|
||||
"deprecation": {
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.couchbase.env.endpoints.queryservice.max-endpoints",
|
||||
"type": "java.lang.Integer",
|
||||
"description": "Maximum number of sockets per node.",
|
||||
"deprecation": {
|
||||
"replacement": "spring.couchbase.env.io.max-endpoints",
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.couchbase.env.endpoints.queryservice.min-endpoints",
|
||||
"type": "java.lang.Integer",
|
||||
"description": "Minimum number of sockets per node.",
|
||||
"deprecation": {
|
||||
"replacement": "spring.couchbase.env.io.min-endpoints",
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.couchbase.env.endpoints.view",
|
||||
"type": "java.lang.Integer",
|
||||
"description": "Number of sockets per node against the view service.",
|
||||
"deprecation": {
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.couchbase.env.endpoints.viewservice.max-endpoints",
|
||||
"type": "java.lang.Integer",
|
||||
"description": "Maximum number of sockets per node.",
|
||||
"deprecation": {
|
||||
"replacement": "spring.couchbase.env.io.max-endpoints",
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.couchbase.env.endpoints.viewservice.min-endpoints",
|
||||
"type": "java.lang.Integer",
|
||||
"description": "Minimum number of sockets per node.",
|
||||
"deprecation": {
|
||||
"replacement": "spring.couchbase.env.io.min-endpoints",
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.couchbase.env.ssl.key-store",
|
||||
"type": "java.lang.String",
|
||||
"description": "Path to the JVM key store that holds the certificates.",
|
||||
"deprecation": {
|
||||
"replacement": "spring.couchbase.env.ssl.bundle",
|
||||
"level": "error",
|
||||
"since": "3.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.couchbase.env.ssl.key-store-password",
|
||||
"type": "java.lang.String",
|
||||
"description": "Password used to access the key store.",
|
||||
"deprecation": {
|
||||
"replacement": "spring.couchbase.env.ssl.bundle",
|
||||
"level": "error",
|
||||
"since": "3.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.couchbase.env.timeouts.socket-connect",
|
||||
"type": "java.time.Duration",
|
||||
"description": "Socket connect connections timeout.",
|
||||
"deprecation": {
|
||||
"level": "error"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
org.springframework.boot.couchbase.autoconfigure.CouchbaseAutoConfiguration
|
||||
@@ -0,0 +1,329 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.couchbase.autoconfigure;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import com.couchbase.client.core.env.Authenticator;
|
||||
import com.couchbase.client.core.env.CertificateAuthenticator;
|
||||
import com.couchbase.client.core.env.IoConfig;
|
||||
import com.couchbase.client.core.env.PasswordAuthenticator;
|
||||
import com.couchbase.client.core.env.SecurityConfig;
|
||||
import com.couchbase.client.core.env.TimeoutConfig;
|
||||
import com.couchbase.client.java.Cluster;
|
||||
import com.couchbase.client.java.codec.JacksonJsonSerializer;
|
||||
import com.couchbase.client.java.codec.JsonSerializer;
|
||||
import com.couchbase.client.java.env.ClusterEnvironment;
|
||||
import com.couchbase.client.java.json.JsonValueModule;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
|
||||
import org.springframework.boot.couchbase.autoconfigure.CouchbaseAutoConfiguration.PropertiesCouchbaseConnectionDetails;
|
||||
import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration;
|
||||
import org.springframework.boot.ssl.NoSuchSslBundleException;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.boot.testsupport.classpath.resources.WithPackageResources;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import static org.assertj.core.api.Assertions.as;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link CouchbaseAutoConfiguration}.
|
||||
*
|
||||
* @author Eddú Meléndez
|
||||
* @author Stephane Nicoll
|
||||
* @author Moritz Halbritter
|
||||
* @author Andy Wilkinson
|
||||
* @author Phillip Webb
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
class CouchbaseAutoConfigurationTests {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(CouchbaseAutoConfiguration.class, SslAutoConfiguration.class));
|
||||
|
||||
@Test
|
||||
void connectionStringIsRequired() {
|
||||
this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(ClusterEnvironment.class)
|
||||
.doesNotHaveBean(Authenticator.class)
|
||||
.doesNotHaveBean(Cluster.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void definesPropertiesBasedConnectionDetailsByDefault() {
|
||||
this.contextRunner.withUserConfiguration(CouchbaseTestConfiguration.class)
|
||||
.withPropertyValues("spring.couchbase.connection-string=localhost")
|
||||
.run((context) -> assertThat(context).hasSingleBean(PropertiesCouchbaseConnectionDetails.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseCustomConnectionDetailsWhenDefined() {
|
||||
this.contextRunner.withBean(CouchbaseConnectionDetails.class, this::couchbaseConnectionDetails)
|
||||
.run((context) -> {
|
||||
assertThat(context).hasSingleBean(ClusterEnvironment.class)
|
||||
.hasSingleBean(Cluster.class)
|
||||
.hasSingleBean(PasswordAuthenticator.class)
|
||||
.hasSingleBean(CouchbaseConnectionDetails.class)
|
||||
.doesNotHaveBean(PropertiesCouchbaseConnectionDetails.class);
|
||||
Cluster cluster = context.getBean(Cluster.class);
|
||||
assertThat(cluster.core()).extracting("connectionString.hosts")
|
||||
.asInstanceOf(InstanceOfAssertFactories.LIST)
|
||||
.extractingResultOf("host")
|
||||
.containsExactly("couchbase.example.com");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void connectionStringCreateEnvironmentAndCluster() {
|
||||
this.contextRunner.withUserConfiguration(CouchbaseTestConfiguration.class)
|
||||
.withPropertyValues("spring.couchbase.connection-string=localhost")
|
||||
.run((context) -> {
|
||||
assertThat(context).hasSingleBean(ClusterEnvironment.class)
|
||||
.hasSingleBean(Authenticator.class)
|
||||
.hasSingleBean(Cluster.class);
|
||||
assertThat(context).doesNotHaveBean("couchbaseAuthenticator");
|
||||
assertThat(context.getBean(Cluster.class))
|
||||
.isSameAs(context.getBean(CouchbaseTestConfiguration.class).couchbaseCluster());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void connectionDetailsOverridesProperties() {
|
||||
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(PasswordAuthenticator.class)
|
||||
.hasSingleBean(Cluster.class);
|
||||
Cluster cluster = context.getBean(Cluster.class);
|
||||
assertThat(cluster.core()).extracting("connectionString.hosts")
|
||||
.asInstanceOf(InstanceOfAssertFactories.LIST)
|
||||
.extractingResultOf("host")
|
||||
.containsExactly("couchbase.example.com");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenObjectMapperBeanIsDefinedThenClusterEnvironmentObjectMapperIsDerivedFromIt() {
|
||||
this.contextRunner.withUserConfiguration(CouchbaseTestConfiguration.class)
|
||||
.withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class))
|
||||
.withPropertyValues("spring.couchbase.connection-string=localhost")
|
||||
.run((context) -> {
|
||||
ClusterEnvironment env = context.getBean(ClusterEnvironment.class);
|
||||
Set<Object> expectedModuleIds = new HashSet<>(
|
||||
context.getBean(ObjectMapper.class).getRegisteredModuleIds());
|
||||
expectedModuleIds.add(new JsonValueModule().getTypeId());
|
||||
JsonSerializer serializer = env.jsonSerializer();
|
||||
assertThat(serializer).extracting("wrapped")
|
||||
.isInstanceOf(JacksonJsonSerializer.class)
|
||||
.extracting("mapper", as(InstanceOfAssertFactories.type(ObjectMapper.class)))
|
||||
.extracting(ObjectMapper::getRegisteredModuleIds)
|
||||
.isEqualTo(expectedModuleIds);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void customizeJsonSerializer() {
|
||||
JsonSerializer customJsonSerializer = mock(JsonSerializer.class);
|
||||
this.contextRunner.withUserConfiguration(CouchbaseTestConfiguration.class)
|
||||
.withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class))
|
||||
.withBean(ClusterEnvironmentBuilderCustomizer.class,
|
||||
() -> (builder) -> builder.jsonSerializer(customJsonSerializer))
|
||||
.withPropertyValues("spring.couchbase.connection-string=localhost")
|
||||
.run((context) -> {
|
||||
ClusterEnvironment env = context.getBean(ClusterEnvironment.class);
|
||||
JsonSerializer serializer = env.jsonSerializer();
|
||||
assertThat(serializer).extracting("wrapped").isSameAs(customJsonSerializer);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void customizeEnvIo() {
|
||||
testClusterEnvironment((env) -> {
|
||||
IoConfig ioConfig = env.ioConfig();
|
||||
assertThat(ioConfig.numKvConnections()).isEqualTo(2);
|
||||
assertThat(ioConfig.maxHttpConnections()).isEqualTo(5);
|
||||
assertThat(ioConfig.idleHttpConnectionTimeout()).isEqualTo(Duration.ofSeconds(3));
|
||||
}, "spring.couchbase.env.io.min-endpoints=2", "spring.couchbase.env.io.max-endpoints=5",
|
||||
"spring.couchbase.env.io.idle-http-connection-timeout=3s");
|
||||
}
|
||||
|
||||
@Test
|
||||
void customizeEnvTimeouts() {
|
||||
testClusterEnvironment((env) -> {
|
||||
TimeoutConfig timeoutConfig = env.timeoutConfig();
|
||||
assertThat(timeoutConfig.connectTimeout()).isEqualTo(Duration.ofSeconds(1));
|
||||
assertThat(timeoutConfig.disconnectTimeout()).isEqualTo(Duration.ofSeconds(2));
|
||||
assertThat(timeoutConfig.kvTimeout()).isEqualTo(Duration.ofMillis(500));
|
||||
assertThat(timeoutConfig.kvDurableTimeout()).isEqualTo(Duration.ofMillis(750));
|
||||
assertThat(timeoutConfig.queryTimeout()).isEqualTo(Duration.ofSeconds(3));
|
||||
assertThat(timeoutConfig.viewTimeout()).isEqualTo(Duration.ofSeconds(4));
|
||||
assertThat(timeoutConfig.searchTimeout()).isEqualTo(Duration.ofSeconds(5));
|
||||
assertThat(timeoutConfig.analyticsTimeout()).isEqualTo(Duration.ofSeconds(6));
|
||||
assertThat(timeoutConfig.managementTimeout()).isEqualTo(Duration.ofSeconds(7));
|
||||
}, "spring.couchbase.env.timeouts.connect=1s", "spring.couchbase.env.timeouts.disconnect=2s",
|
||||
"spring.couchbase.env.timeouts.key-value=500ms",
|
||||
"spring.couchbase.env.timeouts.key-value-durable=750ms", "spring.couchbase.env.timeouts.query=3s",
|
||||
"spring.couchbase.env.timeouts.view=4s", "spring.couchbase.env.timeouts.search=5s",
|
||||
"spring.couchbase.env.timeouts.analytics=6s", "spring.couchbase.env.timeouts.management=7s");
|
||||
}
|
||||
|
||||
@Test
|
||||
void enableSsl() {
|
||||
testClusterEnvironment((env) -> {
|
||||
SecurityConfig securityConfig = env.securityConfig();
|
||||
assertThat(securityConfig.tlsEnabled()).isTrue();
|
||||
assertThat(securityConfig.trustManagerFactory()).isNotNull();
|
||||
}, "spring.couchbase.env.ssl.enabled=true");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources("test.jks")
|
||||
void enableSslWithBundle() {
|
||||
testClusterEnvironment((env) -> {
|
||||
SecurityConfig securityConfig = env.securityConfig();
|
||||
assertThat(securityConfig.tlsEnabled()).isTrue();
|
||||
assertThat(securityConfig.trustManagerFactory()).isNotNull();
|
||||
}, "spring.ssl.bundle.jks.test-bundle.truststore.location=classpath:test.jks",
|
||||
"spring.ssl.bundle.jks.test-bundle.truststore.password=secret",
|
||||
"spring.couchbase.env.ssl.bundle=test-bundle");
|
||||
}
|
||||
|
||||
@Test
|
||||
void enableSslWithInvalidBundle() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.couchbase.connection-string=localhost",
|
||||
"spring.couchbase.env.ssl.bundle=test-bundle")
|
||||
.run((context) -> {
|
||||
assertThat(context).hasFailed();
|
||||
assertThat(context.getStartupFailure()).rootCause()
|
||||
.isInstanceOf(NoSuchSslBundleException.class)
|
||||
.hasMessageContaining("test-bundle");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void disableSslEvenWithBundle() {
|
||||
testClusterEnvironment((env) -> {
|
||||
SecurityConfig securityConfig = env.securityConfig();
|
||||
assertThat(securityConfig.tlsEnabled()).isFalse();
|
||||
assertThat(securityConfig.trustManagerFactory()).isNull();
|
||||
}, "spring.couchbase.env.ssl.enabled=false", "spring.couchbase.env.ssl.bundle=test-bundle");
|
||||
}
|
||||
|
||||
private void testClusterEnvironment(Consumer<ClusterEnvironment> environmentConsumer, String... environment) {
|
||||
this.contextRunner.withUserConfiguration(CouchbaseTestConfiguration.class)
|
||||
.withPropertyValues("spring.couchbase.connection-string=localhost")
|
||||
.withPropertyValues(environment)
|
||||
.run((context) -> environmentConsumer.accept(context.getBean(ClusterEnvironment.class)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void customizeEnvWithCustomCouchbaseConfiguration() {
|
||||
this.contextRunner
|
||||
.withUserConfiguration(CouchbaseTestConfiguration.class, ClusterEnvironmentCustomizerConfiguration.class)
|
||||
.withPropertyValues("spring.couchbase.connection-string=localhost",
|
||||
"spring.couchbase.env.timeouts.connect=100")
|
||||
.run((context) -> {
|
||||
assertThat(context).hasSingleBean(ClusterEnvironment.class);
|
||||
ClusterEnvironment env = context.getBean(ClusterEnvironment.class);
|
||||
assertThat(env.timeoutConfig().kvTimeout()).isEqualTo(Duration.ofSeconds(5));
|
||||
assertThat(env.timeoutConfig().connectTimeout()).isEqualTo(Duration.ofSeconds(2));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void passwordAuthenticationWithUsernameAndPassword() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.couchbase.connection-string=localhost", "spring.couchbase.username=user",
|
||||
"spring.couchbase.password=secret")
|
||||
.run((context) -> assertThat(context).hasSingleBean(PasswordAuthenticator.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources({ "key.crt", "key.pem" })
|
||||
void certificateAuthenticationWithPemPrivateKeyAndCertificate() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.couchbase.connection-string=localhost", "spring.couchbase.env.ssl.enabled=true",
|
||||
"spring.couchbase.authentication.pem.private-key=classpath:key.pem",
|
||||
"spring.couchbase.authentication.pem.certificates=classpath:key.crt")
|
||||
.run((context) -> assertThat(context).hasSingleBean(CertificateAuthenticator.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources("keystore.jks")
|
||||
void certificateAuthenticationWithJavaKeyStore() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.couchbase.connection-string=localhost", "spring.couchbase.env.ssl.enabled=true",
|
||||
"spring.couchbase.authentication.jks.location=classpath:keystore.jks",
|
||||
"spring.couchbase.authentication.jks.password=secret")
|
||||
.run((context) -> assertThat(context).hasSingleBean(CertificateAuthenticator.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void failsWithMissingAuthentication() {
|
||||
this.contextRunner.withPropertyValues("spring.couchbase.connection-string=localhost").run((context) -> {
|
||||
assertThat(context).hasFailed();
|
||||
assertThat(context).getFailure()
|
||||
.hasMessageContaining("Couchbase authentication requires username and password, or certificates");
|
||||
});
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@Bean
|
||||
ClusterEnvironmentBuilderCustomizer clusterEnvironmentBuilderCustomizer() {
|
||||
return (builder) -> builder.timeoutConfig()
|
||||
.kvTimeout(Duration.ofSeconds(5))
|
||||
.connectTimeout(Duration.ofSeconds(2));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.couchbase.autoconfigure;
|
||||
|
||||
import com.couchbase.client.core.env.IoConfig;
|
||||
import com.couchbase.client.core.env.TimeoutConfig;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.couchbase.autoconfigure.CouchbaseProperties.Io;
|
||||
import org.springframework.boot.couchbase.autoconfigure.CouchbaseProperties.Timeouts;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link CouchbaseProperties}.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
class CouchbasePropertiesTests {
|
||||
|
||||
@Test
|
||||
void ioHaveConsistentDefaults() {
|
||||
Io io = new CouchbaseProperties().getEnv().getIo();
|
||||
assertThat(io.getMinEndpoints()).isOne();
|
||||
assertThat(io.getMaxEndpoints()).isEqualTo(IoConfig.DEFAULT_MAX_HTTP_CONNECTIONS);
|
||||
assertThat(io.getIdleHttpConnectionTimeout()).isEqualTo(IoConfig.DEFAULT_IDLE_HTTP_CONNECTION_TIMEOUT);
|
||||
}
|
||||
|
||||
@Test
|
||||
void timeoutsHaveConsistentDefaults() {
|
||||
Timeouts timeouts = new CouchbaseProperties().getEnv().getTimeouts();
|
||||
assertThat(timeouts.getConnect()).isEqualTo(TimeoutConfig.DEFAULT_CONNECT_TIMEOUT);
|
||||
assertThat(timeouts.getDisconnect()).isEqualTo(TimeoutConfig.DEFAULT_DISCONNECT_TIMEOUT);
|
||||
assertThat(timeouts.getKeyValue()).isEqualTo(TimeoutConfig.DEFAULT_KV_TIMEOUT);
|
||||
assertThat(timeouts.getKeyValueDurable()).isEqualTo(TimeoutConfig.DEFAULT_KV_DURABLE_TIMEOUT);
|
||||
assertThat(timeouts.getQuery()).isEqualTo(TimeoutConfig.DEFAULT_QUERY_TIMEOUT);
|
||||
assertThat(timeouts.getView()).isEqualTo(TimeoutConfig.DEFAULT_VIEW_TIMEOUT);
|
||||
assertThat(timeouts.getSearch()).isEqualTo(TimeoutConfig.DEFAULT_SEARCH_TIMEOUT);
|
||||
assertThat(timeouts.getAnalytics()).isEqualTo(TimeoutConfig.DEFAULT_ANALYTICS_TIMEOUT);
|
||||
assertThat(timeouts.getManagement()).isEqualTo(TimeoutConfig.DEFAULT_MANAGEMENT_TIMEOUT);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.couchbase.autoconfigure;
|
||||
|
||||
import com.couchbase.client.core.env.Authenticator;
|
||||
import com.couchbase.client.java.Cluster;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Test configuration for couchbase that mocks access.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
class CouchbaseTestConfiguration {
|
||||
|
||||
private final Cluster cluster = mock(Cluster.class);
|
||||
|
||||
private final Authenticator authenticator = mock(Authenticator.class);
|
||||
|
||||
@Bean
|
||||
Cluster couchbaseCluster() {
|
||||
return this.cluster;
|
||||
}
|
||||
|
||||
@Bean
|
||||
Authenticator couchbaseAuth() {
|
||||
return this.authenticator;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDJjCCAhECFFjLlXVdTxDdLlCifzrA0dTHHJ2mMA0GCSqGSIb3DQEBCwUAME8x
|
||||
CzAJBgNVBAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAaBgNVBAoME0Rl
|
||||
ZmF1bHQgQ29tcGFueSBMdGQxCzAJBgNVBAMMAkNBMCAXDTIzMTAwNTA3Mjg1MFoY
|
||||
DzIxMjMwOTExMDcyODUwWjBRMQswCQYDVQQGEwJYWDEVMBMGA1UEBwwMRGVmYXVs
|
||||
dCBDaXR5MRwwGgYDVQQKDBNEZWZhdWx0IENvbXBhbnkgTHRkMQ0wCwYDVQQDDARr
|
||||
ZXkyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAspCMUdFGyKkgpMbW
|
||||
+UwSg4fdKM4qLSH7voTdsdVM9aAvLvYjBQ4gpORxDZNfUz67R0Ua0/oJt9jD49Wp
|
||||
qcq+tDOnp0dPtn2hFluV5PxM6d+MCSx/frPsfvyt9234okLL1zdLDNFYEbLhSPjA
|
||||
ku3vHw/OwlJOxCRwTkPqcElIV4+IvIbzAgSffyokzm/wKVKEhoT6NcfeU+6wCkTu
|
||||
al1X8loJ+27N6jN13oGZfH7EveBqgR8rPs55+54S/OcVG/uqL9ggOGRJiIZ3jUBk
|
||||
m5cN27wKkaNg/CQwa1UjcU4qshVpknHw1dpgJ2Gbs/yUphwpEZl/FTsZFcK1KCHD
|
||||
rOp3PQIDAQABMA0GCSqGSIb3DQEBCwUAA4H/AAFmEq86broBFxs0cpImaM884PBT
|
||||
bvJBSsFhsOg6mi4Gt01G/lPSj/ExNtH3G5bytCYAPaRxNx/dCs7uON3p86ta4zL8
|
||||
2PxgyhX1oY/GG63ETwn5s3GKpRaGTNVDWvPIM9RX6+bvX/wOg8eYXVaQlG5XYadC
|
||||
Ms9lWqHaM1C/iLGNmUTGcdbvhnmQDky2CwPNm+lXogSWbrsGpAmCkXJD1H+0Mx8I
|
||||
wjDVtGLBwr/8oXI8WbhvISMnS9+dd7+GLm6mU+14Kswi5I7EmBmREvkswi2IVJ6M
|
||||
GL7EY3qA6iqJWqsseYyLxiMr3nBT0SETphzoDanUQI1/jXQPrWIyjqvs
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCykIxR0UbIqSCk
|
||||
xtb5TBKDh90oziotIfu+hN2x1Uz1oC8u9iMFDiCk5HENk19TPrtHRRrT+gm32MPj
|
||||
1ampyr60M6enR0+2faEWW5Xk/Ezp34wJLH9+s+x+/K33bfiiQsvXN0sM0VgRsuFI
|
||||
+MCS7e8fD87CUk7EJHBOQ+pwSUhXj4i8hvMCBJ9/KiTOb/ApUoSGhPo1x95T7rAK
|
||||
RO5qXVfyWgn7bs3qM3XegZl8fsS94GqBHys+znn7nhL85xUb+6ov2CA4ZEmIhneN
|
||||
QGSblw3bvAqRo2D8JDBrVSNxTiqyFWmScfDV2mAnYZuz/JSmHCkRmX8VOxkVwrUo
|
||||
IcOs6nc9AgMBAAECggEAPN9dDolG1aIeYD3uzCa8Sv2WjdIWe7NRlEXMI9MgvL1i
|
||||
SGKdVpxV0ZCU37llLkY85tNujWP4SyXIxdMxVxIoR9syJKsBSCd0sl//bgP6nmHY
|
||||
Zco3HnTswu+VyLtDHuGhhtkxKwn0uXffKBaw44XcVhz38bPIaUI4zN2HPscks8BG
|
||||
j2MEl0N8P/TVrTkhgdjfoRi73VAisrEe+1wCg74BT7cmR8fEr7iNFrv955sdPGdw
|
||||
UTmx8U26++wbeYQs1ZE1713SYnRQuCUFs5GGjzOhNFi27zuhI6TafoVm9PO4j+ZC
|
||||
JUKTyUTBUsRMvm9z1IoHdjM8yInAv2g0J1bAeCTY+wKBgQDuMNMbNVoiXRKsSUry
|
||||
22T3W6HVLfLNKiYMNxsAkJjOiyyJcC+yg9BErn/haIHSafD2WmuWbW5ASViyl6fn
|
||||
D8qMluTwEaSrTgHXWI4ahWyapDShDQYp1s4dB75Aa/LVcFCay54YEtyCPzCPlj1K
|
||||
jz5OBV14NEVVA2cf59fIc/LXCwKBgQC/6m3TefUp5jnN/QUOx2OtZo8Y1pVrsuMB
|
||||
AuTtb21Khxn/86ZpVzySzg79/DkSNf9/sZhzj0IkviWNP5S8iAAaFC1q08CYhdCX
|
||||
d7tVnHlzpZmmoHUhG6dlJZayr1duZrURp2rP18+wIsKiFRImAyjc6yswVRpZgAiG
|
||||
gOkHCB231wKBgGlwXZMWy/6YOtLfYvkcm5ZQDtSCkY+2j78qiZ53Y91SiHWSntqk
|
||||
NQaiRGOw0n8lfJBhOG0PphV5InV0YtQLDnurtE59UOqwDmqYfddJpujRtaZxUIAm
|
||||
4XjCW7rCzm0jWdscNbCscMaLWGDHffxKaqc5AsZaRTK73eOmysOmaCI/AoGAf/yd
|
||||
RZ1dzJWHE0Kb7uE2LlvpLo1clLh1/ySo+1eGMV+sDS+2WSYedWEKSoO8o9JzE/ui
|
||||
Sd7OI6bTcEFotdqVBs9SAp45IP6Mv5bPziZOMLvNnnv/4RaKKkBJId0hl7TTKHTY
|
||||
HMg176ce2eznb4ZH6BzFbrQyoGFsThcGUPQurX0CgYBYtkDTp21TI1nuak7xpMIY
|
||||
BJQpqF5ahBf/+QYWtL0f3ca9MO2++zv5/XXitvt48cY1bCHNrVvSHgRzwSrOorZA
|
||||
5u7a5zyvfXjY3LY3k0VHddaVjU0mHsjx/1ux0wO2v8wQjOVZpT7XweB3WlUEGV7C
|
||||
5T/p+rmGg5Y5dTKUVCyvbQ==
|
||||
-----END PRIVATE KEY-----
|
||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user