diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/DeprecatedReactiveElasticsearchRestClientProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/DeprecatedReactiveElasticsearchRestClientProperties.java new file mode 100644 index 0000000000..cb879c531a --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/DeprecatedReactiveElasticsearchRestClientProperties.java @@ -0,0 +1,150 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.data.elasticsearch; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; +import org.springframework.util.unit.DataSize; + +/** + * Deprecated configuration properties for Elasticsearch Reactive REST clients. + * + * @author Brian Clozel + * @deprecated since 2.6.0 for removal in 2.8.0 + */ +@Deprecated +@ConfigurationProperties(prefix = "spring.data.elasticsearch.client.reactive") +class DeprecatedReactiveElasticsearchRestClientProperties { + + /** + * Comma-separated list of the Elasticsearch endpoints to connect to. + */ + private List endpoints = new ArrayList<>(Collections.singletonList("localhost:9200")); + + /** + * Whether the client should use SSL to connect to the endpoints. + */ + private boolean useSsl = false; + + /** + * Credentials username. + */ + private String username; + + /** + * Credentials password. + */ + private String password; + + /** + * Connection timeout. + */ + private Duration connectionTimeout; + + /** + * Read and Write Socket timeout. + */ + private Duration socketTimeout; + + /** + * Limit on the number of bytes that can be buffered whenever the input stream needs + * to be aggregated. + */ + private DataSize maxInMemorySize; + + private boolean customized = false; + + @DeprecatedConfigurationProperty(replacement = "spring.elasticsearch.uris") + public List getEndpoints() { + return this.endpoints; + } + + public void setEndpoints(List endpoints) { + this.customized = true; + this.endpoints = endpoints; + } + + @DeprecatedConfigurationProperty(reason = "Use of SSL should be indicated through an https URI scheme") + public boolean isUseSsl() { + return this.useSsl; + } + + public void setUseSsl(boolean useSsl) { + this.customized = true; + this.useSsl = useSsl; + } + + @DeprecatedConfigurationProperty(replacement = "spring.elasticsearch.username") + public String getUsername() { + return this.username; + } + + public void setUsername(String username) { + this.customized = true; + this.username = username; + } + + @DeprecatedConfigurationProperty(replacement = "spring.elasticsearch.password") + public String getPassword() { + return this.password; + } + + public void setPassword(String password) { + this.customized = true; + this.password = password; + } + + @DeprecatedConfigurationProperty(replacement = "spring.elasticsearch.connection-timeout") + public Duration getConnectionTimeout() { + return this.connectionTimeout; + } + + public void setConnectionTimeout(Duration connectionTimeout) { + this.customized = true; + this.connectionTimeout = connectionTimeout; + } + + @DeprecatedConfigurationProperty(replacement = "spring.elasticsearch.socket-timeout") + public Duration getSocketTimeout() { + return this.socketTimeout; + } + + public void setSocketTimeout(Duration socketTimeout) { + this.customized = true; + this.socketTimeout = socketTimeout; + } + + @DeprecatedConfigurationProperty(replacement = "spring.elasticsearch.webclient.max-in-memory-size") + public DataSize getMaxInMemorySize() { + return this.maxInMemorySize; + } + + public void setMaxInMemorySize(DataSize maxInMemorySize) { + this.customized = true; + this.maxInMemorySize = maxInMemorySize; + } + + boolean isCustomized() { + return this.customized; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRestClientAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRestClientAutoConfiguration.java index 3469c53347..753333c2c0 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRestClientAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRestClientAutoConfiguration.java @@ -16,11 +16,18 @@ package org.springframework.boot.autoconfigure.data.elasticsearch; +import java.net.URI; +import java.time.Duration; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + import reactor.netty.http.client.HttpClient; 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.elasticsearch.ElasticsearchProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.context.annotation.Bean; @@ -29,6 +36,7 @@ import org.springframework.data.elasticsearch.client.ClientConfiguration; import org.springframework.data.elasticsearch.client.reactive.ReactiveElasticsearchClient; import org.springframework.data.elasticsearch.client.reactive.ReactiveRestClients; import org.springframework.data.elasticsearch.client.reactive.ReactiveRestClients.WebClientConfigurationCallback; +import org.springframework.util.Assert; import org.springframework.util.unit.DataSize; import org.springframework.web.reactive.function.client.ExchangeStrategies; import org.springframework.web.reactive.function.client.WebClient; @@ -40,30 +48,39 @@ import org.springframework.web.reactive.function.client.WebClient; * @author Brian Clozel * @since 2.2.0 */ +@SuppressWarnings("deprecation") @Configuration(proxyBeanMethods = false) @ConditionalOnClass({ ReactiveRestClients.class, WebClient.class, HttpClient.class }) -@EnableConfigurationProperties(ReactiveElasticsearchRestClientProperties.class) +@EnableConfigurationProperties({ ElasticsearchProperties.class, ReactiveElasticsearchRestClientProperties.class, + DeprecatedReactiveElasticsearchRestClientProperties.class }) public class ReactiveElasticsearchRestClientAutoConfiguration { + private final ConsolidatedProperties properties; + + ReactiveElasticsearchRestClientAutoConfiguration(ElasticsearchProperties properties, + ReactiveElasticsearchRestClientProperties restClientProperties, + DeprecatedReactiveElasticsearchRestClientProperties reactiveProperties) { + this.properties = new ConsolidatedProperties(properties, restClientProperties, reactiveProperties); + } + @Bean @ConditionalOnMissingBean - public ClientConfiguration clientConfiguration(ReactiveElasticsearchRestClientProperties properties) { + public ClientConfiguration clientConfiguration() { ClientConfiguration.MaybeSecureClientConfigurationBuilder builder = ClientConfiguration.builder() - .connectedTo(properties.getEndpoints().toArray(new String[0])); + .connectedTo(this.properties.getEndpoints().toArray(new String[0])); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); - map.from(properties.isUseSsl()).whenTrue().toCall(builder::usingSsl); - map.from(properties.getUsername()).whenHasText() - .to((username) -> builder.withBasicAuth(username, properties.getPassword())); - map.from(properties.getConnectionTimeout()).to(builder::withConnectTimeout); - map.from(properties.getSocketTimeout()).to(builder::withSocketTimeout); - configureExchangeStrategies(map, builder, properties); + map.from(this.properties.isUseSsl()).whenTrue().toCall(builder::usingSsl); + map.from(this.properties.getCredentials()) + .to((credentials) -> builder.withBasicAuth(credentials.getUsername(), credentials.getPassword())); + map.from(this.properties.getConnectionTimeout()).to(builder::withConnectTimeout); + map.from(this.properties.getSocketTimeout()).to(builder::withSocketTimeout); + configureExchangeStrategies(map, builder); return builder.build(); } private void configureExchangeStrategies(PropertyMapper map, - ClientConfiguration.TerminalClientConfigurationBuilder builder, - ReactiveElasticsearchRestClientProperties properties) { - map.from(properties.getMaxInMemorySize()).asInt(DataSize::toBytes).to((maxInMemorySize) -> { + ClientConfiguration.TerminalClientConfigurationBuilder builder) { + map.from(this.properties.getMaxInMemorySize()).asInt(DataSize::toBytes).to((maxInMemorySize) -> { builder.withClientConfigurer(WebClientConfigurationCallback.from((webClient) -> { ExchangeStrategies exchangeStrategies = ExchangeStrategies.builder() .codecs((configurer) -> configurer.defaultCodecs().maxInMemorySize(maxInMemorySize)).build(); @@ -78,4 +95,166 @@ public class ReactiveElasticsearchRestClientAutoConfiguration { return ReactiveRestClients.create(clientConfiguration); } + private static final class ConsolidatedProperties { + + private final ElasticsearchProperties properties; + + private final ReactiveElasticsearchRestClientProperties restClientProperties; + + private final DeprecatedReactiveElasticsearchRestClientProperties deprecatedProperties; + + private final List uris; + + private ConsolidatedProperties(ElasticsearchProperties properties, + ReactiveElasticsearchRestClientProperties restClientProperties, + DeprecatedReactiveElasticsearchRestClientProperties deprecatedreactiveProperties) { + this.properties = properties; + this.restClientProperties = restClientProperties; + this.deprecatedProperties = deprecatedreactiveProperties; + this.uris = properties.getUris().stream().map((s) -> s.startsWith("http") ? s : "http://" + s) + .map(URI::create).collect(Collectors.toList()); + } + + private List getEndpoints() { + if (this.deprecatedProperties.isCustomized()) { + return this.deprecatedProperties.getEndpoints(); + } + return this.uris.stream().map((uri) -> uri.getHost() + ":" + uri.getPort()).collect(Collectors.toList()); + } + + private Credentials getCredentials() { + if (this.deprecatedProperties.isCustomized()) { + return Credentials.from(this.deprecatedProperties); + } + Credentials propertyCredentials = Credentials.from(this.properties); + Credentials uriCredentials = Credentials.from(this.properties.getUris()); + if (uriCredentials == null) { + return propertyCredentials; + } + if (propertyCredentials != null && !uriCredentials.equals(propertyCredentials)) { + throw new IllegalArgumentException( + "Credentials from URI user info do not match those from spring.elasticsearch.username and " + + "spring.elasticsearch.password"); + } + return uriCredentials; + + } + + private Duration getConnectionTimeout() { + return this.deprecatedProperties.isCustomized() ? this.deprecatedProperties.getConnectionTimeout() + : this.properties.getConnectionTimeout(); + } + + private Duration getSocketTimeout() { + return this.deprecatedProperties.isCustomized() ? this.deprecatedProperties.getSocketTimeout() + : this.properties.getSocketTimeout(); + } + + private boolean isUseSsl() { + if (this.deprecatedProperties.isCustomized()) { + return this.deprecatedProperties.isUseSsl(); + } + Set schemes = this.uris.stream().map((uri) -> uri.getScheme()).collect(Collectors.toSet()); + Assert.isTrue(schemes.size() == 1, () -> "Configured Elasticsearch URIs have varying schemes"); + return schemes.iterator().next().equals("https"); + } + + private DataSize getMaxInMemorySize() { + return this.deprecatedProperties.isCustomized() ? this.deprecatedProperties.getMaxInMemorySize() + : this.restClientProperties.getMaxInMemorySize(); + } + + private static final class Credentials { + + private final String username; + + private final String password; + + private Credentials(String username, String password) { + this.username = username; + this.password = password; + } + + private String getUsername() { + return this.username; + } + + private String getPassword() { + return this.password; + } + + private static Credentials from(List uris) { + Set userInfos = uris.stream().map(URI::create).map((uri) -> uri.getUserInfo()) + .collect(Collectors.toSet()); + Assert.isTrue(userInfos.size() == 1, () -> "Configured Elasticsearch URIs have varying user infos"); + String userInfo = userInfos.iterator().next(); + if (userInfo != null) { + String[] parts = userInfo.split(":"); + return new Credentials(parts[0], (parts.length == 2) ? parts[1] : ""); + } + return null; + } + + private static Credentials from(ElasticsearchProperties properties) { + String username = properties.getUsername(); + String password = properties.getPassword(); + if (username == null && password == null) { + return null; + } + return new Credentials(username, password); + } + + private static Credentials from(DeprecatedReactiveElasticsearchRestClientProperties properties) { + String username = properties.getUsername(); + String password = properties.getPassword(); + if (username == null && password == null) { + return null; + } + return new Credentials(username, password); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Credentials other = (Credentials) obj; + if (this.password == null) { + if (other.password != null) { + return false; + } + } + else if (!this.password.equals(other.password)) { + return false; + } + if (this.username == null) { + if (other.username != null) { + return false; + } + } + else if (!this.username.equals(other.username)) { + return false; + } + return true; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((this.password == null) ? 0 : this.password.hashCode()); + result = prime * result + ((this.username == null) ? 0 : this.username.hashCode()); + return result; + } + + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRestClientProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRestClientProperties.java index a4a6de83c1..1673f1c2f8 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRestClientProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRestClientProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,6 @@ package org.springframework.boot.autoconfigure.data.elasticsearch; -import java.time.Duration; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.util.unit.DataSize; @@ -30,93 +25,15 @@ import org.springframework.util.unit.DataSize; * @author Brian Clozel * @since 2.2.0 */ -@ConfigurationProperties(prefix = "spring.data.elasticsearch.client.reactive") +@ConfigurationProperties(prefix = "spring.elasticsearch.webclient") public class ReactiveElasticsearchRestClientProperties { - /** - * Comma-separated list of the Elasticsearch endpoints to connect to. - */ - private List endpoints = new ArrayList<>(Collections.singletonList("localhost:9200")); - - /** - * Whether the client should use SSL to connect to the endpoints. - */ - private boolean useSsl = false; - - /** - * Credentials username. - */ - private String username; - - /** - * Credentials password. - */ - private String password; - - /** - * Connection timeout. - */ - private Duration connectionTimeout; - - /** - * Read and Write Socket timeout. - */ - private Duration socketTimeout; - /** * Limit on the number of bytes that can be buffered whenever the input stream needs * to be aggregated. */ private DataSize maxInMemorySize; - public List getEndpoints() { - return this.endpoints; - } - - public void setEndpoints(List endpoints) { - this.endpoints = endpoints; - } - - public boolean isUseSsl() { - return this.useSsl; - } - - public void setUseSsl(boolean useSsl) { - this.useSsl = useSsl; - } - - 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 Duration getConnectionTimeout() { - return this.connectionTimeout; - } - - public void setConnectionTimeout(Duration connectionTimeout) { - this.connectionTimeout = connectionTimeout; - } - - public Duration getSocketTimeout() { - return this.socketTimeout; - } - - public void setSocketTimeout(Duration socketTimeout) { - this.socketTimeout = socketTimeout; - } - public DataSize getMaxInMemorySize() { return this.maxInMemorySize; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/DeprecatedElasticsearchRestClientProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/DeprecatedElasticsearchRestClientProperties.java new file mode 100644 index 0000000000..373bc00c60 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/DeprecatedElasticsearchRestClientProperties.java @@ -0,0 +1,159 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.elasticsearch; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; + +/** + * Deprecated configuration properties for Elasticsearch REST clients. + * + * @author Brian Clozel + * @deprecated since 2.6.0 for removal in 2.8.0. + */ +@ConfigurationProperties(prefix = "spring.elasticsearch.rest") +@Deprecated +class DeprecatedElasticsearchRestClientProperties { + + /** + * Comma-separated list of the Elasticsearch instances to use. + */ + private List uris = new ArrayList<>(Collections.singletonList("http://localhost:9200")); + + /** + * Credentials username. + */ + private String username; + + /** + * Credentials password. + */ + private String password; + + /** + * Connection timeout. + */ + private Duration connectionTimeout = Duration.ofSeconds(1); + + /** + * Read timeout. + */ + private Duration readTimeout = Duration.ofSeconds(30); + + private final Sniffer sniffer = new Sniffer(); + + private boolean customized = false; + + @DeprecatedConfigurationProperty(replacement = "spring.elasticsearch.uris") + public List getUris() { + return this.uris; + } + + public void setUris(List uris) { + this.customized = true; + this.uris = uris; + } + + @DeprecatedConfigurationProperty(replacement = "spring.elasticsearch.username") + public String getUsername() { + return this.username; + } + + public void setUsername(String username) { + this.customized = true; + this.username = username; + } + + @DeprecatedConfigurationProperty(replacement = "spring.elasticsearch.password") + public String getPassword() { + return this.password; + } + + public void setPassword(String password) { + this.customized = true; + this.password = password; + } + + @DeprecatedConfigurationProperty(replacement = "spring.elasticsearch.connection-timeout") + public Duration getConnectionTimeout() { + return this.connectionTimeout; + } + + public void setConnectionTimeout(Duration connectionTimeout) { + this.customized = true; + this.connectionTimeout = connectionTimeout; + } + + @DeprecatedConfigurationProperty(replacement = "spring.elasticsearch.socket-timeout") + public Duration getReadTimeout() { + return this.readTimeout; + } + + public void setReadTimeout(Duration readTimeout) { + this.customized = true; + this.readTimeout = readTimeout; + } + + boolean isCustomized() { + return this.customized; + } + + public Sniffer getSniffer() { + return this.sniffer; + } + + @Deprecated + class Sniffer { + + /** + * Interval between consecutive ordinary sniff executions. + */ + private Duration interval = Duration.ofMinutes(5); + + /** + * Delay of a sniff execution scheduled after a failure. + */ + private Duration delayAfterFailure = Duration.ofMinutes(1); + + @DeprecatedConfigurationProperty(replacement = "spring.elasticsearch.restclient.sniffer.interval") + public Duration getInterval() { + return this.interval; + } + + public void setInterval(Duration interval) { + DeprecatedElasticsearchRestClientProperties.this.customized = true; + this.interval = interval; + } + + @DeprecatedConfigurationProperty(replacement = "spring.elasticsearch.restclient.sniffer.delay-after-failure") + public Duration getDelayAfterFailure() { + return this.delayAfterFailure; + } + + public void setDelayAfterFailure(Duration delayAfterFailure) { + DeprecatedElasticsearchRestClientProperties.this.customized = true; + this.delayAfterFailure = delayAfterFailure; + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchProperties.java new file mode 100644 index 0000000000..738a793629 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchProperties.java @@ -0,0 +1,100 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.elasticsearch; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration properties for Elasticsearch. + * + * @author Andy Wilkinson + * @since 2.4.0 + */ +@ConfigurationProperties("spring.elasticsearch") +public class ElasticsearchProperties { + + /** + * Comma-separated list of the Elasticsearch instances to use. + */ + private List uris = new ArrayList<>(Collections.singletonList("http://localhost:9200")); + + /** + * Username for authentication with Elasticsearch. + */ + private String username; + + /** + * Password for authentication with Elasticsearch. + */ + private String password; + + /** + * Connection timeout used when communicating with Elasticsearch. + */ + private Duration connectionTimeout = Duration.ofSeconds(1); + + /** + * Socket timeout used when communicating with Elasticsearch. + */ + private Duration socketTimeout = Duration.ofSeconds(30); + + public List getUris() { + return this.uris; + } + + public void setUris(List uris) { + this.uris = uris; + } + + 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 Duration getConnectionTimeout() { + return this.connectionTimeout; + } + + public void setConnectionTimeout(Duration connectionTimeout) { + this.connectionTimeout = connectionTimeout; + } + + public Duration getSocketTimeout() { + return this.socketTimeout; + } + + public void setSocketTimeout(Duration socketTimeout) { + this.socketTimeout = socketTimeout; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfiguration.java index 9894fd5890..e96e6cc5b0 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,10 +36,12 @@ import org.springframework.context.annotation.Import; * @author Stephane Nicoll * @since 2.1.0 */ +@SuppressWarnings("deprecation") @Configuration(proxyBeanMethods = false) @ConditionalOnClass(RestHighLevelClient.class) @ConditionalOnMissingBean(RestClient.class) -@EnableConfigurationProperties(ElasticsearchRestClientProperties.class) +@EnableConfigurationProperties({ ElasticsearchProperties.class, ElasticsearchRestClientProperties.class, + DeprecatedElasticsearchRestClientProperties.class }) @Import({ RestClientBuilderConfiguration.class, RestHighLevelClientConfiguration.class, RestClientSnifferConfiguration.class }) public class ElasticsearchRestClientAutoConfiguration { 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 33faaa3ff0..e84a5c9cca 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 @@ -19,6 +19,7 @@ package org.springframework.boot.autoconfigure.elasticsearch; import java.net.URI; import java.net.URISyntaxException; import java.time.Duration; +import java.util.List; import org.apache.http.HttpHost; import org.apache.http.auth.AuthScope; @@ -53,15 +54,23 @@ class ElasticsearchRestClientConfigurations { @ConditionalOnMissingBean(RestClientBuilder.class) static class RestClientBuilderConfiguration { + private final ConsolidatedProperties properties; + + @SuppressWarnings("deprecation") + RestClientBuilderConfiguration(ElasticsearchProperties properties, + DeprecatedElasticsearchRestClientProperties deprecatedProperties) { + this.properties = new ConsolidatedProperties(properties, deprecatedProperties); + } + @Bean - RestClientBuilderCustomizer defaultRestClientBuilderCustomizer(ElasticsearchRestClientProperties properties) { - return new DefaultRestClientBuilderCustomizer(properties); + RestClientBuilderCustomizer defaultRestClientBuilderCustomizer() { + return new DefaultRestClientBuilderCustomizer(this.properties); } @Bean - RestClientBuilder elasticsearchRestClientBuilder(ElasticsearchRestClientProperties properties, + RestClientBuilder elasticsearchRestClientBuilder( ObjectProvider builderCustomizers) { - HttpHost[] hosts = properties.getUris().stream().map(this::createHttpHost).toArray(HttpHost[]::new); + HttpHost[] hosts = this.properties.getUris().stream().map(this::createHttpHost).toArray(HttpHost[]::new); RestClientBuilder builder = RestClient.builder(hosts); builder.setHttpClientConfigCallback((httpClientBuilder) -> { builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(httpClientBuilder)); @@ -117,13 +126,18 @@ class ElasticsearchRestClientConfigurations { @Bean @ConditionalOnMissingBean - Sniffer elasticsearchSniffer(RestHighLevelClient client, ElasticsearchRestClientProperties properties) { + @SuppressWarnings("deprecation") + Sniffer elasticsearchSniffer(RestHighLevelClient client, ElasticsearchRestClientProperties properties, + DeprecatedElasticsearchRestClientProperties deprecatedProperties) { SnifferBuilder builder = Sniffer.builder(client.getLowLevelClient()); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); - map.from(properties.getSniffer().getInterval()).asInt(Duration::toMillis) - .to(builder::setSniffIntervalMillis); - map.from(properties.getSniffer().getDelayAfterFailure()).asInt(Duration::toMillis) - .to(builder::setSniffAfterFailureDelayMillis); + Duration interval = deprecatedProperties.isCustomized() ? deprecatedProperties.getSniffer().getInterval() + : properties.getSniffer().getInterval(); + map.from(interval).asInt(Duration::toMillis).to(builder::setSniffIntervalMillis); + Duration delayAfterFailure = deprecatedProperties.isCustomized() + ? deprecatedProperties.getSniffer().getDelayAfterFailure() + : properties.getSniffer().getDelayAfterFailure(); + map.from(delayAfterFailure).asInt(Duration::toMillis).to(builder::setSniffAfterFailureDelayMillis); return builder.build(); } @@ -133,9 +147,9 @@ class ElasticsearchRestClientConfigurations { private static final PropertyMapper map = PropertyMapper.get(); - private final ElasticsearchRestClientProperties properties; + private final ConsolidatedProperties properties; - DefaultRestClientBuilderCustomizer(ElasticsearchRestClientProperties properties) { + DefaultRestClientBuilderCustomizer(ConsolidatedProperties properties) { this.properties = properties; } @@ -152,7 +166,7 @@ class ElasticsearchRestClientConfigurations { public void customize(RequestConfig.Builder builder) { map.from(this.properties::getConnectionTimeout).whenNonNull().asInt(Duration::toMillis) .to(builder::setConnectTimeout); - map.from(this.properties::getReadTimeout).whenNonNull().asInt(Duration::toMillis) + map.from(this.properties::getSocketTimeout).whenNonNull().asInt(Duration::toMillis) .to(builder::setSocketTimeout); } @@ -160,7 +174,7 @@ class ElasticsearchRestClientConfigurations { private static class PropertiesCredentialsProvider extends BasicCredentialsProvider { - PropertiesCredentialsProvider(ElasticsearchRestClientProperties properties) { + PropertiesCredentialsProvider(ConsolidatedProperties properties) { if (StringUtils.hasText(properties.getUsername())) { Credentials credentials = new UsernamePasswordCredentials(properties.getUsername(), properties.getPassword()); @@ -201,4 +215,44 @@ class ElasticsearchRestClientConfigurations { } + @SuppressWarnings("deprecation") + private static final class ConsolidatedProperties { + + private final ElasticsearchProperties properties; + + private final DeprecatedElasticsearchRestClientProperties deprecatedProperties; + + private ConsolidatedProperties(ElasticsearchProperties properties, + DeprecatedElasticsearchRestClientProperties deprecatedProperties) { + this.properties = properties; + this.deprecatedProperties = deprecatedProperties; + } + + private List getUris() { + return this.deprecatedProperties.isCustomized() ? this.deprecatedProperties.getUris() + : this.properties.getUris(); + } + + private String getUsername() { + return this.deprecatedProperties.isCustomized() ? this.deprecatedProperties.getUsername() + : this.properties.getUsername(); + } + + private String getPassword() { + return this.deprecatedProperties.isCustomized() ? this.deprecatedProperties.getPassword() + : this.properties.getPassword(); + } + + private Duration getConnectionTimeout() { + return this.deprecatedProperties.isCustomized() ? this.deprecatedProperties.getConnectionTimeout() + : this.properties.getConnectionTimeout(); + } + + private Duration getSocketTimeout() { + return this.deprecatedProperties.isCustomized() ? this.deprecatedProperties.getReadTimeout() + : this.properties.getSocketTimeout(); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientProperties.java index 649246d42b..322c234460 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,88 +17,24 @@ package org.springframework.boot.autoconfigure.elasticsearch; import java.time.Duration; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; + +import org.elasticsearch.client.RestClient; +import org.elasticsearch.client.RestHighLevelClient; import org.springframework.boot.context.properties.ConfigurationProperties; /** - * Configuration properties for Elasticsearch REST clients. + * Configuration properties specific to Elasticsearch's {@link RestClient} and + * {@link RestHighLevelClient}. * * @author Brian Clozel * @since 2.1.0 */ -@ConfigurationProperties(prefix = "spring.elasticsearch.rest") +@ConfigurationProperties(prefix = "spring.elasticsearch.restclient") public class ElasticsearchRestClientProperties { - /** - * Comma-separated list of the Elasticsearch instances to use. - */ - private List uris = new ArrayList<>(Collections.singletonList("http://localhost:9200")); - - /** - * Credentials username. - */ - private String username; - - /** - * Credentials password. - */ - private String password; - - /** - * Connection timeout. - */ - private Duration connectionTimeout = Duration.ofSeconds(1); - - /** - * Read timeout. - */ - private Duration readTimeout = Duration.ofSeconds(30); - private final Sniffer sniffer = new Sniffer(); - public List getUris() { - return this.uris; - } - - public void setUris(List uris) { - this.uris = uris; - } - - 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 Duration getConnectionTimeout() { - return this.connectionTimeout; - } - - public void setConnectionTimeout(Duration connectionTimeout) { - this.connectionTimeout = connectionTimeout; - } - - public Duration getReadTimeout() { - return this.readTimeout; - } - - public void setReadTimeout(Duration readTimeout) { - this.readTimeout = readTimeout; - } - public Sniffer getSniffer() { return this.sniffer; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 74146977bd..cd89cd21d0 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -860,6 +860,12 @@ "http://localhost:9200" ] }, + { + "name": "spring.elasticsearch.uris", + "defaultValue": [ + "http://localhost:9200" + ] + }, { "name": "spring.flyway.connect-retries-interval", "defaultValue": 120 diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRestClientAutoConfigurationIntegrationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRestClientAutoConfigurationIntegrationTests.java index 55a09707a4..6a651b85a5 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRestClientAutoConfigurationIntegrationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRestClientAutoConfigurationIntegrationTests.java @@ -52,11 +52,10 @@ class ReactiveElasticsearchRestClientAutoConfigurationIntegrationTests { @Test void restClientCanQueryElasticsearchNode() { - this.contextRunner.withPropertyValues( - "spring.data.elasticsearch.client.reactive.endpoints=" + elasticsearch.getHost() + ":" - + elasticsearch.getFirstMappedPort(), - "spring.data.elasticsearch.client.reactive.connection-timeout=120s", - "spring.data.elasticsearch.client.reactive.socket-timeout=120s").run((context) -> { + this.contextRunner + .withPropertyValues("spring.elasticsearch.uris=" + elasticsearch.getHttpHostAddress(), + "spring.elasticsearch.connection-timeout=120s", "spring.elasticsearch.socket-timeout=120s") + .run((context) -> { ReactiveElasticsearchClient client = context.getBean(ReactiveElasticsearchClient.class); Map source = new HashMap<>(); source.put("a", "alpha"); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRestClientAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRestClientAutoConfigurationTests.java index 2da45b8a9c..f6180bafee 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRestClientAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRestClientAutoConfigurationTests.java @@ -16,12 +16,20 @@ package org.springframework.boot.autoconfigure.data.elasticsearch; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; import java.time.Duration; +import java.util.Base64; import java.util.List; import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -50,8 +58,14 @@ class ReactiveElasticsearchRestClientAutoConfigurationTests { @Test void configureShouldCreateDefaultBeans() { - this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ClientConfiguration.class) - .hasSingleBean(ReactiveElasticsearchClient.class)); + this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(ClientConfiguration.class) + .hasSingleBean(ReactiveElasticsearchClient.class); + List endpoints = context.getBean(ClientConfiguration.class).getEndpoints(); + assertThat(endpoints).hasSize(1); + assertThat(endpoints.get(0).getHostString()).isEqualTo("localhost"); + assertThat(endpoints.get(0).getPort()).isEqualTo(9200); + }); } @Test @@ -68,6 +82,7 @@ class ReactiveElasticsearchRestClientAutoConfigurationTests { } @Test + @Deprecated void whenEndpointIsCustomizedThenClientConfigurationHasCustomEndpoint() { this.contextRunner.withPropertyValues("spring.data.elasticsearch.client.reactive.endpoints=localhost:9876") .run((context) -> { @@ -79,6 +94,7 @@ class ReactiveElasticsearchRestClientAutoConfigurationTests { } @Test + @Deprecated void whenMultipleEndpointsAreConfiguredThenClientConfigurationHasMultipleEndpoints() { this.contextRunner .withPropertyValues("spring.data.elasticsearch.client.reactive.endpoints=localhost:9876,localhost:8765") @@ -93,6 +109,122 @@ class ReactiveElasticsearchRestClientAutoConfigurationTests { } @Test + void whenUriIsCustomizedThenClientConfigurationHasCustomEndpoint() { + this.contextRunner.withPropertyValues("spring.elasticsearch.uris=http://localhost:9876").run((context) -> { + List endpoints = context.getBean(ClientConfiguration.class).getEndpoints(); + assertThat(endpoints).hasSize(1); + assertThat(endpoints.get(0).getHostString()).isEqualTo("localhost"); + assertThat(endpoints.get(0).getPort()).isEqualTo(9876); + }); + } + + @Test + void whenUriHasHttpsSchemeThenClientConfigurationUsesSsl() { + this.contextRunner.withPropertyValues("spring.elasticsearch.uris=https://localhost:9876").run((context) -> { + ClientConfiguration clientConfiguration = context.getBean(ClientConfiguration.class); + List endpoints = clientConfiguration.getEndpoints(); + assertThat(endpoints).hasSize(1); + assertThat(endpoints.get(0).getHostString()).isEqualTo("localhost"); + assertThat(endpoints.get(0).getPort()).isEqualTo(9876); + assertThat(clientConfiguration.useSsl()).isTrue(); + }); + } + + @Test + void whenMultipleUrisAreConfiguredThenClientConfigurationHasMultipleEndpoints() { + this.contextRunner.withPropertyValues("spring.elasticsearch.uris=http://localhost:9876,http://localhost:8765") + .run((context) -> { + List endpoints = context.getBean(ClientConfiguration.class).getEndpoints(); + assertThat(endpoints).hasSize(2); + assertThat(endpoints.get(0).getHostString()).isEqualTo("localhost"); + assertThat(endpoints.get(0).getPort()).isEqualTo(9876); + assertThat(endpoints.get(1).getHostString()).isEqualTo("localhost"); + assertThat(endpoints.get(1).getPort()).isEqualTo(8765); + }); + } + + @Test + void whenMultipleUrisHaveHttpsSchemeThenClientConfigurationUsesSsl() { + this.contextRunner.withPropertyValues("spring.elasticsearch.uris=https://localhost:9876,https://localhost:8765") + .run((context) -> { + ClientConfiguration clientConfiguration = context.getBean(ClientConfiguration.class); + List endpoints = clientConfiguration.getEndpoints(); + assertThat(endpoints).hasSize(2); + assertThat(endpoints.get(0).getHostString()).isEqualTo("localhost"); + assertThat(endpoints.get(0).getPort()).isEqualTo(9876); + assertThat(endpoints.get(1).getHostString()).isEqualTo("localhost"); + assertThat(endpoints.get(1).getPort()).isEqualTo(8765); + assertThat(clientConfiguration.useSsl()).isTrue(); + }); + } + + @Test + void whenMultipleUrisHaveVaryingSchemesThenRunFails() { + this.contextRunner.withPropertyValues("spring.elasticsearch.uris=https://localhost:9876,http://localhost:8765") + .run((context) -> { + assertThat(context).hasFailed(); + assertThat(context).getFailure().hasRootCauseInstanceOf(IllegalArgumentException.class) + .hasRootCauseMessage("Configured Elasticsearch URIs have varying schemes"); + }); + } + + @Test + void whenUriHasUsernameOnlyThenDefaultAuthorizationHeaderHasUsernameAndEmptyPassword() { + this.contextRunner.withPropertyValues("spring.elasticsearch.uris=http://user@localhost:9200").run((context) -> { + ClientConfiguration clientConfiguration = context.getBean(ClientConfiguration.class); + assertThat(clientConfiguration.getDefaultHeaders().get(HttpHeaders.AUTHORIZATION)).containsExactly( + "Basic " + Base64.getEncoder().encodeToString("user:".getBytes(StandardCharsets.UTF_8))); + }); + } + + @Test + void whenUriHasUsernameAndPasswordThenDefaultAuthorizationHeaderHasUsernameAndPassword() { + this.contextRunner.withPropertyValues("spring.elasticsearch.uris=http://user:secret@localhost:9200") + .run((context) -> { + ClientConfiguration clientConfiguration = context.getBean(ClientConfiguration.class); + assertThat(clientConfiguration.getDefaultHeaders().get(HttpHeaders.AUTHORIZATION)) + .containsExactly("Basic " + Base64.getEncoder() + .encodeToString("user:secret".getBytes(StandardCharsets.UTF_8))); + }); + } + + @Test + void whenMultipleUrisHaveVaryingUserInfosThenRunFails() { + this.contextRunner + .withPropertyValues("spring.elasticsearch.uris=http://user:secret@localhost:9876,http://localhost:8765") + .run((context) -> { + assertThat(context).hasFailed(); + assertThat(context).getFailure().hasRootCauseInstanceOf(IllegalArgumentException.class) + .hasRootCauseMessage("Configured Elasticsearch URIs have varying user infos"); + }); + } + + @Test + void whenUriUserInfoMatchesUsernameAndPasswordPropertiesThenDefaultAuthorizationHeaderIsConfigured() { + this.contextRunner.withPropertyValues("spring.elasticsearch.uris=http://user:secret@localhost:9876", + "spring.elasticsearch.username=user", "spring.elasticsearch.password=secret").run((context) -> { + ClientConfiguration clientConfiguration = context.getBean(ClientConfiguration.class); + assertThat(clientConfiguration.getDefaultHeaders().get(HttpHeaders.AUTHORIZATION)) + .containsExactly("Basic " + Base64.getEncoder() + .encodeToString("user:secret".getBytes(StandardCharsets.UTF_8))); + }); + } + + @Test + void whenUriUserInfoAndUsernameAndPasswordPropertiesDoNotMatchThenRunFails() { + this.contextRunner + .withPropertyValues("spring.elasticsearch.uris=http://user:secret@localhost:9876", + "spring.elasticsearch.username=alice", "spring.elasticsearch.password=confidential") + .run((context) -> { + assertThat(context).hasFailed(); + assertThat(context).getFailure().hasRootCauseInstanceOf(IllegalArgumentException.class) + .hasRootCauseMessage("Credentials from URI user info do not match those from " + + "spring.elasticsearch.username and spring.elasticsearch.password"); + }); + } + + @Test + @Deprecated void whenConfiguredToUseSslThenClientConfigurationUsesSsl() { this.contextRunner.withPropertyValues("spring.data.elasticsearch.client.reactive.use-ssl=true") .run((context) -> assertThat(context.getBean(ClientConfiguration.class).useSsl()).isTrue()); @@ -101,50 +233,46 @@ class ReactiveElasticsearchRestClientAutoConfigurationTests { @Test void whenSocketTimeoutIsNotConfiguredThenClientConfigurationUsesDefault() { this.contextRunner.run((context) -> assertThat(context.getBean(ClientConfiguration.class).getSocketTimeout()) - .isEqualTo(Duration.ofSeconds(5))); + .isEqualTo(Duration.ofSeconds(30))); } @Test void whenConnectionTimeoutIsNotConfiguredThenClientConfigurationUsesDefault() { this.contextRunner.run((context) -> assertThat(context.getBean(ClientConfiguration.class).getConnectTimeout()) - .isEqualTo(Duration.ofSeconds(10))); + .isEqualTo(Duration.ofSeconds(1))); } - @Test - void whenSocketTimeoutIsConfiguredThenClientConfigurationHasCustomSocketTimeout() { - this.contextRunner.withPropertyValues("spring.data.elasticsearch.client.reactive.socket-timeout=2s") + @ParameterizedPropertyPrefixTest + void whenSocketTimeoutIsConfiguredThenClientConfigurationHasCustomSocketTimeout(String prefix) { + this.contextRunner.withPropertyValues(prefix + "socket-timeout=2s") .run((context) -> assertThat(context.getBean(ClientConfiguration.class).getSocketTimeout()) .isEqualTo(Duration.ofSeconds(2))); } - @Test - void whenConnectionTimeoutIsConfiguredThenClientConfigurationHasCustomConnectTimeout() { - this.contextRunner.withPropertyValues("spring.data.elasticsearch.client.reactive.connection-timeout=2s") + @ParameterizedPropertyPrefixTest + void whenConnectionTimeoutIsConfiguredThenClientConfigurationHasCustomConnectTimeout(String prefix) { + this.contextRunner.withPropertyValues(prefix + "connection-timeout=2s") .run((context) -> assertThat(context.getBean(ClientConfiguration.class).getConnectTimeout()) .isEqualTo(Duration.ofSeconds(2))); } - @Test - void whenCredentialsAreConfiguredThenClientConfigurationHasDefaultAuthorizationHeader() { - this.contextRunner - .withPropertyValues("spring.data.elasticsearch.client.reactive.username=alice", - "spring.data.elasticsearch.client.reactive.password=secret") + @ParameterizedPropertyPrefixTest + void whenCredentialsAreConfiguredThenClientConfigurationHasDefaultAuthorizationHeader(String prefix) { + this.contextRunner.withPropertyValues(prefix + "username=alice", prefix + "password=secret") .run((context) -> assertThat( context.getBean(ClientConfiguration.class).getDefaultHeaders().get(HttpHeaders.AUTHORIZATION)) .containsExactly("Basic YWxpY2U6c2VjcmV0")); } - @Test - void whenMaxInMemorySizeIsConfiguredThenUnderlyingWebClientHasCustomMaxInMemorySize() { - this.contextRunner.withPropertyValues("spring.data.elasticsearch.client.reactive.max-in-memory-size=1MB") - .run((context) -> { - WebClient client = configureWebClient( - context.getBean(ClientConfiguration.class).getClientConfigurers()); - assertThat(client).extracting("exchangeFunction").extracting("strategies") - .extracting("codecConfigurer").extracting("defaultCodecs") - .asInstanceOf(InstanceOfAssertFactories.type(DefaultCodecConfig.class)) - .extracting(DefaultCodecConfig::maxInMemorySize).isEqualTo(1024 * 1024); - }); + @ParameterizedTest + @ValueSource(strings = { "spring.elasticsearch.webclient.", "spring.data.elasticsearch.client.reactive." }) + void whenMaxInMemorySizeIsConfiguredThenUnderlyingWebClientHasCustomMaxInMemorySize(String prefix) { + this.contextRunner.withPropertyValues(prefix + "max-in-memory-size=1MB").run((context) -> { + WebClient client = configureWebClient(context.getBean(ClientConfiguration.class).getClientConfigurers()); + assertThat(client).extracting("exchangeFunction").extracting("strategies").extracting("codecConfigurer") + .extracting("defaultCodecs").asInstanceOf(InstanceOfAssertFactories.type(DefaultCodecConfig.class)) + .extracting(DefaultCodecConfig::maxInMemorySize).isEqualTo(1024 * 1024); + }); } private WebClient configureWebClient(List> callbacks) { @@ -175,4 +303,12 @@ class ReactiveElasticsearchRestClientAutoConfigurationTests { } + @ParameterizedTest + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + @ValueSource(strings = { "spring.data.elasticsearch.client.reactive.", "spring.elasticsearch." }) + static @interface ParameterizedPropertyPrefixTest { + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfigurationIntegrationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfigurationIntegrationTests.java index 6165778f71..49dbdb7e28 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfigurationIntegrationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfigurationIntegrationTests.java @@ -55,7 +55,8 @@ class ElasticsearchRestClientAutoConfigurationIntegrationTests { @Test void restClientCanQueryElasticsearchNode() { this.contextRunner - .withPropertyValues("spring.elasticsearch.rest.uris=http://" + elasticsearch.getHttpHostAddress()) + .withPropertyValues("spring.elasticsearch.uris=" + elasticsearch.getHttpHostAddress(), + "spring.elasticsearch.connection-timeout=120s", "spring.elasticsearch.socket-timeout=120s") .run((context) -> { RestHighLevelClient client = context.getBean(RestHighLevelClient.class); Map source = new HashMap<>(); 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 bb7b457beb..fac35367e0 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 @@ -16,6 +16,10 @@ package org.springframework.boot.autoconfigure.elasticsearch; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.time.Duration; import java.util.Map; @@ -32,6 +36,8 @@ import org.elasticsearch.client.RestClientBuilder; import org.elasticsearch.client.RestHighLevelClient; import org.elasticsearch.client.sniff.Sniffer; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.FilteredClassLoader; @@ -105,7 +111,7 @@ class ElasticsearchRestClientAutoConfigurationTests { } @Test - void configureWithCustomTimeouts() { + void configureWithLegacyCustomTimeouts() { this.contextRunner.withPropertyValues("spring.elasticsearch.rest.connection-timeout=15s", "spring.elasticsearch.rest.read-timeout=1m").run((context) -> { assertThat(context).hasSingleBean(RestHighLevelClient.class); @@ -114,6 +120,16 @@ class ElasticsearchRestClientAutoConfigurationTests { }); } + @Test + void configureWithCustomTimeouts() { + this.contextRunner.withPropertyValues("spring.elasticsearch.connection-timeout=15s", + "spring.elasticsearch.socket-timeout=1m").run((context) -> { + assertThat(context).hasSingleBean(RestHighLevelClient.class); + RestHighLevelClient restClient = context.getBean(RestHighLevelClient.class); + assertTimeouts(restClient, Duration.ofSeconds(15), Duration.ofMinutes(1)); + }); + } + private static void assertTimeouts(RestHighLevelClient restClient, Duration connectTimeout, Duration readTimeout) { assertThat(restClient.getLowLevelClient()).extracting("client.defaultConfig.socketTimeout") .isEqualTo(Math.toIntExact(readTimeout.toMillis())); @@ -121,50 +137,51 @@ class ElasticsearchRestClientAutoConfigurationTests { .isEqualTo(Math.toIntExact(connectTimeout.toMillis())); } - @Test - void configureUriWithUsernameOnly() { - this.contextRunner.withPropertyValues("spring.elasticsearch.rest.uris=http://user@localhost:9200") - .run((context) -> { - RestClient client = context.getBean(RestHighLevelClient.class).getLowLevelClient(); - assertThat(client.getNodes().stream().map(Node::getHost).map(HttpHost::toString)) - .containsExactly("http://localhost:9200"); - assertThat(client).extracting("client") - .extracting("credentialsProvider", - InstanceOfAssertFactories.type(CredentialsProvider.class)) - .satisfies((credentialsProvider) -> { - Credentials credentials = credentialsProvider - .getCredentials(new AuthScope("localhost", 9200)); - assertThat(credentials.getUserPrincipal().getName()).isEqualTo("user"); - assertThat(credentials.getPassword()).isNull(); - }); - }); + @ParameterizedPropertyPrefixTest + void configureUriWithNoScheme(String prefix) { + this.contextRunner.withPropertyValues(prefix + "uris=localhost:9876").run((context) -> { + RestClient client = context.getBean(RestHighLevelClient.class).getLowLevelClient(); + assertThat(client.getNodes().stream().map(Node::getHost).map(HttpHost::toString)) + .containsExactly("http://localhost:9876"); + }); } - @Test - void configureUriWithUsernameAndEmptyPassword() { - this.contextRunner.withPropertyValues("spring.elasticsearch.rest.uris=http://user:@localhost:9200") - .run((context) -> { - RestClient client = context.getBean(RestHighLevelClient.class).getLowLevelClient(); - assertThat(client.getNodes().stream().map(Node::getHost).map(HttpHost::toString)) - .containsExactly("http://localhost:9200"); - assertThat(client).extracting("client") - .extracting("credentialsProvider", - InstanceOfAssertFactories.type(CredentialsProvider.class)) - .satisfies((credentialsProvider) -> { - Credentials credentials = credentialsProvider - .getCredentials(new AuthScope("localhost", 9200)); - assertThat(credentials.getUserPrincipal().getName()).isEqualTo("user"); - assertThat(credentials.getPassword()).isEmpty(); - }); - }); + @ParameterizedPropertyPrefixTest + void configureUriWithUsernameOnly(String prefix) { + this.contextRunner.withPropertyValues(prefix + "uris=http://user@localhost:9200").run((context) -> { + RestClient client = context.getBean(RestHighLevelClient.class).getLowLevelClient(); + assertThat(client.getNodes().stream().map(Node::getHost).map(HttpHost::toString)) + .containsExactly("http://localhost:9200"); + assertThat(client).extracting("client") + .extracting("credentialsProvider", InstanceOfAssertFactories.type(CredentialsProvider.class)) + .satisfies((credentialsProvider) -> { + Credentials credentials = credentialsProvider.getCredentials(new AuthScope("localhost", 9200)); + assertThat(credentials.getUserPrincipal().getName()).isEqualTo("user"); + assertThat(credentials.getPassword()).isNull(); + }); + }); } - @Test - void configureUriWithUsernameAndPasswordWhenUsernameAndPasswordPropertiesSet() { - this.contextRunner - .withPropertyValues("spring.elasticsearch.rest.uris=http://user:password@localhost:9200,localhost:9201", - "spring.elasticsearch.rest.username=admin", "spring.elasticsearch.rest.password=admin") - .run((context) -> { + @ParameterizedPropertyPrefixTest + void configureUriWithUsernameAndEmptyPassword(String prefix) { + this.contextRunner.withPropertyValues(prefix + "uris=http://user:@localhost:9200").run((context) -> { + RestClient client = context.getBean(RestHighLevelClient.class).getLowLevelClient(); + assertThat(client.getNodes().stream().map(Node::getHost).map(HttpHost::toString)) + .containsExactly("http://localhost:9200"); + assertThat(client).extracting("client") + .extracting("credentialsProvider", InstanceOfAssertFactories.type(CredentialsProvider.class)) + .satisfies((credentialsProvider) -> { + Credentials credentials = credentialsProvider.getCredentials(new AuthScope("localhost", 9200)); + assertThat(credentials.getUserPrincipal().getName()).isEqualTo("user"); + assertThat(credentials.getPassword()).isEmpty(); + }); + }); + } + + @ParameterizedPropertyPrefixTest + void configureUriWithUsernameAndPasswordWhenUsernameAndPasswordPropertiesSet(String prefix) { + this.contextRunner.withPropertyValues(prefix + "uris=http://user:password@localhost:9200,localhost:9201", + prefix + "username=admin", prefix + "password=admin").run((context) -> { RestClient client = context.getBean(RestHighLevelClient.class).getLowLevelClient(); assertThat(client.getNodes().stream().map(Node::getHost).map(HttpHost::toString)) .containsExactly("http://localhost:9200", "http://localhost:9201"); @@ -203,10 +220,10 @@ class ElasticsearchRestClientAutoConfigurationTests { }); } - @Test - void configureWithCustomSnifferSettings() { - this.contextRunner.withPropertyValues("spring.elasticsearch.rest.sniffer.interval=180s", - "spring.elasticsearch.rest.sniffer.delay-after-failure=30s").run((context) -> { + @ParameterizedSnifferPropertyPrefixTest + void configureWithCustomSnifferSettings(String prefix) { + this.contextRunner.withPropertyValues(prefix + "interval=180s", prefix + "delay-after-failure=30s") + .run((context) -> { assertThat(context).hasSingleBean(Sniffer.class); Sniffer sniffer = context.getBean(Sniffer.class); assertThat(sniffer).hasFieldOrPropertyWithValue("sniffIntervalMillis", @@ -279,4 +296,20 @@ class ElasticsearchRestClientAutoConfigurationTests { } + @ParameterizedTest + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + @ValueSource(strings = { "spring.elasticsearch.rest.", "spring.elasticsearch." }) + static @interface ParameterizedPropertyPrefixTest { + + } + + @ParameterizedTest + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + @ValueSource(strings = { "spring.elasticsearch.rest.sniffer.", "spring.elasticsearch.restclient.sniffer." }) + static @interface ParameterizedSnifferPropertyPrefixTest { + + } + } diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/nosql.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/nosql.adoc index 3719c3de8f..ff3a4d7c40 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/nosql.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/nosql.adoc @@ -261,7 +261,7 @@ If you add your own `@Bean` of type `SolrClient`, it replaces the default. [[data.nosql.elasticsearch]] === Elasticsearch https://www.elastic.co/products/elasticsearch[Elasticsearch] is an open source, distributed, RESTful search and analytics engine. -Spring Boot offers basic auto-configuration for Elasticsearch. +Spring Boot offers basic auto-configuration for Elasticsearch clients. Spring Boot supports several clients: @@ -276,34 +276,36 @@ Spring Boot provides a dedicated "`Starter`", `spring-boot-starter-data-elastics ==== Connecting to Elasticsearch using REST clients Elasticsearch ships https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/index.html[two different REST clients] that you can use to query a cluster: the "Low Level" client and the "High Level" client. Spring Boot provides support for the "High Level" client, which ships with `org.elasticsearch.client:elasticsearch-rest-high-level-client`. - -If you have this dependency on the classpath, Spring Boot will auto-configure and register a `RestHighLevelClient` bean that by default targets `http://localhost:9200`. -You can further tune how `RestHighLevelClient` is configured, as shown in the following example: +Additionally, Spring Boot provides support for a reactive client, based on Spring Framework's `WebClient`, that ships with `org.springframework.data:spring-data-elasticsearch`. +By default, the clients will target `http://localhost:9200`. +You can use `spring.elasticsearch.*` properties to further tune how the clients are configured, as shown in the following example: [source,yaml,indent=0,subs="verbatim",configprops,configblocks] ---- spring: elasticsearch: - rest: - uris: "https://search.example.com:9200" - read-timeout: "10s" - username: "user" - password: "secret" + uris: "https://search.example.com:9200" + socket-timeout: "10s" + username: "user" + password: "secret" ---- -You can also register an arbitrary number of beans that implement `RestClientBuilderCustomizer` for more advanced customizations. -To take full control over the registration, define a `RestClientBuilder` bean. +[[data.nosql.elasticsearch.connecting-using-rest.restclient]] +===== Connecting to Elasticsearch using RestHighLevelClient +If you have `elasticsearch-rest-high-level-client` on the classpath, Spring Boot will auto-configure and register a `RestHighLevelClient` bean. +In addition to the properties described previously, to fine-tune the `RestHighLevelClient`, you can register an arbitrary number of beans that implement `RestClientBuilderCustomizer` for more advanced customizations. +To take full control over its registration, define a `RestClientBuilder` bean. TIP: If your application needs access to a "Low Level" `RestClient`, you can get it by calling `client.getLowLevelClient()` on the auto-configured `RestHighLevelClient`. -Additionally, if `elasticsearch-rest-client-sniffer` is on the classpath, a `Sniffer` is auto-configured to automatically discover nodes from a running Elasticsearch cluster and set them to the `RestHighLevelClient` bean. +Additionally, if `elasticsearch-rest-client-sniffer` is on the classpath, a `Sniffer` is auto-configured to automatically discover nodes from a running Elasticsearch cluster and set them on the `RestHighLevelClient` bean. You can further tune how `Sniffer` is configured, as shown in the following example: [source,yaml,indent=0,subs="verbatim",configprops,configblocks] ---- spring: elasticsearch: - rest: + restclient: sniffer: interval: 10m delay-after-failure: 30s @@ -311,31 +313,23 @@ You can further tune how `Sniffer` is configured, as shown in the following exam -[[data.nosql.elasticsearch.connecting-using-reactive-rest]] -==== Connecting to Elasticsearch using Reactive REST clients +[[data.nosql.elasticsearch.connecting-using-rest.webclient]] +===== Connecting to Elasticsearch using ReactiveElasticsearchClient {spring-data-elasticsearch}[Spring Data Elasticsearch] ships `ReactiveElasticsearchClient` for querying Elasticsearch instances in a reactive fashion. It is built on top of WebFlux's `WebClient`, so both `spring-boot-starter-elasticsearch` and `spring-boot-starter-webflux` dependencies are useful to enable this support. -By default, Spring Boot will auto-configure and register a `ReactiveElasticsearchClient` -bean that targets `http://localhost:9200`. -You can further tune how it is configured, as shown in the following example: +By default, Spring Boot will auto-configure and register a `ReactiveElasticsearchClient`. +In addition to the properties described previously, the `spring.elasticsearch.webclient.*` properties can be used to configure reactive-specific settings, as shown in the following example: [source,yaml,indent=0,subs="verbatim",configprops,configblocks] ---- spring: - data: - elasticsearch: - client: - reactive: - endpoints: "search.example.com:9200" - use-ssl: true - socket-timeout: "10s" - username: "user" - password: "secret" + elasticsearch: + webclient: + max-in-memory-size: 1MB ---- -If the configuration properties are not enough and you'd like to fully control the client -configuration, you can register a custom `ClientConfiguration` bean. +If the `spring.elasticsearch.*` and `spring.elasticsearch.webclient.*` configuration properties are not enough and you'd like to fully control the client configuration, you can register a custom `ClientConfiguration` bean. @@ -352,7 +346,7 @@ as shown in the following example: include::{docs-java}/features/nosql/elasticsearch/connectingusingspringdata/MyBean.java[] ---- -In the presence of `spring-data-elasticsearch` and the required dependencies for using a `WebClient` (typically `spring-boot-starter-webflux`), Spring Boot can also auto-configure a <> and a `ReactiveElasticsearchTemplate` as beans. +In the presence of `spring-data-elasticsearch` and the required dependencies for using a `WebClient` (typically `spring-boot-starter-webflux`), Spring Boot can also auto-configure a <> and a `ReactiveElasticsearchTemplate` as beans. They are the reactive equivalent of the other REST clients. diff --git a/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml index 7a6938e9f2..41eb4bea3d 100644 --- a/src/checkstyle/checkstyle-suppressions.xml +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -54,4 +54,7 @@ + + +