From 6324b72707a96a5dc0ce5a2155f7edbcd5c183bd Mon Sep 17 00:00:00 2001 From: Peter-Josef Meisch Date: Wed, 25 Jun 2025 20:09:00 +0200 Subject: [PATCH] Use the new Rest5Client as default, provide the old RestClient as optional. Original Pull Request: #3125 Closes #3117 Signed-off-by: Peter-Josef Meisch --- pom.xml | 2 + .../ROOT/pages/elasticsearch/clients.adoc | 197 +++++++++++- .../elasticsearch/elasticsearch-new.adoc | 1 + .../migration-guide-5.5-6.0.adoc | 4 + .../client/ClientConfiguration.java | 8 +- .../client/elc/ElasticsearchClients.java | 291 +++++++----------- .../elc/ElasticsearchConfiguration.java | 27 +- .../elc/ElasticsearchExceptionTranslator.java | 7 +- ...icsearchLegacyRestClientConfiguration.java | 144 +++++++++ .../ReactiveElasticsearchConfiguration.java | 30 +- ...icsearchLegacyRestClientConfiguration.java | 144 +++++++++ .../client/elc/rest5_client/Rest5Clients.java | 274 +++++++++++++++++ .../client/elc/rest5_client/package-info.java | 21 ++ .../client/elc/rest_client/RestClients.java | 195 ++++++++++++ .../client/elc/rest_client/package-info.java | 22 ++ .../elasticsearch/support/VersionInfo.java | 11 + .../elasticsearch/client/RestClientsTest.java | 204 +++++++++--- .../elasticsearch/client/elc/DevTests.java | 5 +- .../ELCRest5ClientWiremockTests.java} | 7 +- .../ELCRestClientWiremockTests.java | 145 +++++++++ .../client/elc/rest_client/package-info.java | 2 + .../ElasticsearchConfigurationELCTests.java | 9 +- ...LegacyRestClientConfigurationELCTests.java | 88 ++++++ ...iveElasticsearchConfigurationELCTests.java | 1 - ...LegacyRestClientConfigurationELCTests.java | 85 +++++ 25 files changed, 1667 insertions(+), 257 deletions(-) create mode 100644 src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchLegacyRestClientConfiguration.java create mode 100644 src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchLegacyRestClientConfiguration.java create mode 100644 src/main/java/org/springframework/data/elasticsearch/client/elc/rest5_client/Rest5Clients.java create mode 100644 src/main/java/org/springframework/data/elasticsearch/client/elc/rest5_client/package-info.java create mode 100644 src/main/java/org/springframework/data/elasticsearch/client/elc/rest_client/RestClients.java create mode 100644 src/main/java/org/springframework/data/elasticsearch/client/elc/rest_client/package-info.java rename src/test/java/org/springframework/data/elasticsearch/client/elc/{ELCWiremockTests.java => rest5_client/ELCRest5ClientWiremockTests.java} (93%) create mode 100644 src/test/java/org/springframework/data/elasticsearch/client/elc/rest_client/ELCRestClientWiremockTests.java create mode 100644 src/test/java/org/springframework/data/elasticsearch/client/elc/rest_client/package-info.java create mode 100644 src/test/java/org/springframework/data/elasticsearch/config/configuration/ElasticsearchLegacyRestClientConfigurationELCTests.java create mode 100644 src/test/java/org/springframework/data/elasticsearch/config/configuration/ReactiveElasticsearchLegacyRestClientConfigurationELCTests.java diff --git a/pom.xml b/pom.xml index 53f548016..04edd59d6 100644 --- a/pom.xml +++ b/pom.xml @@ -132,6 +132,7 @@ + org.elasticsearch.client elasticsearch-rest-client @@ -142,6 +143,7 @@ commons-logging + true diff --git a/src/main/antora/modules/ROOT/pages/elasticsearch/clients.adoc b/src/main/antora/modules/ROOT/pages/elasticsearch/clients.adoc index 0cf7d5ea3..b8250a9f9 100644 --- a/src/main/antora/modules/ROOT/pages/elasticsearch/clients.adoc +++ b/src/main/antora/modules/ROOT/pages/elasticsearch/clients.adoc @@ -6,10 +6,10 @@ This chapter illustrates configuration and usage of supported Elasticsearch clie Spring Data Elasticsearch operates upon an Elasticsearch client (provided by Elasticsearch client libraries) that is connected to a single Elasticsearch node or a cluster. Although the Elasticsearch Client can be used directly to work with the cluster, applications using Spring Data Elasticsearch normally use the higher level abstractions of xref:elasticsearch/template.adoc[Elasticsearch Operations] and xref:elasticsearch/repositories/elasticsearch-repositories.adoc[Elasticsearch Repositories]. -[[elasticsearch.clients.restclient]] -== Imperative Rest Client +[[elasticsearch.clients.rest5client]] +== Imperative Rest5Client -To use the imperative (non-reactive) client, a configuration bean must be configured like this: +To use the imperative (non-reactive) Rest5Client, a configuration bean must be configured like this: ==== [source,java] @@ -31,7 +31,7 @@ public class MyClientConfig extends ElasticsearchConfiguration { <.> for a detailed description of the builder methods see xref:elasticsearch/clients.adoc#elasticsearch.clients.configuration[Client Configuration] ==== -The javadoc:org.springframework.data.elasticsearch.client.elc.ElasticsearchConfiguration[]] class allows further configuration by overriding for example the `jsonpMapper()` or `transportOptions()` methods. +The javadoc:org.springframework.data.elasticsearch.client.elc.ElasticsearchConfiguration[] class allows further configuration by overriding for example the `jsonpMapper()` or `transportOptions()` methods. The following beans can then be injected in other Spring components: @@ -46,7 +46,81 @@ ElasticsearchOperations operations; <.> ElasticsearchClient elasticsearchClient; <.> @Autowired -RestClient restClient; <.> +Rest5Client rest5Client; <.> + +@Autowired +JsonpMapper jsonpMapper; <.> +---- + +<.> an implementation of javadoc:org.springframework.data.elasticsearch.core.ElasticsearchOperations[] +<.> the `co.elastic.clients.elasticsearch.ElasticsearchClient` that is used. +<.> the low level `Rest5Client` from the Elasticsearch libraries +<.> the `JsonpMapper` user by the Elasticsearch `Transport` +==== + +Basically one should just use the javadoc:org.springframework.data.elasticsearch.core.ElasticsearchOperations[] to interact with the Elasticsearch cluster. +When using repositories, this instance is used under the hood as well. + +[[elasticsearch.clients.restclient]] +== Deprecated Imperative RestClient + +To use the imperative (non-reactive) RestClient - deprecated since version 6 - , the following dependency needs to be added, adapt the correct version. The exclusion is needed in a Spring Boot application: +==== +[source,xml] +---- + + org.elasticsearch.client + elasticsearch-rest-client + ${elasticsearch-client.version} + + + commons-logging + commons-logging + + + + +---- +==== + +The configuration bean must be configured like this: + +==== +[source,java] +---- +import org.springframework.data.elasticsearch.client.elc.ElasticsearchLegacyRestClientConfiguration; + +@Configuration +public class MyClientConfig extends ElasticsearchLegacyRestClientConfiguration { + + @Override + public ClientConfiguration clientConfiguration() { + return ClientConfiguration.builder() <.> + .connectedTo("localhost:9200") + .build(); + } +} +---- + +<.> for a detailed description of the builder methods see xref:elasticsearch/clients.adoc#elasticsearch.clients.configuration[Client Configuration] +==== + +The javadoc:org.springframework.data.elasticsearch.client.elc.ElasticsearchConfiguration[] class allows further configuration by overriding for example the `jsonpMapper()` or `transportOptions()` methods. + + +The following beans can then be injected in other Spring components: + +==== +[source,java] +---- +import org.springframework.beans.factory.annotation.Autowired;@Autowired +ElasticsearchOperations operations; <.> + +@Autowired +ElasticsearchClient elasticsearchClient; <.> + +@Autowired +RestClient restClient; <.> @Autowired JsonpMapper jsonpMapper; <.> @@ -61,8 +135,8 @@ JsonpMapper jsonpMapper; <.> Basically one should just use the javadoc:org.springframework.data.elasticsearch.core.ElasticsearchOperations[] to interact with the Elasticsearch cluster. When using repositories, this instance is used under the hood as well. -[[elasticsearch.clients.reactiverestclient]] -== Reactive Rest Client +[[elasticsearch.clients.reactiverest5client]] +== Reactive Rest5Client When working with the reactive stack, the configuration must be derived from a different class: @@ -99,6 +173,65 @@ ReactiveElasticsearchOperations operations; <.> @Autowired ReactiveElasticsearchClient elasticsearchClient; <.> +@Autowired +Rest5Client rest5Client; <.> + +@Autowired +JsonpMapper jsonpMapper; <.> +---- + +the following can be injected: + +<.> an implementation of javadoc:org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations[] +<.> the `org.springframework.data.elasticsearch.client.elc.ReactiveElasticsearchClient` that is used. +This is a reactive implementation based on the Elasticsearch client implementation. +<.> the low level `RestClient` from the Elasticsearch libraries +<.> the `JsonpMapper` user by the Elasticsearch `Transport` +==== + +Basically one should just use the javadoc:org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations[] to interact with the Elasticsearch cluster. +When using repositories, this instance is used under the hood as well. + +[[elasticsearch.clients.reactiverestclient]] +== Deprecated Reactive RestClient + +See the section above for the imperative code to use the deprecated RestClient for the necessary dependencies to include. + +When working with the reactive stack, the configuration must be derived from a different class: + +==== +[source,java] +---- +import org.springframework.data.elasticsearch.client.elc.ReactiveElasticsearchLegacyRestClientConfiguration; + +@Configuration +public class MyClientConfig extends ReactiveElasticsearchLegacyRestClientConfiguration { + + @Override + public ClientConfiguration clientConfiguration() { + return ClientConfiguration.builder() <.> + .connectedTo("localhost:9200") + .build(); + } +} +---- + +<.> for a detailed description of the builder methods see xref:elasticsearch/clients.adoc#elasticsearch.clients.configuration[Client Configuration] +==== + +The javadoc:org.springframework.data.elasticsearch.client.elc.ReactiveElasticsearchConfiguration[] class allows further configuration by overriding for example the `jsonpMapper()` or `transportOptions()` methods. + +The following beans can then be injected in other Spring components: + +==== +[source,java] +---- +@Autowired +ReactiveElasticsearchOperations operations; <.> + +@Autowired +ReactiveElasticsearchClient elasticsearchClient; <.> + @Autowired RestClient restClient; <.> @@ -183,8 +316,8 @@ In the case this is not enough, the user can add callback functions by using the The following callbacks are provided: -[[elasticsearch.clients.configuration.callbacks.rest]] -==== Configuration of the low level Elasticsearch `RestClient`: +[[elasticsearch.clients.configuration.callbacks.rest5]] +==== Configuration of the low level Elasticsearch `Rest5Client`: This callback provides a `org.elasticsearch.client.RestClientBuilder` that can be used to configure the Elasticsearch `RestClient`: @@ -193,7 +326,24 @@ This callback provides a `org.elasticsearch.client.RestClientBuilder` that can b ---- ClientConfiguration.builder() .connectedTo("localhost:9200", "localhost:9291") - .withClientConfigurer(ElasticsearchClients.ElasticsearchRestClientConfigurationCallback.from(restClientBuilder -> { + .withClientConfigurer(Rest5Clients.ElasticsearchRest5ClientConfigurationCallback.from(restClientBuilder -> { + // configure the Elasticsearch Rest5Client + return restClientBuilder; + })) + .build(); +---- +==== +[[elasticsearch.clients.configuration.callbacks.rest]] +==== Configuration of the deprecated low level Elasticsearch `RestClient`: + +This callback provides a `org.elasticsearch.client.RestClientBuilder` that can be used to configure the Elasticsearch +`RestClient`: +==== +[source,java] +---- +ClientConfiguration.builder() + .connectedTo("localhost:9200", "localhost:9291") + .withClientConfigurer(RestClients.ElasticsearchRestClientConfigurationCallback.from(restClientBuilder -> { // configure the Elasticsearch RestClient return restClientBuilder; })) @@ -201,10 +351,29 @@ ClientConfiguration.builder() ---- ==== -[[elasticsearch.clients.configurationcallbacks.httpasync]] -==== Configuration of the HttpAsyncClient used by the low level Elasticsearch `RestClient`: +[[elasticsearch.clients.configurationcallbacks.httpasync5]] +==== Configuration of the HttpAsyncClient used by the low level Elasticsearch `Rest5Client`: -This callback provides a `org.apache.http.impl.nio.client.HttpAsyncClientBuilder` to configure the HttpCLient that is +This callback provides a `org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder` to configure the HttpClient that is +used by the `Rest5Client`. + +==== +[source,java] +---- +ClientConfiguration.builder() + .connectedTo("localhost:9200", "localhost:9291") + .withClientConfigurer(Rest5Clients.ElasticsearchHttpClientConfigurationCallback.from(httpAsyncClientBuilder -> { + // configure the HttpAsyncClient + return httpAsyncClientBuilder; + })) + .build(); +---- +==== + +[[elasticsearch.clients.configurationcallbacks.httpasync]] +==== Configuration of the HttpAsyncClient used by the deprecated low level Elasticsearch `RestClient`: + +This callback provides a `org.apache.http.impl.nio.client.HttpAsyncClientBuilder` to configure the HttpClient that is used by the `RestClient`. ==== @@ -212,7 +381,7 @@ used by the `RestClient`. ---- ClientConfiguration.builder() .connectedTo("localhost:9200", "localhost:9291") - .withClientConfigurer(ElasticsearchClients.ElasticsearchHttpClientConfigurationCallback.from(httpAsyncClientBuilder -> { + .withClientConfigurer(RestClients.ElasticsearchHttpClientConfigurationCallback.from(httpAsyncClientBuilder -> { // configure the HttpAsyncClient return httpAsyncClientBuilder; })) diff --git a/src/main/antora/modules/ROOT/pages/elasticsearch/elasticsearch-new.adoc b/src/main/antora/modules/ROOT/pages/elasticsearch/elasticsearch-new.adoc index b87f7276b..88a474d92 100644 --- a/src/main/antora/modules/ROOT/pages/elasticsearch/elasticsearch-new.adoc +++ b/src/main/antora/modules/ROOT/pages/elasticsearch/elasticsearch-new.adoc @@ -7,6 +7,7 @@ * Upgarde to Spring 7 * Switch to jspecify nullability annotations * Upgrade to Elasticsearch 9.0.3 +* Use the new Elasticsearch Rest5Client as default [[new-features.5-5-0]] diff --git a/src/main/antora/modules/ROOT/pages/migration-guides/migration-guide-5.5-6.0.adoc b/src/main/antora/modules/ROOT/pages/migration-guides/migration-guide-5.5-6.0.adoc index 7667701a1..87c62ab42 100644 --- a/src/main/antora/modules/ROOT/pages/migration-guides/migration-guide-5.5-6.0.adoc +++ b/src/main/antora/modules/ROOT/pages/migration-guides/migration-guide-5.5-6.0.adoc @@ -6,9 +6,13 @@ This section describes breaking changes from version 5.5.x to 6.0.x and how remo [[elasticsearch-migration-guide-5.5-6.0.breaking-changes]] == Breaking Changes +From version 6.0 on, Spring Data Elasticsearch uses the Elasticsearch 9 libraries and as default the new `Rest5Client` provided by these libraries. It is still possible to use the old `RestClient`, check xref:elasticsearch/clients.adoc[Elasticsearch clients] for information. The configuration callbacks for this `RestClient` have been moved from `org.springframework.data.elasticsearch.client.elc.ElasticsearchClients` to the `org.springframework.data.elasticsearch.client.elc.rest_client.RestClients` class. + [[elasticsearch-migration-guide-5.5-6.0.deprecations]] == Deprecations +All the code using the old `RestClient` has been moved to the `org.springframework.data.elasticsearch.client.elc.rest_client` package and has been deprecated. Users should switch to the classes from the `org.springframework.data.elasticsearch.client.elc.rest5_client` package. + === Removals diff --git a/src/main/java/org/springframework/data/elasticsearch/client/ClientConfiguration.java b/src/main/java/org/springframework/data/elasticsearch/client/ClientConfiguration.java index f092e2bf6..64949fcf2 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/ClientConfiguration.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/ClientConfiguration.java @@ -127,10 +127,16 @@ public interface ClientConfiguration { Optional getCaFingerprint(); /** - * Returns the {@link HostnameVerifier} to use. Can be {@link Optional#empty()} if not configured. + * Returns the {@link HostnameVerifier} to use. Must be {@link Optional#empty()} if not configured. + * Cannot be used with the Rest5Client used from Elasticsearch 9 on as the underlying Apache http components 5 does not offer a way + * to set this. Users that need a hostname verifier must integrate this in a SSLContext. + * Returning a value here is ignored in this case * * @return the {@link HostnameVerifier} to use. Can be {@link Optional#empty()} if not configured. + * @deprecated since 6.0 */ + // todo #3117 document this + @Deprecated(since = "6.0", forRemoval=true) Optional getHostNameVerifier(); /** diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchClients.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchClients.java index c4f6452cc..0e23ab36a 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchClients.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchClients.java @@ -15,44 +15,38 @@ */ package org.springframework.data.elasticsearch.client.elc; +import static org.springframework.data.elasticsearch.client.elc.rest5_client.Rest5Clients.*; +import static org.springframework.data.elasticsearch.client.elc.rest_client.RestClients.*; + import co.elastic.clients.elasticsearch.ElasticsearchClient; import co.elastic.clients.json.JsonpMapper; import co.elastic.clients.json.jackson.JacksonJsonpMapper; import co.elastic.clients.transport.ElasticsearchTransport; import co.elastic.clients.transport.TransportOptions; -import co.elastic.clients.transport.TransportUtils; import co.elastic.clients.transport.Version; +import co.elastic.clients.transport.rest5_client.Rest5ClientOptions; +import co.elastic.clients.transport.rest5_client.Rest5ClientTransport; +import co.elastic.clients.transport.rest5_client.low_level.RequestOptions; +import co.elastic.clients.transport.rest5_client.low_level.Rest5Client; import co.elastic.clients.transport.rest_client.RestClientOptions; import co.elastic.clients.transport.rest_client.RestClientTransport; -import java.net.InetSocketAddress; -import java.time.Duration; -import java.util.Arrays; -import java.util.List; import java.util.function.Consumer; import java.util.function.Function; -import java.util.function.Supplier; -import java.util.stream.Collectors; -import org.apache.http.HttpHost; -import org.apache.http.HttpRequest; -import org.apache.http.HttpRequestInterceptor; -import org.apache.http.client.config.RequestConfig; import org.apache.http.entity.ContentType; import org.apache.http.impl.nio.client.HttpAsyncClientBuilder; -import org.apache.http.message.BasicHeader; import org.apache.http.message.BasicNameValuePair; -import org.apache.http.protocol.HttpContext; -import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.RestClient; -import org.elasticsearch.client.RestClientBuilder; import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.client.ClientConfiguration; -import org.springframework.data.elasticsearch.support.HttpHeaders; +import org.springframework.data.elasticsearch.support.VersionInfo; import org.springframework.util.Assert; /** - * Utility class to create the different Elasticsearch clients + * Utility class to create the different Elasticsearch clients. The RestClient class is the one used in Elasticsearch + * until version 9, it is still available, but it's use is deprecated. The Rest5Client class is the one that should be + * used from Elasticsearch 9 on. * * @author Peter-Josef Meisch * @since 4.4 @@ -119,18 +113,32 @@ public final class ElasticsearchClients { * * @param restClient the underlying {@link RestClient} * @return the {@link ReactiveElasticsearchClient} + * @deprecated since 6.0, use the version with a Rest5Client. */ + @Deprecated(since = "6.0", forRemoval = true) public static ReactiveElasticsearchClient createReactive(RestClient restClient) { return createReactive(restClient, null, DEFAULT_JSONP_MAPPER); } + /** + * Creates a new {@link ReactiveElasticsearchClient}. + * + * @param rest5Client the underlying {@link RestClient} + * @return the {@link ReactiveElasticsearchClient} + */ + public static ReactiveElasticsearchClient createReactive(Rest5Client rest5Client) { + return createReactive(rest5Client, null, DEFAULT_JSONP_MAPPER); + } + /** * Creates a new {@link ReactiveElasticsearchClient}. * * @param restClient the underlying {@link RestClient} * @param transportOptions options to be added to each request. * @return the {@link ReactiveElasticsearchClient} + * @deprecated since 6.0, use the version with a Rest5Client. */ + @Deprecated(since = "6.0", forRemoval = true) public static ReactiveElasticsearchClient createReactive(RestClient restClient, @Nullable TransportOptions transportOptions, JsonpMapper jsonpMapper) { @@ -139,6 +147,21 @@ public final class ElasticsearchClients { var transport = getElasticsearchTransport(restClient, REACTIVE_CLIENT, transportOptions, jsonpMapper); return createReactive(transport); } + /** + * Creates a new {@link ReactiveElasticsearchClient}. + * + * @param rest5Client the underlying {@link RestClient} + * @param transportOptions options to be added to each request. + * @return the {@link ReactiveElasticsearchClient} + */ + public static ReactiveElasticsearchClient createReactive(Rest5Client rest5Client, + @Nullable TransportOptions transportOptions, JsonpMapper jsonpMapper) { + + Assert.notNull(rest5Client, "restClient must not be null"); + + var transport = getElasticsearchTransport(rest5Client, REACTIVE_CLIENT, transportOptions, jsonpMapper); + return createReactive(transport); + } /** * Creates a new {@link ReactiveElasticsearchClient} that uses the given {@link ElasticsearchTransport}. @@ -156,17 +179,21 @@ public final class ElasticsearchClients { // region imperative client /** - * Creates a new imperative {@link ElasticsearchClient} + * Creates a new imperative {@link ElasticsearchClient}. This uses a RestClient, if the old RestClient is needed, this + * must be created with the {@link org.springframework.data.elasticsearch.client.elc.rest_client.RestClients} class + * and passed in as parameter. * * @param clientConfiguration configuration options, must not be {@literal null}. * @return the {@link ElasticsearchClient} */ public static ElasticsearchClient createImperative(ClientConfiguration clientConfiguration) { - return createImperative(getRestClient(clientConfiguration), null, DEFAULT_JSONP_MAPPER); + return createImperative(getRest5Client(clientConfiguration), null, DEFAULT_JSONP_MAPPER); } /** - * Creates a new imperative {@link ElasticsearchClient} + * Creates a new imperative {@link ElasticsearchClient}. This uses a RestClient, if the old RestClient is needed, this + * must be created with the {@link org.springframework.data.elasticsearch.client.elc.rest_client.RestClients} class + * and passed in as parameter. * * @param clientConfiguration configuration options, must not be {@literal null}. * @param transportOptions options to be added to each request. @@ -174,7 +201,7 @@ public final class ElasticsearchClients { */ public static ElasticsearchClient createImperative(ClientConfiguration clientConfiguration, TransportOptions transportOptions) { - return createImperative(getRestClient(clientConfiguration), transportOptions, DEFAULT_JSONP_MAPPER); + return createImperative(getRest5Client(clientConfiguration), transportOptions, DEFAULT_JSONP_MAPPER); } /** @@ -182,11 +209,23 @@ public final class ElasticsearchClients { * * @param restClient the RestClient to use * @return the {@link ElasticsearchClient} + * @deprecated since 6.0, use the version with a Rest5Client. */ + @Deprecated(since = "6.0", forRemoval = true) public static ElasticsearchClient createImperative(RestClient restClient) { return createImperative(restClient, null, DEFAULT_JSONP_MAPPER); } + /** + * Creates a new imperative {@link ElasticsearchClient} + * + * @param rest5Client the Rest5Client to use + * @return the {@link ElasticsearchClient} + */ + public static ElasticsearchClient createImperative(Rest5Client rest5Client) { + return createImperative(rest5Client, null, DEFAULT_JSONP_MAPPER); + } + /** * Creates a new imperative {@link ElasticsearchClient} * @@ -194,7 +233,9 @@ public final class ElasticsearchClients { * @param transportOptions options to be added to each request. * @param jsonpMapper the mapper for the transport to use * @return the {@link ElasticsearchClient} + * @deprecated since 6.0, use the version with a Rest5Client. */ + @Deprecated(since = "6.0", forRemoval = true) public static ElasticsearchClient createImperative(RestClient restClient, @Nullable TransportOptions transportOptions, JsonpMapper jsonpMapper) { @@ -206,6 +247,27 @@ public final class ElasticsearchClients { return createImperative(transport); } + /** + * Creates a new imperative {@link ElasticsearchClient} + * + * @param rest5Client the Rest5Client to use + * @param transportOptions options to be added to each request. + * @param jsonpMapper the mapper for the transport to use + * @return the {@link ElasticsearchClient} + * @since 6.0 + */ + public static ElasticsearchClient createImperative(Rest5Client rest5Client, + @Nullable TransportOptions transportOptions, + JsonpMapper jsonpMapper) { + + Assert.notNull(rest5Client, "restClient must not be null"); + + ElasticsearchTransport transport = getElasticsearchTransport(rest5Client, IMPERATIVE_CLIENT, transportOptions, + jsonpMapper); + + return createImperative(transport); + } + /** * Creates a new {@link ElasticsearchClient} that uses the given {@link ElasticsearchTransport}. * @@ -220,96 +282,6 @@ public final class ElasticsearchClients { } // endregion - // region low level RestClient - private static RestClientOptions.Builder getRestClientOptionsBuilder(@Nullable TransportOptions transportOptions) { - - if (transportOptions instanceof RestClientOptions restClientOptions) { - return restClientOptions.toBuilder(); - } - - var builder = new RestClientOptions.Builder(RequestOptions.DEFAULT.toBuilder()); - - if (transportOptions != null) { - transportOptions.headers().forEach(header -> builder.addHeader(header.getKey(), header.getValue())); - transportOptions.queryParameters().forEach(builder::setParameter); - builder.onWarnings(transportOptions.onWarnings()); - } - - return builder; - } - - /** - * Creates a low level {@link RestClient} for the given configuration. - * - * @param clientConfiguration must not be {@literal null} - * @return the {@link RestClient} - */ - public static RestClient getRestClient(ClientConfiguration clientConfiguration) { - return getRestClientBuilder(clientConfiguration).build(); - } - - private static RestClientBuilder getRestClientBuilder(ClientConfiguration clientConfiguration) { - HttpHost[] httpHosts = formattedHosts(clientConfiguration.getEndpoints(), clientConfiguration.useSsl()).stream() - .map(HttpHost::create).toArray(HttpHost[]::new); - RestClientBuilder builder = RestClient.builder(httpHosts); - - if (clientConfiguration.getPathPrefix() != null) { - builder.setPathPrefix(clientConfiguration.getPathPrefix()); - } - - HttpHeaders headers = clientConfiguration.getDefaultHeaders(); - - if (!headers.isEmpty()) { - builder.setDefaultHeaders(toHeaderArray(headers)); - } - - builder.setHttpClientConfigCallback(clientBuilder -> { - if (clientConfiguration.getCaFingerprint().isPresent()) { - clientBuilder - .setSSLContext(TransportUtils.sslContextFromCaFingerprint(clientConfiguration.getCaFingerprint().get())); - } - clientConfiguration.getSslContext().ifPresent(clientBuilder::setSSLContext); - clientConfiguration.getHostNameVerifier().ifPresent(clientBuilder::setSSLHostnameVerifier); - clientBuilder.addInterceptorLast(new CustomHeaderInjector(clientConfiguration.getHeadersSupplier())); - - RequestConfig.Builder requestConfigBuilder = RequestConfig.custom(); - Duration connectTimeout = clientConfiguration.getConnectTimeout(); - - if (!connectTimeout.isNegative()) { - requestConfigBuilder.setConnectTimeout(Math.toIntExact(connectTimeout.toMillis())); - } - - Duration socketTimeout = clientConfiguration.getSocketTimeout(); - - if (!socketTimeout.isNegative()) { - requestConfigBuilder.setSocketTimeout(Math.toIntExact(socketTimeout.toMillis())); - requestConfigBuilder.setConnectionRequestTimeout(Math.toIntExact(socketTimeout.toMillis())); - } - - clientBuilder.setDefaultRequestConfig(requestConfigBuilder.build()); - - clientConfiguration.getProxy().map(HttpHost::create).ifPresent(clientBuilder::setProxy); - - for (ClientConfiguration.ClientConfigurationCallback clientConfigurer : clientConfiguration - .getClientConfigurers()) { - if (clientConfigurer instanceof ElasticsearchHttpClientConfigurationCallback restClientConfigurationCallback) { - clientBuilder = restClientConfigurationCallback.configure(clientBuilder); - } - } - - return clientBuilder; - }); - - for (ClientConfiguration.ClientConfigurationCallback clientConfigurationCallback : clientConfiguration - .getClientConfigurers()) { - if (clientConfigurationCallback instanceof ElasticsearchRestClientConfigurationCallback configurationCallback) { - builder = configurationCallback.configure(builder); - } - } - return builder; - } - // endregion - // region Elasticsearch transport /** * Creates an {@link ElasticsearchTransport} that will use the given client that additionally is customized with a @@ -320,7 +292,9 @@ public final class ElasticsearchClients { * @param transportOptions options for the transport * @param jsonpMapper mapper for the transport * @return ElasticsearchTransport + * @deprecated since 6.0, use the version taking a Rest5Client */ + @Deprecated(since = "6.0", forRemoval = true) public static ElasticsearchTransport getElasticsearchTransport(RestClient restClient, String clientType, @Nullable TransportOptions transportOptions, JsonpMapper jsonpMapper) { @@ -329,7 +303,7 @@ public final class ElasticsearchClients { Assert.notNull(jsonpMapper, "jsonpMapper must not be null"); TransportOptions.Builder transportOptionsBuilder = transportOptions != null ? transportOptions.toBuilder() - : new RestClientOptions(RequestOptions.DEFAULT, false).toBuilder(); + : new RestClientOptions(org.elasticsearch.client.RequestOptions.DEFAULT, false).toBuilder(); RestClientOptions.Builder restClientOptionsBuilder = getRestClientOptionsBuilder(transportOptions); @@ -353,70 +327,35 @@ public final class ElasticsearchClients { return new RestClientTransport(restClient, jsonpMapper, restClientOptionsBuilder.build()); } + + /** + * Creates an {@link ElasticsearchTransport} that will use the given client that additionally is customized with a + * header to contain the clientType + * + * @param rest5Client the client to use + * @param clientType the client type to pass in each request as header + * @param transportOptions options for the transport + * @param jsonpMapper mapper for the transport + * @return ElasticsearchTransport + */ + public static ElasticsearchTransport getElasticsearchTransport(Rest5Client rest5Client, String clientType, + @Nullable TransportOptions transportOptions, JsonpMapper jsonpMapper) { + + Assert.notNull(rest5Client, "restClient must not be null"); + Assert.notNull(clientType, "clientType must not be null"); + Assert.notNull(jsonpMapper, "jsonpMapper must not be null"); + + TransportOptions.Builder transportOptionsBuilder = transportOptions != null ? transportOptions.toBuilder() + : new Rest5ClientOptions(RequestOptions.DEFAULT, false).toBuilder(); + + Rest5ClientOptions.Builder rest5ClientOptionsBuilder = getRest5ClientOptionsBuilder(transportOptions); + + rest5ClientOptionsBuilder.addHeader(X_SPRING_DATA_ELASTICSEARCH_CLIENT, + VersionInfo.clientVersions() + " / " + clientType); + + return new Rest5ClientTransport(rest5Client, jsonpMapper, rest5ClientOptionsBuilder.build()); + } // endregion - private static List formattedHosts(List hosts, boolean useSsl) { - return hosts.stream().map(it -> (useSsl ? "https" : "http") + "://" + it.getHostString() + ':' + it.getPort()) - .collect(Collectors.toList()); - } - - private static org.apache.http.Header[] toHeaderArray(HttpHeaders headers) { - return headers.entrySet().stream() // - .flatMap(entry -> entry.getValue().stream() // - .map(value -> new BasicHeader(entry.getKey(), value))) // - .toArray(org.apache.http.Header[]::new); - } - - /** - * Interceptor to inject custom supplied headers. - * - * @since 4.4 - */ - private record CustomHeaderInjector(Supplier headersSupplier) implements HttpRequestInterceptor { - - @Override - public void process(HttpRequest request, HttpContext context) { - HttpHeaders httpHeaders = headersSupplier.get(); - - if (httpHeaders != null && !httpHeaders.isEmpty()) { - Arrays.stream(toHeaderArray(httpHeaders)).forEach(request::addHeader); - } - } - } - - /** - * {@link org.springframework.data.elasticsearch.client.ClientConfiguration.ClientConfigurationCallback} to configure - * the Elasticsearch RestClient's Http client with a {@link HttpAsyncClientBuilder} - * - * @since 4.4 - */ - public interface ElasticsearchHttpClientConfigurationCallback - extends ClientConfiguration.ClientConfigurationCallback { - - static ElasticsearchHttpClientConfigurationCallback from( - Function httpClientBuilderCallback) { - - Assert.notNull(httpClientBuilderCallback, "httpClientBuilderCallback must not be null"); - - return httpClientBuilderCallback::apply; - } - } - - /** - * {@link org.springframework.data.elasticsearch.client.ClientConfiguration.ClientConfigurationCallback} to configure - * the RestClient client with a {@link RestClientBuilder} - * - * @since 5.0 - */ - public interface ElasticsearchRestClientConfigurationCallback - extends ClientConfiguration.ClientConfigurationCallback { - - static ElasticsearchRestClientConfigurationCallback from( - Function restClientBuilderCallback) { - - Assert.notNull(restClientBuilderCallback, "restClientBuilderCallback must not be null"); - - return restClientBuilderCallback::apply; - } - } + // todo #3117 remove and document that ElasticsearchHttpClientConfigurationCallback has been move to RestClients. } diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchConfiguration.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchConfiguration.java index 93d26101a..f4a90fff5 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchConfiguration.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchConfiguration.java @@ -20,12 +20,13 @@ import co.elastic.clients.json.JsonpMapper; import co.elastic.clients.json.jackson.JacksonJsonpMapper; import co.elastic.clients.transport.ElasticsearchTransport; import co.elastic.clients.transport.TransportOptions; -import co.elastic.clients.transport.rest_client.RestClientOptions; +import co.elastic.clients.transport.rest5_client.Rest5ClientOptions; +import co.elastic.clients.transport.rest5_client.low_level.RequestOptions; +import co.elastic.clients.transport.rest5_client.low_level.Rest5Client; -import org.elasticsearch.client.RequestOptions; -import org.elasticsearch.client.RestClient; import org.springframework.context.annotation.Bean; import org.springframework.data.elasticsearch.client.ClientConfiguration; +import org.springframework.data.elasticsearch.client.elc.rest5_client.Rest5Clients; import org.springframework.data.elasticsearch.config.ElasticsearchConfigurationSupport; import org.springframework.data.elasticsearch.core.ElasticsearchOperations; import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter; @@ -38,7 +39,9 @@ import com.fasterxml.jackson.databind.SerializationFeature; /** * Base class for a @{@link org.springframework.context.annotation.Configuration} class to set up the Elasticsearch * connection using the Elasticsearch Client. This class exposes different parts of the setup as Spring beans. Deriving - * classes must provide the {@link ClientConfiguration} to use. + * classes must provide the {@link ClientConfiguration} to use. From Version 6.0 on, this class uses the new Rest5Client + * from Elasticsearch 9. The old implementation using the RestClient is still available under the name + * {@link ElasticsearchLegacyRestClientConfiguration}. * * @author Peter-Josef Meisch * @since 4.4 @@ -60,27 +63,27 @@ public abstract class ElasticsearchConfiguration extends ElasticsearchConfigurat * @return RestClient */ @Bean - public RestClient elasticsearchRestClient(ClientConfiguration clientConfiguration) { + public Rest5Client elasticsearchRest5Client(ClientConfiguration clientConfiguration) { Assert.notNull(clientConfiguration, "clientConfiguration must not be null"); - return ElasticsearchClients.getRestClient(clientConfiguration); + return Rest5Clients.getRest5Client(clientConfiguration); } /** - * Provides the Elasticsearch transport to be used. The default implementation uses the {@link RestClient} bean and + * Provides the Elasticsearch transport to be used. The default implementation uses the {@link Rest5Client} bean and * the {@link JsonpMapper} bean provided in this class. * * @return the {@link ElasticsearchTransport} * @since 5.2 */ @Bean - public ElasticsearchTransport elasticsearchTransport(RestClient restClient, JsonpMapper jsonpMapper) { + public ElasticsearchTransport elasticsearchTransport(Rest5Client rest5Client, JsonpMapper jsonpMapper) { - Assert.notNull(restClient, "restClient must not be null"); + Assert.notNull(rest5Client, "restClient must not be null"); Assert.notNull(jsonpMapper, "jsonpMapper must not be null"); - return ElasticsearchClients.getElasticsearchTransport(restClient, ElasticsearchClients.IMPERATIVE_CLIENT, + return ElasticsearchClients.getElasticsearchTransport(rest5Client, ElasticsearchClients.IMPERATIVE_CLIENT, transportOptions(), jsonpMapper); } @@ -115,7 +118,7 @@ public abstract class ElasticsearchConfiguration extends ElasticsearchConfigurat } /** - * Provides the JsonpMapper bean that is used in the {@link #elasticsearchTransport(RestClient, JsonpMapper)} method. + * Provides the JsonpMapper bean that is used in the {@link #elasticsearchTransport(Rest5Client, JsonpMapper)} method. * * @return the {@link JsonpMapper} to use * @since 5.2 @@ -135,6 +138,6 @@ public abstract class ElasticsearchConfiguration extends ElasticsearchConfigurat * @return the options that should be added to every request. Must not be {@literal null} */ public TransportOptions transportOptions() { - return new RestClientOptions(RequestOptions.DEFAULT, false); + return new Rest5ClientOptions(RequestOptions.DEFAULT, false); } } diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchExceptionTranslator.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchExceptionTranslator.java index 224e9c671..6f454e60b 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchExceptionTranslator.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchExceptionTranslator.java @@ -119,14 +119,19 @@ public class ElasticsearchExceptionTranslator implements PersistenceExceptionTra String message = null; if (exception instanceof ResponseException responseException) { + // this code is for the old RestClient status = responseException.getResponse().getStatusLine().getStatusCode(); message = responseException.getMessage(); + } else if (exception instanceof ElasticsearchException elasticsearchException) { + // using the RestClient throws this + status = elasticsearchException.status(); + message = elasticsearchException.getMessage(); } else if (exception.getCause() != null) { checkForConflictException(exception.getCause()); } if (status != null && message != null) { - if (status == 409 && message.contains("type\":\"version_conflict_engine_exception")) + if (status == 409 && message.contains("version_conflict_engine_exception")) if (message.contains("version conflict, required seqNo")) { throw new OptimisticLockingFailureException("Cannot index a document due to seq_no+primary_term conflict", exception); diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchLegacyRestClientConfiguration.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchLegacyRestClientConfiguration.java new file mode 100644 index 000000000..2005faf5b --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchLegacyRestClientConfiguration.java @@ -0,0 +1,144 @@ +/* + * Copyright 2021-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.data.elasticsearch.client.elc; + +import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.json.JsonpMapper; +import co.elastic.clients.json.jackson.JacksonJsonpMapper; +import co.elastic.clients.transport.ElasticsearchTransport; +import co.elastic.clients.transport.TransportOptions; +import co.elastic.clients.transport.rest_client.RestClientOptions; + +import org.elasticsearch.client.RequestOptions; +import org.elasticsearch.client.RestClient; +import org.springframework.context.annotation.Bean; +import org.springframework.data.elasticsearch.client.ClientConfiguration; +import org.springframework.data.elasticsearch.client.elc.rest_client.RestClients; +import org.springframework.data.elasticsearch.config.ElasticsearchConfigurationSupport; +import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter; +import org.springframework.util.Assert; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; + +/** + * Base class for a @{@link org.springframework.context.annotation.Configuration} class to set up the Elasticsearch + * connection using the Elasticsearch Client. This class exposes different parts of the setup as Spring beans. Deriving + * classes must provide the {@link ClientConfiguration} to use.
+ * This class uses the Elasticsearch RestClient which was replaced by the Rest5Client in Elasticsearch 9. It is still + * available here but deprecated. + * + * @author Peter-Josef Meisch + * @since 4.4 + * @deprecated since 6.0, use {@link ElasticsearchConfiguration} + */ +@Deprecated(since = "6.0", forRemoval=true) +public abstract class ElasticsearchLegacyRestClientConfiguration extends ElasticsearchConfigurationSupport { + + /** + * Must be implemented by deriving classes to provide the {@link ClientConfiguration}. + * + * @return configuration, must not be {@literal null} + */ + @Bean(name = "elasticsearchClientConfiguration") + public abstract ClientConfiguration clientConfiguration(); + + /** + * Provides the underlying low level Elasticsearch RestClient. + * + * @param clientConfiguration configuration for the client, must not be {@literal null} + * @return RestClient + */ + @Bean + public RestClient elasticsearchRestClient(ClientConfiguration clientConfiguration) { + + Assert.notNull(clientConfiguration, "clientConfiguration must not be null"); + + return RestClients.getRestClient(clientConfiguration); + } + + /** + * Provides the Elasticsearch transport to be used. The default implementation uses the {@link RestClient} bean and + * the {@link JsonpMapper} bean provided in this class. + * + * @return the {@link ElasticsearchTransport} + * @since 5.2 + */ + @Bean + public ElasticsearchTransport elasticsearchTransport(RestClient restClient, JsonpMapper jsonpMapper) { + + Assert.notNull(restClient, "restClient must not be null"); + Assert.notNull(jsonpMapper, "jsonpMapper must not be null"); + + return ElasticsearchClients.getElasticsearchTransport(restClient, ElasticsearchClients.IMPERATIVE_CLIENT, + transportOptions(), jsonpMapper); + } + + /** + * Provides the {@link ElasticsearchClient} to be used. + * + * @param transport the {@link ElasticsearchTransport} to use + * @return ElasticsearchClient instance + */ + @Bean + public ElasticsearchClient elasticsearchClient(ElasticsearchTransport transport) { + + Assert.notNull(transport, "transport must not be null"); + + return ElasticsearchClients.createImperative(transport); + } + + /** + * Creates a {@link ElasticsearchOperations} implementation using an {@link ElasticsearchClient}. + * + * @return never {@literal null}. + */ + @Bean(name = { "elasticsearchOperations", "elasticsearchTemplate" }) + public ElasticsearchOperations elasticsearchOperations(ElasticsearchConverter elasticsearchConverter, + ElasticsearchClient elasticsearchClient) { + + ElasticsearchTemplate template = new ElasticsearchTemplate(elasticsearchClient, elasticsearchConverter); + template.setRefreshPolicy(refreshPolicy()); + + return template; + } + + /** + * Provides the JsonpMapper bean that is used in the {@link #elasticsearchTransport(RestClient, JsonpMapper)} method. + * + * @return the {@link JsonpMapper} to use + * @since 5.2 + */ + @Bean + public JsonpMapper jsonpMapper() { + // we need to create our own objectMapper that keeps null values in order to provide the storeNullValue + // functionality. The one Elasticsearch would provide removes the nulls. We remove unwanted nulls before they get + // into this mapper, so we can safely keep them here. + var objectMapper = (new ObjectMapper()) + .configure(SerializationFeature.INDENT_OUTPUT, false) + .setSerializationInclusion(JsonInclude.Include.ALWAYS); + return new JacksonJsonpMapper(objectMapper); + } + + /** + * @return the options that should be added to every request. Must not be {@literal null} + */ + public TransportOptions transportOptions() { + return new RestClientOptions(RequestOptions.DEFAULT, false); + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchConfiguration.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchConfiguration.java index 2506b59c0..544927073 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchConfiguration.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchConfiguration.java @@ -19,12 +19,18 @@ import co.elastic.clients.json.JsonpMapper; import co.elastic.clients.json.jackson.JacksonJsonpMapper; import co.elastic.clients.transport.ElasticsearchTransport; import co.elastic.clients.transport.TransportOptions; +import co.elastic.clients.transport.rest5_client.Rest5ClientOptions; +import co.elastic.clients.transport.rest5_client.low_level.RequestOptions; +import co.elastic.clients.transport.rest5_client.low_level.Rest5Client; import co.elastic.clients.transport.rest_client.RestClientOptions; -import org.elasticsearch.client.RequestOptions; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; import org.elasticsearch.client.RestClient; import org.springframework.context.annotation.Bean; import org.springframework.data.elasticsearch.client.ClientConfiguration; +import org.springframework.data.elasticsearch.client.elc.rest5_client.Rest5Clients; import org.springframework.data.elasticsearch.config.ElasticsearchConfigurationSupport; import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations; import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter; @@ -55,11 +61,11 @@ public abstract class ReactiveElasticsearchConfiguration extends ElasticsearchCo * @return RestClient */ @Bean - public RestClient elasticsearchRestClient(ClientConfiguration clientConfiguration) { + public Rest5Client elasticsearchRestClient(ClientConfiguration clientConfiguration) { Assert.notNull(clientConfiguration, "clientConfiguration must not be null"); - return ElasticsearchClients.getRestClient(clientConfiguration); + return Rest5Clients.getRest5Client(clientConfiguration); } /** @@ -70,12 +76,12 @@ public abstract class ReactiveElasticsearchConfiguration extends ElasticsearchCo * @since 5.2 */ @Bean - public ElasticsearchTransport elasticsearchTransport(RestClient restClient, JsonpMapper jsonpMapper) { + public ElasticsearchTransport elasticsearchTransport(Rest5Client rest5Client, JsonpMapper jsonpMapper) { - Assert.notNull(restClient, "restClient must not be null"); + Assert.notNull(rest5Client, "restClient must not be null"); Assert.notNull(jsonpMapper, "jsonpMapper must not be null"); - return ElasticsearchClients.getElasticsearchTransport(restClient, ElasticsearchClients.REACTIVE_CLIENT, + return ElasticsearchClients.getElasticsearchTransport(rest5Client, ElasticsearchClients.REACTIVE_CLIENT, transportOptions(), jsonpMapper); } @@ -110,7 +116,7 @@ public abstract class ReactiveElasticsearchConfiguration extends ElasticsearchCo } /** - * Provides the JsonpMapper that is used in the {@link #elasticsearchTransport(RestClient, JsonpMapper)} method and + * Provides the JsonpMapper that is used in the {@link #elasticsearchTransport(Rest5Client, JsonpMapper)} method and * exposes it as a bean. * * @return the {@link JsonpMapper} to use @@ -118,13 +124,19 @@ public abstract class ReactiveElasticsearchConfiguration extends ElasticsearchCo */ @Bean public JsonpMapper jsonpMapper() { - return new JacksonJsonpMapper(); + // we need to create our own objectMapper that keeps null values in order to provide the storeNullValue + // functionality. The one Elasticsearch would provide removes the nulls. We remove unwanted nulls before they get + // into this mapper, so we can safely keep them here. + var objectMapper = (new ObjectMapper()) + .configure(SerializationFeature.INDENT_OUTPUT, false) + .setSerializationInclusion(JsonInclude.Include.ALWAYS); + return new JacksonJsonpMapper(objectMapper); } /** * @return the options that should be added to every request. Must not be {@literal null} */ public TransportOptions transportOptions() { - return new RestClientOptions(RequestOptions.DEFAULT, false).toBuilder().build(); + return new Rest5ClientOptions(RequestOptions.DEFAULT, false); } } diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchLegacyRestClientConfiguration.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchLegacyRestClientConfiguration.java new file mode 100644 index 000000000..98e3bf8e2 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchLegacyRestClientConfiguration.java @@ -0,0 +1,144 @@ +/* + * Copyright 2021-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.data.elasticsearch.client.elc; + +import co.elastic.clients.json.JsonpMapper; +import co.elastic.clients.json.jackson.JacksonJsonpMapper; +import co.elastic.clients.transport.ElasticsearchTransport; +import co.elastic.clients.transport.TransportOptions; +import co.elastic.clients.transport.rest_client.RestClientOptions; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import org.elasticsearch.client.RequestOptions; +import org.elasticsearch.client.RestClient; +import org.springframework.context.annotation.Bean; +import org.springframework.data.elasticsearch.client.ClientConfiguration; +import org.springframework.data.elasticsearch.client.elc.rest_client.RestClients; +import org.springframework.data.elasticsearch.config.ElasticsearchConfigurationSupport; +import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations; +import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter; +import org.springframework.util.Assert; + +/** + * Base class for a @{@link org.springframework.context.annotation.Configuration} class to set up the Elasticsearch + * connection using the {@link ReactiveElasticsearchClient}. This class exposes different parts of the setup as Spring + * beans. Deriving * classes must provide the {@link ClientConfiguration} to use.
+ * This class uses the Elasticsearch RestClient which was replaced b y the Rest5Client in Elasticsearch 9. It is still + * available here but deprecated. * + * + * @author Peter-Josef Meisch + * @since 4.4 + * @deprecated since 6.0 use {@link ReactiveElasticsearchConfiguration} + */ +@Deprecated(since = "6.0", forRemoval=true) +public abstract class ReactiveElasticsearchLegacyRestClientConfiguration extends ElasticsearchConfigurationSupport { + + /** + * Must be implemented by deriving classes to provide the {@link ClientConfiguration}. + * + * @return configuration, must not be {@literal null} + */ + @Bean(name = "elasticsearchClientConfiguration") + public abstract ClientConfiguration clientConfiguration(); + + /** + * Provides the underlying low level RestClient. + * + * @param clientConfiguration configuration for the client, must not be {@literal null} + * @return RestClient + */ + @Bean + public RestClient elasticsearchRestClient(ClientConfiguration clientConfiguration) { + + Assert.notNull(clientConfiguration, "clientConfiguration must not be null"); + + return RestClients.getRestClient(clientConfiguration); + } + + /** + * Provides the Elasticsearch transport to be used. The default implementation uses the {@link RestClient} bean and + * the {@link JsonpMapper} bean provided in this class. + * + * @return the {@link ElasticsearchTransport} + * @since 5.2 + */ + @Bean + public ElasticsearchTransport elasticsearchTransport(RestClient restClient, JsonpMapper jsonpMapper) { + + Assert.notNull(restClient, "restClient must not be null"); + Assert.notNull(jsonpMapper, "jsonpMapper must not be null"); + + return ElasticsearchClients.getElasticsearchTransport(restClient, ElasticsearchClients.REACTIVE_CLIENT, + transportOptions(), jsonpMapper); + } + + /** + * Provides the {@link ReactiveElasticsearchClient} instance used. + * + * @param transport the ElasticsearchTransport to use + * @return ReactiveElasticsearchClient instance. + */ + @Bean + public ReactiveElasticsearchClient reactiveElasticsearchClient(ElasticsearchTransport transport) { + + Assert.notNull(transport, "transport must not be null"); + + return ElasticsearchClients.createReactive(transport); + } + + /** + * Creates {@link ReactiveElasticsearchOperations}. + * + * @return never {@literal null}. + */ + @Bean(name = { "reactiveElasticsearchOperations", "reactiveElasticsearchTemplate" }) + public ReactiveElasticsearchOperations reactiveElasticsearchOperations(ElasticsearchConverter elasticsearchConverter, + ReactiveElasticsearchClient reactiveElasticsearchClient) { + + ReactiveElasticsearchTemplate template = new ReactiveElasticsearchTemplate(reactiveElasticsearchClient, + elasticsearchConverter); + template.setRefreshPolicy(refreshPolicy()); + + return template; + } + + /** + * Provides the JsonpMapper that is used in the {@link #elasticsearchTransport(RestClient, JsonpMapper)} method and + * exposes it as a bean. + * + * @return the {@link JsonpMapper} to use + * @since 5.2 + */ + @Bean + public JsonpMapper jsonpMapper() { + // we need to create our own objectMapper that keeps null values in order to provide the storeNullValue + // functionality. The one Elasticsearch would provide removes the nulls. We remove unwanted nulls before they get + // into this mapper, so we can safely keep them here. + var objectMapper = (new ObjectMapper()) + .configure(SerializationFeature.INDENT_OUTPUT, false) + .setSerializationInclusion(JsonInclude.Include.ALWAYS); + return new JacksonJsonpMapper(objectMapper); + } + + /** + * @return the options that should be added to every request. Must not be {@literal null} + */ + public TransportOptions transportOptions() { + return new RestClientOptions(RequestOptions.DEFAULT, false).toBuilder().build(); + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/rest5_client/Rest5Clients.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/rest5_client/Rest5Clients.java new file mode 100644 index 000000000..4d40e99af --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/rest5_client/Rest5Clients.java @@ -0,0 +1,274 @@ +package org.springframework.data.elasticsearch.client.elc.rest5_client; + +import co.elastic.clients.transport.TransportOptions; +import co.elastic.clients.transport.TransportUtils; +import co.elastic.clients.transport.rest5_client.Rest5ClientOptions; +import co.elastic.clients.transport.rest5_client.low_level.RequestOptions; +import co.elastic.clients.transport.rest5_client.low_level.Rest5Client; +import co.elastic.clients.transport.rest5_client.low_level.Rest5ClientBuilder; + +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.NoSuchAlgorithmException; +import java.time.Duration; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Function; + +import javax.net.ssl.SSLContext; + +import org.apache.hc.client5.http.config.ConnectionConfig; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.DefaultAuthenticationStrategy; +import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; +import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder; +import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager; +import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder; +import org.apache.hc.client5.http.impl.routing.DefaultProxyRoutePlanner; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.message.BasicHeader; +import org.apache.hc.core5.http.nio.ssl.BasicClientTlsStrategy; +import org.apache.hc.core5.util.Timeout; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; +import org.springframework.data.elasticsearch.client.ClientConfiguration; +import org.springframework.data.elasticsearch.support.HttpHeaders; +import org.springframework.data.elasticsearch.support.VersionInfo; +import org.springframework.util.Assert; + +/** + * Utility class containing the functions to create the Elasticsearch Rest5Client used from Elasticsearch 9 on. + * + * @since 6.0 + */ +public final class Rest5Clients { + + // values copied from Rest5ClientBuilder + public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 1000; + public static final int DEFAULT_SOCKET_TIMEOUT_MILLIS = 30000; + public static final int DEFAULT_RESPONSE_TIMEOUT_MILLIS = 0; // meaning infinite + public static final int DEFAULT_MAX_CONN_PER_ROUTE = 10; + public static final int DEFAULT_MAX_CONN_TOTAL = 30; + + private Rest5Clients() {} + + /** + * Creates a low level {@link Rest5Client} for the given configuration. + * + * @param clientConfiguration must not be {@literal null} + * @return the {@link Rest5Client} + */ + public static Rest5Client getRest5Client(ClientConfiguration clientConfiguration) { + return getRest5ClientBuilder(clientConfiguration).build(); + } + + private static Rest5ClientBuilder getRest5ClientBuilder(ClientConfiguration clientConfiguration) { + + HttpHost[] httpHosts = getHttpHosts(clientConfiguration); + Rest5ClientBuilder builder = Rest5Client.builder(httpHosts); + + if (clientConfiguration.getPathPrefix() != null) { + builder.setPathPrefix(clientConfiguration.getPathPrefix()); + } + + HttpHeaders headers = clientConfiguration.getDefaultHeaders(); + + if (!headers.isEmpty()) { + builder.setDefaultHeaders(toHeaderArray(headers)); + } + + // we need to provide our own HttpClient, as the Rest5ClientBuilder + // does not provide a callback for configuration the http client as the old RestClientBuilder. + var httpClient = createHttpClient(clientConfiguration); + builder.setHttpClient(httpClient); + + for (ClientConfiguration.ClientConfigurationCallback clientConfigurationCallback : clientConfiguration + .getClientConfigurers()) { + if (clientConfigurationCallback instanceof ElasticsearchRest5ClientConfigurationCallback configurationCallback) { + builder = configurationCallback.configure(builder); + } + } + + return builder; + } + + private static HttpHost @NonNull [] getHttpHosts(ClientConfiguration clientConfiguration) { + List hosts = clientConfiguration.getEndpoints(); + boolean useSsl = clientConfiguration.useSsl(); + return hosts.stream() + .map(it -> (useSsl ? "https" : "http") + "://" + it.getHostString() + ':' + it.getPort()) + .map(URI::create) + .map(HttpHost::create) + .toArray(HttpHost[]::new); + } + + private static Header[] toHeaderArray(HttpHeaders headers) { + return headers.entrySet().stream() // + .flatMap(entry -> entry.getValue().stream() // + .map(value -> new BasicHeader(entry.getKey(), value))) // + .toList().toArray(new Header[0]); + } + + // the basic logic to create the http client is copied from the Rest5ClientBuilder class, this is taken from the + // Elasticsearch code, as there is no public usable instance in that + private static CloseableHttpAsyncClient createHttpClient(ClientConfiguration clientConfiguration) { + + var requestConfigBuilder = RequestConfig.custom(); + var connectionConfigBuilder = ConnectionConfig.custom(); + + Duration connectTimeout = clientConfiguration.getConnectTimeout(); + + if (!connectTimeout.isNegative()) { + connectionConfigBuilder.setConnectTimeout( + Timeout.of(Math.toIntExact(connectTimeout.toMillis()), TimeUnit.MILLISECONDS)); + } + + Duration socketTimeout = clientConfiguration.getSocketTimeout(); + + if (!socketTimeout.isNegative()) { + var soTimeout = Timeout.of(Math.toIntExact(socketTimeout.toMillis()), TimeUnit.MILLISECONDS); + connectionConfigBuilder.setSocketTimeout(soTimeout); + requestConfigBuilder.setConnectionRequestTimeout(soTimeout); + } else { + connectionConfigBuilder.setSocketTimeout(Timeout.of(DEFAULT_SOCKET_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); + requestConfigBuilder + .setConnectionRequestTimeout(Timeout.of(DEFAULT_RESPONSE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); + } + + try { + SSLContext sslContext = clientConfiguration.getCaFingerprint().isPresent() + ? TransportUtils.sslContextFromCaFingerprint(clientConfiguration.getCaFingerprint().get()) + : (clientConfiguration.getSslContext().isPresent() + ? clientConfiguration.getSslContext().get() + : SSLContext.getDefault()); + + ConnectionConfig connectionConfig = connectionConfigBuilder.build(); + + PoolingAsyncClientConnectionManager defaultConnectionManager = PoolingAsyncClientConnectionManagerBuilder.create() + .setDefaultConnectionConfig(connectionConfig) + .setMaxConnPerRoute(DEFAULT_MAX_CONN_PER_ROUTE) + .setMaxConnTotal(DEFAULT_MAX_CONN_TOTAL) + .setTlsStrategy(new BasicClientTlsStrategy(sslContext)) + .build(); + + var requestConfig = requestConfigBuilder.build(); + + var immutableRefToHttpClientBuilder = new Object() { + HttpAsyncClientBuilder httpClientBuilder = HttpAsyncClientBuilder.create() + .setDefaultRequestConfig(requestConfig) + .setConnectionManager(defaultConnectionManager) + .setUserAgent(VersionInfo.clientVersions()) + .setTargetAuthenticationStrategy(new DefaultAuthenticationStrategy()) + .setThreadFactory(new RestClientThreadFactory()); + }; + + clientConfiguration.getProxy().ifPresent(proxy -> { + try { + var proxyRoutePlanner = new DefaultProxyRoutePlanner(HttpHost.create(proxy)); + immutableRefToHttpClientBuilder.httpClientBuilder.setRoutePlanner(proxyRoutePlanner); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + }); + + immutableRefToHttpClientBuilder.httpClientBuilder.addRequestInterceptorFirst((request, entity, context) -> { + clientConfiguration.getHeadersSupplier().get().forEach((header, values) -> { + // The accept and content-type headers are already put on the request, despite this being the first + // interceptor. + if ("Accept".equalsIgnoreCase(header) || " Content-Type".equalsIgnoreCase(header)) { + request.removeHeaders(header); + } + values.forEach(value -> request.addHeader(header, value)); + }); + }); + + for (ClientConfiguration.ClientConfigurationCallback clientConfigurer : clientConfiguration + .getClientConfigurers()) { + if (clientConfigurer instanceof ElasticsearchHttpClientConfigurationCallback httpClientConfigurer) { + immutableRefToHttpClientBuilder.httpClientBuilder = httpClientConfigurer.configure(immutableRefToHttpClientBuilder.httpClientBuilder); + } + } + + return immutableRefToHttpClientBuilder.httpClientBuilder.build(); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("could not create the default ssl context", e); + } + } + + /* + * Copied from the Elasticsearch code as this class is not public there. + */ + private static class RestClientThreadFactory implements ThreadFactory { + private static final AtomicLong CLIENT_THREAD_POOL_ID_GENERATOR = new AtomicLong(); + private final long clientThreadPoolId; + private final AtomicLong clientThreadId; + + private RestClientThreadFactory() { + this.clientThreadPoolId = CLIENT_THREAD_POOL_ID_GENERATOR.getAndIncrement(); + this.clientThreadId = new AtomicLong(); + } + + public Thread newThread(Runnable runnable) { + return new Thread(runnable, String.format(Locale.ROOT, "elasticsearch-rest-client-%d-thread-%d", + this.clientThreadPoolId, this.clientThreadId.incrementAndGet())); + } + } + + /** + * {@link org.springframework.data.elasticsearch.client.ClientConfiguration.ClientConfigurationCallback} to configure + * the Elasticsearch Rest5Client's Http client with a {@link HttpAsyncClientBuilder} + * + * @since 6.0 + */ + public interface ElasticsearchHttpClientConfigurationCallback + extends ClientConfiguration.ClientConfigurationCallback { + + static Rest5Clients.ElasticsearchHttpClientConfigurationCallback from( + Function httpClientBuilderCallback) { + + Assert.notNull(httpClientBuilderCallback, "httpClientBuilderCallback must not be null"); + + return httpClientBuilderCallback::apply; + } + } + + /** + * {@link ClientConfiguration.ClientConfigurationCallback} to configure the Rest5Client client with a + * {@link Rest5ClientBuilder} + * + * @since 6.0 + */ + public interface ElasticsearchRest5ClientConfigurationCallback + extends ClientConfiguration.ClientConfigurationCallback { + + static ElasticsearchRest5ClientConfigurationCallback from( + Function rest5ClientBuilderCallback) { + + Assert.notNull(rest5ClientBuilderCallback, "rest5ClientBuilderCallback must not be null"); + + return rest5ClientBuilderCallback::apply; + } + } + + public static Rest5ClientOptions.Builder getRest5ClientOptionsBuilder(@Nullable TransportOptions transportOptions) { + + if (transportOptions instanceof Rest5ClientOptions rest5ClientOptions) { + return rest5ClientOptions.toBuilder(); + } + + var builder = new Rest5ClientOptions.Builder(RequestOptions.DEFAULT.toBuilder()); + + if (transportOptions != null) { + transportOptions.headers().forEach(header -> builder.addHeader(header.getKey(), header.getValue())); + transportOptions.queryParameters().forEach(builder::setParameter); + builder.onWarnings(transportOptions.onWarnings()); + } + + return builder; + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/rest5_client/package-info.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/rest5_client/package-info.java new file mode 100644 index 000000000..af2aff84c --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/rest5_client/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 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. + */ + +/** + * This package contains related to the new (from Elasticsearch 9 on) Rest5Client. There are also classes copied over from Elasticsearch in order to have a Rest5ClientBuilder that allows to configure the http client. + */ +@org.jspecify.annotations.NullMarked +package org.springframework.data.elasticsearch.client.elc.rest5_client; diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/rest_client/RestClients.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/rest_client/RestClients.java new file mode 100644 index 000000000..5c51f8c31 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/rest_client/RestClients.java @@ -0,0 +1,195 @@ +package org.springframework.data.elasticsearch.client.elc.rest_client; + +import co.elastic.clients.transport.TransportOptions; +import co.elastic.clients.transport.TransportUtils; +import co.elastic.clients.transport.rest_client.RestClientOptions; + +import java.net.InetSocketAddress; +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.apache.http.HttpHost; +import org.apache.http.HttpRequest; +import org.apache.http.HttpRequestInterceptor; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.impl.nio.client.HttpAsyncClientBuilder; +import org.apache.http.message.BasicHeader; +import org.apache.http.protocol.HttpContext; +import org.elasticsearch.client.RequestOptions; +import org.elasticsearch.client.RestClient; +import org.elasticsearch.client.RestClientBuilder; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; +import org.springframework.data.elasticsearch.client.ClientConfiguration; +import org.springframework.data.elasticsearch.client.elc.ElasticsearchClients; +import org.springframework.data.elasticsearch.support.HttpHeaders; +import org.springframework.util.Assert; + +/** + * Utility class containing the functions to create the Elasticsearch RestClient used up to Elasticsearch 9. + * + * @since 6.0 + * @deprecated since 6.0, use the new Rest5Client the code for that is in the package ../rest_client. + */ +@Deprecated(since = "6.0", forRemoval = true) +public final class RestClients { + + /** + * Creates a low level {@link RestClient} for the given configuration. + * + * @param clientConfiguration must not be {@literal null} + * @return the {@link RestClient} + */ + public static RestClient getRestClient(ClientConfiguration clientConfiguration) { + return getRestClientBuilder(clientConfiguration).build(); + } + + private static RestClientBuilder getRestClientBuilder(ClientConfiguration clientConfiguration) { + HttpHost[] httpHosts = getHttpHosts(clientConfiguration); + RestClientBuilder builder = RestClient.builder(httpHosts); + + if (clientConfiguration.getPathPrefix() != null) { + builder.setPathPrefix(clientConfiguration.getPathPrefix()); + } + + HttpHeaders headers = clientConfiguration.getDefaultHeaders(); + + if (!headers.isEmpty()) { + builder.setDefaultHeaders(toHeaderArray(headers)); + } + + builder.setHttpClientConfigCallback(clientBuilder -> { + if (clientConfiguration.getCaFingerprint().isPresent()) { + clientBuilder + .setSSLContext(TransportUtils.sslContextFromCaFingerprint(clientConfiguration.getCaFingerprint().get())); + } + clientConfiguration.getSslContext().ifPresent(clientBuilder::setSSLContext); + clientConfiguration.getHostNameVerifier().ifPresent(clientBuilder::setSSLHostnameVerifier); + clientBuilder.addInterceptorLast(new CustomHeaderInjector(clientConfiguration.getHeadersSupplier())); + + RequestConfig.Builder requestConfigBuilder = RequestConfig.custom(); + Duration connectTimeout = clientConfiguration.getConnectTimeout(); + + if (!connectTimeout.isNegative()) { + requestConfigBuilder.setConnectTimeout(Math.toIntExact(connectTimeout.toMillis())); + } + + Duration socketTimeout = clientConfiguration.getSocketTimeout(); + + if (!socketTimeout.isNegative()) { + requestConfigBuilder.setSocketTimeout(Math.toIntExact(socketTimeout.toMillis())); + requestConfigBuilder.setConnectionRequestTimeout(Math.toIntExact(socketTimeout.toMillis())); + } + + clientBuilder.setDefaultRequestConfig(requestConfigBuilder.build()); + + clientConfiguration.getProxy().map(HttpHost::create).ifPresent(clientBuilder::setProxy); + + for (ClientConfiguration.ClientConfigurationCallback clientConfigurer : clientConfiguration + .getClientConfigurers()) { + if (clientConfigurer instanceof RestClients.ElasticsearchHttpClientConfigurationCallback restClientConfigurationCallback) { + clientBuilder = restClientConfigurationCallback.configure(clientBuilder); + } + } + + return clientBuilder; + }); + + for (ClientConfiguration.ClientConfigurationCallback clientConfigurationCallback : clientConfiguration + .getClientConfigurers()) { + if (clientConfigurationCallback instanceof ElasticsearchRestClientConfigurationCallback configurationCallback) { + builder = configurationCallback.configure(builder); + } + } + return builder; + } + + private static HttpHost @NonNull [] getHttpHosts(ClientConfiguration clientConfiguration) { + List hosts = clientConfiguration.getEndpoints(); + boolean useSsl = clientConfiguration.useSsl(); + return hosts.stream() + .map(it -> (useSsl ? "https" : "http") + "://" + it.getHostString() + ':' + it.getPort()) + .map(HttpHost::create).toArray(HttpHost[]::new); + } + + private static org.apache.http.Header[] toHeaderArray(HttpHeaders headers) { + return headers.entrySet().stream() // + .flatMap(entry -> entry.getValue().stream() // + .map(value -> new BasicHeader(entry.getKey(), value))) // + .toArray(org.apache.http.Header[]::new); + } + + /** + * Interceptor to inject custom supplied headers. + * + * @since 4.4 + */ + record CustomHeaderInjector(Supplier headersSupplier) implements HttpRequestInterceptor { + + @Override + public void process(HttpRequest request, HttpContext context) { + HttpHeaders httpHeaders = headersSupplier.get(); + + if (httpHeaders != null && !httpHeaders.isEmpty()) { + Arrays.stream(toHeaderArray(httpHeaders)).forEach(request::addHeader); + } + } + } + + /** + * {@link org.springframework.data.elasticsearch.client.ClientConfiguration.ClientConfigurationCallback} to configure + * the Elasticsearch RestClient's Http client with a {@link HttpAsyncClientBuilder} + * + * @since 4.4 + */ + public interface ElasticsearchHttpClientConfigurationCallback + extends ClientConfiguration.ClientConfigurationCallback { + + static RestClients.ElasticsearchHttpClientConfigurationCallback from( + Function httpClientBuilderCallback) { + + Assert.notNull(httpClientBuilderCallback, "httpClientBuilderCallback must not be null"); + + return httpClientBuilderCallback::apply; + } + } + + /** + * {@link org.springframework.data.elasticsearch.client.ClientConfiguration.ClientConfigurationCallback} to configure + * the RestClient client with a {@link RestClientBuilder} + * + * @since 5.0 + */ + public interface ElasticsearchRestClientConfigurationCallback + extends ClientConfiguration.ClientConfigurationCallback { + + static ElasticsearchRestClientConfigurationCallback from( + Function restClientBuilderCallback) { + + Assert.notNull(restClientBuilderCallback, "restClientBuilderCallback must not be null"); + + return restClientBuilderCallback::apply; + } + } + + public static RestClientOptions.Builder getRestClientOptionsBuilder(@Nullable TransportOptions transportOptions) { + + if (transportOptions instanceof RestClientOptions restClientOptions) { + return restClientOptions.toBuilder(); + } + + var builder = new RestClientOptions.Builder(RequestOptions.DEFAULT.toBuilder()); + + if (transportOptions != null) { + transportOptions.headers().forEach(header -> builder.addHeader(header.getKey(), header.getValue())); + transportOptions.queryParameters().forEach(builder::setParameter); + builder.onWarnings(transportOptions.onWarnings()); + } + + return builder; + } + +} diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/rest_client/package-info.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/rest_client/package-info.java new file mode 100644 index 000000000..bc34f799b --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/rest_client/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright 2022-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. + */ + +/** + * This package contains related to the old (up to Elasticsearch 9) RestClient. + */ +@Deprecated(since = "6.0", forRemoval=true) +@org.jspecify.annotations.NullMarked +package org.springframework.data.elasticsearch.client.elc.rest_client; diff --git a/src/main/java/org/springframework/data/elasticsearch/support/VersionInfo.java b/src/main/java/org/springframework/data/elasticsearch/support/VersionInfo.java index d62ea73f8..cf5e0742f 100644 --- a/src/main/java/org/springframework/data/elasticsearch/support/VersionInfo.java +++ b/src/main/java/org/springframework/data/elasticsearch/support/VersionInfo.java @@ -92,6 +92,17 @@ public final class VersionInfo { } } + /** + * @return a String to use in a header, containing the versions of Spring Data Elasticsearch and the Elasticsearch + * library + * @since 6.0 + */ + public static String clientVersions() { + return String.format("spring-data-elasticsearch %s / elasticsearch client %s", + versionProperties.getProperty(VERSION_SPRING_DATA_ELASTICSEARCH), + versionProperties.getProperty(VERSION_ELASTICSEARCH_CLIENT)); + } + /** * gets the version properties from the classpath resource. * diff --git a/src/test/java/org/springframework/data/elasticsearch/client/RestClientsTest.java b/src/test/java/org/springframework/data/elasticsearch/client/RestClientsTest.java index f8c9b6e49..62c983a4b 100644 --- a/src/test/java/org/springframework/data/elasticsearch/client/RestClientsTest.java +++ b/src/test/java/org/springframework/data/elasticsearch/client/RestClientsTest.java @@ -1,21 +1,22 @@ /* - * Copyright 2019-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. - */ +* Copyright 2019-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.data.elasticsearch.client; import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.*; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; import static io.specto.hoverfly.junit.dsl.HoverflyDsl.*; import static io.specto.hoverfly.junit.verification.HoverflyVerifications.*; @@ -40,10 +41,13 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.springframework.data.elasticsearch.client.elc.ElasticsearchClients; import org.springframework.data.elasticsearch.client.elc.ReactiveElasticsearchClient; +import org.springframework.data.elasticsearch.client.elc.rest5_client.Rest5Clients; +import org.springframework.data.elasticsearch.client.elc.rest_client.RestClients; import org.springframework.data.elasticsearch.support.HttpHeaders; import com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.common.ConsoleNotifier; import com.github.tomakehurst.wiremock.matching.AnythingPattern; import com.github.tomakehurst.wiremock.matching.EqualToPattern; import com.github.tomakehurst.wiremock.stubbing.StubMapping; @@ -115,25 +119,38 @@ public class RestClientsTest { return httpHeaders; }); - if (clientUnderTestFactory instanceof ELCUnderTestFactory) { + if (clientUnderTestFactory instanceof ELCRest5ClientUnderTestFactory) { configurationBuilder.withClientConfigurer( - ElasticsearchClients.ElasticsearchHttpClientConfigurationCallback.from(httpClientBuilder -> { + Rest5Clients.ElasticsearchHttpClientConfigurationCallback.from(httpClientBuilder -> { httpClientConfigurerCount.incrementAndGet(); return httpClientBuilder; })); configurationBuilder.withClientConfigurer( - ElasticsearchClients.ElasticsearchRestClientConfigurationCallback.from(restClientBuilder -> { + Rest5Clients.ElasticsearchRest5ClientConfigurationCallback.from(rest5ClientBuilder -> { + restClientConfigurerCount.incrementAndGet(); + return rest5ClientBuilder; + })); + + } else if (clientUnderTestFactory instanceof ELCRestClientUnderTestFactory) { + configurationBuilder.withClientConfigurer( + RestClients.ElasticsearchHttpClientConfigurationCallback.from(httpClientBuilder -> { + httpClientConfigurerCount.incrementAndGet(); + return httpClientBuilder; + })); + configurationBuilder.withClientConfigurer( + RestClients.ElasticsearchRestClientConfigurationCallback.from(restClientBuilder -> { restClientConfigurerCount.incrementAndGet(); return restClientBuilder; })); + } else if (clientUnderTestFactory instanceof ReactiveELCUnderTestFactory) { configurationBuilder - .withClientConfigurer(ElasticsearchClients.ElasticsearchHttpClientConfigurationCallback.from(webClient -> { + .withClientConfigurer(RestClients.ElasticsearchHttpClientConfigurationCallback.from(webClient -> { httpClientConfigurerCount.incrementAndGet(); return webClient; })); configurationBuilder.withClientConfigurer( - ElasticsearchClients.ElasticsearchRestClientConfigurationCallback.from(restClientBuilder -> { + RestClients.ElasticsearchRestClientConfigurationCallback.from(restClientBuilder -> { restClientConfigurerCount.incrementAndGet(); return restClientBuilder; })); @@ -155,8 +172,9 @@ public class RestClientsTest { .withHeader("def2", new EqualToPattern("def2-1")) // .withHeader("supplied", new EqualToPattern("val0")) // // on the first call Elasticsearch does the version check and thus already increments the counter - .withHeader("supplied", new EqualToPattern("val" + (i))) // - ); + .withHeader("supplied", new EqualToPattern("val" + i)) // + .withHeader("supplied", including("val0", "val" + i))); + ; } assertThat(httpClientConfigurerCount).hasValue(1); @@ -166,8 +184,8 @@ public class RestClientsTest { @ParameterizedTest // #2088 @MethodSource("clientUnderTestFactorySource") - @DisplayName("should set compatibility headers") - void shouldSetCompatibilityHeaders(ClientUnderTestFactory clientUnderTestFactory) { + @DisplayName("should set explicit compatibility headers") + void shouldSetExplicitCompatibilityHeaders(ClientUnderTestFactory clientUnderTestFactory) { wireMockServer(server -> { @@ -190,7 +208,8 @@ public class RestClientsTest { } """ // , 201) // - .withHeader("Content-Type", "application/vnd.elasticsearch+json;compatible-with=7") // + .withHeader("Content-Type", + "application/vnd.elasticsearch+json;compatible-with=7") // .withHeader("X-Elastic-Product", "Elasticsearch"))); ClientConfigurationBuilder configurationBuilder = new ClientConfigurationBuilder(); @@ -198,8 +217,8 @@ public class RestClientsTest { .connectedTo("localhost:" + server.port()) // .withHeaders(() -> { HttpHeaders defaultCompatibilityHeaders = new HttpHeaders(); - defaultCompatibilityHeaders.add("Accept", "application/vnd.elasticsearch+json;compatible-with=7"); - defaultCompatibilityHeaders.add("Content-Type", "application/vnd.elasticsearch+json;compatible-with=7"); + defaultCompatibilityHeaders.add("Accept", "application/vnd.elasticsearch+json; compatible-with=7"); + defaultCompatibilityHeaders.add("Content-Type", "application/vnd.elasticsearch+json; compatible-with=7"); return defaultCompatibilityHeaders; }); @@ -214,11 +233,66 @@ public class RestClientsTest { } } - clientUnderTest.index(new Foo("42")); + clientUnderTest.index(new Foo("42")); verify(putRequestedFor(urlMatching(urlPattern)) // - .withHeader("Accept", new EqualToPattern("application/vnd.elasticsearch+json;compatible-with=7")) // - .withHeader("Content-Type", new EqualToPattern("application/vnd.elasticsearch+json;compatible-with=7")) // + .withHeader("Accept", new EqualToPattern("application/vnd.elasticsearch+json; compatible-with=7")) + .withHeader("Content-Type", new EqualToPattern("application/vnd.elasticsearch+json; compatible-with=7"))); + }); + } + + @ParameterizedTest + @MethodSource("clientUnderTestFactorySource") + @DisplayName("should set implicit compatibility headers") + void shouldSetImplicitCompatibilityHeaders(ClientUnderTestFactory clientUnderTestFactory) { + + wireMockServer(server -> { + + String urlPattern = "^/index/_doc/42(\\?.*)?$"; + var elasticsearchMajorVersion = clientUnderTestFactory.getElasticsearchMajorVersion(); + stubFor(put(urlMatching(urlPattern)) // + .willReturn(jsonResponse(""" + { + "_id": "42", + "_index": "test", + "_primary_term": 1, + "_seq_no": 0, + "_shards": { + "failed": 0, + "successful": 1, + "total": 2 + }, + "_type": "_doc", + "_version": 1, + "result": "created" + } + """ // + , 201) // + .withHeader("Content-Type", + "application/vnd.elasticsearch+json; compatible-with=" + elasticsearchMajorVersion) // + .withHeader("X-Elastic-Product", "Elasticsearch"))); + + ClientConfigurationBuilder configurationBuilder = new ClientConfigurationBuilder(); + configurationBuilder.connectedTo("localhost:" + server.port()); + + ClientConfiguration clientConfiguration = configurationBuilder.build(); + ClientUnderTest clientUnderTest = clientUnderTestFactory.create(clientConfiguration); + + class Foo { + public final String id; + + Foo(String id) { + this.id = id; + } + } + + clientUnderTest.index(new Foo("42")); + + verify(putRequestedFor(urlMatching(urlPattern)) // + .withHeader("Accept", + new EqualToPattern("application/vnd.elasticsearch+json; compatible-with=" + elasticsearchMajorVersion)) // + .withHeader("Content-Type", + new EqualToPattern("application/vnd.elasticsearch+json; compatible-with=" + elasticsearchMajorVersion)) // ); }); } @@ -280,6 +354,7 @@ public class RestClientsTest { private void wireMockServer(WiremockConsumer consumer) { WireMockServer wireMockServer = new WireMockServer(options() // .dynamicPort() // + // .notifier(new ConsoleNotifier(true)) // for debugging output .usingFilesUnderDirectory("src/test/resources/wiremock-mappings")); // needed, otherwise Wiremock goes to // test/resources/mappings try { @@ -295,7 +370,7 @@ public class RestClientsTest { } /** - * The client to be tested. Abstraction to be able to test reactive and non-reactive clients. + * The client to be tested. Abstraction to be able to test reactive and non-reactive clients in different versions. */ interface ClientUnderTest { /** @@ -332,16 +407,19 @@ public class RestClientsTest { protected Integer getExpectedRestClientConfigCalls() { return 0; } + + protected abstract int getElasticsearchMajorVersion(); } /** - * {@link ClientUnderTestFactory} implementation for the {@link co.elastic.clients.elasticsearch.ElasticsearchClient}. + * {@link ClientUnderTestFactory} implementation for the {@link co.elastic.clients.elasticsearch.ElasticsearchClient} + * using the Rest5_Client. */ - static class ELCUnderTestFactory extends ClientUnderTestFactory { + static class ELCRest5ClientUnderTestFactory extends ClientUnderTestFactory { @Override protected String getDisplayName() { - return "ElasticsearchClient"; + return "ElasticsearchRest5Client"; } @Override @@ -349,6 +427,11 @@ public class RestClientsTest { return 1; } + @Override + protected int getElasticsearchMajorVersion() { + return 9; + } + @Override ClientUnderTest create(ClientConfiguration clientConfiguration) { @@ -372,6 +455,51 @@ public class RestClientsTest { } } + /** + * {@link ClientUnderTestFactory} implementation for the {@link co.elastic.clients.elasticsearch.ElasticsearchClient} + * using the old Rest_Client. + */ + static class ELCRestClientUnderTestFactory extends ClientUnderTestFactory { + + @Override + protected String getDisplayName() { + return "ElasticsearchRestClient"; + } + + @Override + protected Integer getExpectedRestClientConfigCalls() { + return 1; + } + + @Override + protected int getElasticsearchMajorVersion() { + return 9; + } + + @Override + ClientUnderTest create(ClientConfiguration clientConfiguration) { + + var restClient = RestClients.getRestClient(clientConfiguration); + ElasticsearchClient client = ElasticsearchClients.createImperative(restClient); + return new ClientUnderTest() { + @Override + public boolean ping() throws Exception { + return client.ping().value(); + } + + @Override + public boolean usesInitialRequest() { + return false; + } + + @Override + public void index(T entity) throws IOException { + client.index(ir -> ir.index("index").id("42").document(entity)); + } + }; + } + } + /** * {@link ClientUnderTestFactory} implementation for the {@link ReactiveElasticsearchClient}. */ @@ -387,6 +515,11 @@ public class RestClientsTest { return 1; } + @Override + protected int getElasticsearchMajorVersion() { + return 9; + } + @Override ClientUnderTest create(ClientConfiguration clientConfiguration) { @@ -416,8 +549,9 @@ public class RestClientsTest { * @return stream of factories */ static Stream clientUnderTestFactorySource() { - return Stream.of( // - new ELCUnderTestFactory(), // + return Stream.of( + new ELCRestClientUnderTestFactory(), + new ELCRest5ClientUnderTestFactory(), new ReactiveELCUnderTestFactory()); } } diff --git a/src/test/java/org/springframework/data/elasticsearch/client/elc/DevTests.java b/src/test/java/org/springframework/data/elasticsearch/client/elc/DevTests.java index 25308b569..c0f9b085b 100644 --- a/src/test/java/org/springframework/data/elasticsearch/client/elc/DevTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/client/elc/DevTests.java @@ -37,6 +37,9 @@ import co.elastic.clients.json.jackson.JacksonJsonpMapper; import co.elastic.clients.transport.ElasticsearchTransport; import co.elastic.clients.transport.TransportOptions; import co.elastic.clients.transport.rest_client.RestClientOptions; +import org.elasticsearch.client.RestClient; +import org.springframework.data.elasticsearch.client.elc.rest5_client.Rest5Clients; +import org.springframework.data.elasticsearch.client.elc.rest_client.RestClients; import reactor.core.publisher.Mono; import java.io.IOException; @@ -85,7 +88,7 @@ public class DevTests { private final ReactiveElasticsearchClient reactiveElasticsearchClient = ElasticsearchClients .createReactive(clientConfiguration(), transportOptions); private final ElasticsearchClient imperativeElasticsearchClient = ElasticsearchClients - .createImperative(ElasticsearchClients.getRestClient(clientConfiguration()), transportOptions, jsonpMapper); + .createImperative(Rest5Clients.getRest5Client(clientConfiguration()), transportOptions, jsonpMapper); @Test void someTest() throws IOException { diff --git a/src/test/java/org/springframework/data/elasticsearch/client/elc/ELCWiremockTests.java b/src/test/java/org/springframework/data/elasticsearch/client/elc/rest5_client/ELCRest5ClientWiremockTests.java similarity index 93% rename from src/test/java/org/springframework/data/elasticsearch/client/elc/ELCWiremockTests.java rename to src/test/java/org/springframework/data/elasticsearch/client/elc/rest5_client/ELCRest5ClientWiremockTests.java index 875fd752e..1bead7a8b 100644 --- a/src/test/java/org/springframework/data/elasticsearch/client/elc/ELCWiremockTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/client/elc/rest5_client/ELCRest5ClientWiremockTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.elasticsearch.client.elc; +package org.springframework.data.elasticsearch.client.elc.rest5_client; import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.*; @@ -29,6 +29,7 @@ import org.springframework.data.annotation.Id; import org.springframework.data.elasticsearch.annotations.Document; import org.springframework.data.elasticsearch.annotations.Field; import org.springframework.data.elasticsearch.client.ClientConfiguration; +import org.springframework.data.elasticsearch.client.elc.ElasticsearchConfiguration; import org.springframework.data.elasticsearch.core.ElasticsearchOperations; import org.springframework.test.context.junit.jupiter.SpringExtension; @@ -41,7 +42,7 @@ import com.github.tomakehurst.wiremock.junit5.WireMockExtension; */ @SuppressWarnings("UastIncorrectHttpHeaderInspection") @ExtendWith(SpringExtension.class) -public class ELCWiremockTests { +public class ELCRest5ClientWiremockTests { @RegisterExtension static WireMockExtension wireMock = WireMockExtension.newInstance() .options(wireMockConfig() @@ -69,7 +70,7 @@ public class ELCWiremockTests { wireMock.stubFor(put(urlPathEqualTo("/null-fields/_doc/42")) .withRequestBody(equalToJson(""" { - "_class": "org.springframework.data.elasticsearch.client.elc.ELCWiremockTests$EntityWithNullFields", + "_class": "org.springframework.data.elasticsearch.client.elc.rest5_client.ELCRest5ClientWiremockTests$EntityWithNullFields", "id": "42", "field1": null } diff --git a/src/test/java/org/springframework/data/elasticsearch/client/elc/rest_client/ELCRestClientWiremockTests.java b/src/test/java/org/springframework/data/elasticsearch/client/elc/rest_client/ELCRestClientWiremockTests.java new file mode 100644 index 000000000..22dd2a08a --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/client/elc/rest_client/ELCRestClientWiremockTests.java @@ -0,0 +1,145 @@ +/* + * Copyright 2024-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.data.elasticsearch.client.elc.rest_client; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.*; + +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.annotation.Id; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.client.ClientConfiguration; +import org.springframework.data.elasticsearch.client.elc.ElasticsearchConfiguration; +import org.springframework.data.elasticsearch.client.elc.ElasticsearchLegacyRestClientConfiguration; +import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; + +/** + * Tests that need to check the data produced by the Elasticsearch client + * + * @author Peter-Josef Meisch + */ +@SuppressWarnings("UastIncorrectHttpHeaderInspection") +@Deprecated(since = "6.0", forRemoval = true) +@ExtendWith(SpringExtension.class) +public class ELCRestClientWiremockTests { + + @RegisterExtension static WireMockExtension wireMock = WireMockExtension.newInstance() + .options(wireMockConfig() + .dynamicPort() + // needed, otherwise Wiremock goes to test/resources/mappings + .usingFilesUnderDirectory("src/test/resources/wiremock-mappings")) + .build(); + + @Configuration + static class Config extends ElasticsearchLegacyRestClientConfiguration { + @Override + public ClientConfiguration clientConfiguration() { + return ClientConfiguration.builder() + .connectedTo("localhost:" + wireMock.getPort()) + .build(); + } + } + + @Autowired ElasticsearchOperations operations; + + @Test // #2839 + @DisplayName("should store null values if configured") + void shouldStoreNullValuesIfConfigured() { + + wireMock.stubFor(put(urlPathEqualTo("/null-fields/_doc/42")) + .withRequestBody(equalToJson(""" + { + "_class": "org.springframework.data.elasticsearch.client.elc.rest_client.ELCRestClientWiremockTests$EntityWithNullFields", + "id": "42", + "field1": null + } + """)) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("X-elastic-product", "Elasticsearch") + .withHeader("content-type", "application/vnd.elasticsearch+json;compatible-with=8") + .withBody(""" + { + "_index": "null-fields", + "_id": "42", + "_version": 1, + "result": "created", + "forced_refresh": true, + "_shards": { + "total": 2, + "successful": 1, + "failed": 0 + }, + "_seq_no": 1, + "_primary_term": 1 + } + """))); + + var entity = new EntityWithNullFields(); + entity.setId("42"); + + operations.save(entity); + // no need to assert anything, if the field1:null is not sent, we run into a 404 error + } + + @Document(indexName = "null-fields") + static class EntityWithNullFields { + @Nullable + @Id private String id; + @Nullable + @Field(storeNullValue = true) private String field1; + @Nullable + @Field private String field2; + + @Nullable + public String getId() { + return id; + } + + public void setId(@Nullable String id) { + this.id = id; + } + + @Nullable + public String getField1() { + return field1; + } + + public void setField1(@Nullable String field1) { + this.field1 = field1; + } + + @Nullable + public String getField2() { + return field2; + } + + public void setField2(@Nullable String field2) { + this.field2 = field2; + } + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/client/elc/rest_client/package-info.java b/src/test/java/org/springframework/data/elasticsearch/client/elc/rest_client/package-info.java new file mode 100644 index 000000000..98699f58e --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/client/elc/rest_client/package-info.java @@ -0,0 +1,2 @@ +@Deprecated(since = "6.0", forRemoval=true) +package org.springframework.data.elasticsearch.client.elc.rest_client; diff --git a/src/test/java/org/springframework/data/elasticsearch/config/configuration/ElasticsearchConfigurationELCTests.java b/src/test/java/org/springframework/data/elasticsearch/config/configuration/ElasticsearchConfigurationELCTests.java index 973f129b3..118ba7c62 100644 --- a/src/test/java/org/springframework/data/elasticsearch/config/configuration/ElasticsearchConfigurationELCTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/config/configuration/ElasticsearchConfigurationELCTests.java @@ -19,7 +19,8 @@ import static org.assertj.core.api.Assertions.*; import co.elastic.clients.elasticsearch.ElasticsearchClient; -import org.elasticsearch.client.RestClient; +import co.elastic.clients.transport.rest5_client.low_level.Rest5Client; +import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -50,7 +51,7 @@ public class ElasticsearchConfigurationELCTests { considerNestedRepositories = true) static class Config extends ElasticsearchConfiguration { @Override - public ClientConfiguration clientConfiguration() { + public @NonNull ClientConfiguration clientConfiguration() { return ClientConfiguration.builder() // .connectedTo("localhost:9200") // .build(); @@ -61,7 +62,7 @@ public class ElasticsearchConfigurationELCTests { * using a repository with an entity that is set to createIndex = false as we have no elastic running for this test * and just check that all the necessary beans are created. */ - @Autowired private RestClient restClient; + @Autowired private Rest5Client rest5Client; @Autowired private ElasticsearchClient elasticsearchClient; @Autowired private ElasticsearchOperations elasticsearchOperations; @@ -69,7 +70,7 @@ public class ElasticsearchConfigurationELCTests { @Test public void providesRequiredBeans() { - assertThat(restClient).isNotNull(); + assertThat(rest5Client).isNotNull(); assertThat(elasticsearchClient).isNotNull(); assertThat(elasticsearchOperations).isNotNull(); assertThat(repository).isNotNull(); diff --git a/src/test/java/org/springframework/data/elasticsearch/config/configuration/ElasticsearchLegacyRestClientConfigurationELCTests.java b/src/test/java/org/springframework/data/elasticsearch/config/configuration/ElasticsearchLegacyRestClientConfigurationELCTests.java new file mode 100644 index 000000000..4af7fdc4a --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/config/configuration/ElasticsearchLegacyRestClientConfigurationELCTests.java @@ -0,0 +1,88 @@ +/* + * Copyright 2021-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.data.elasticsearch.config.configuration; + +import static org.assertj.core.api.Assertions.*; + +import co.elastic.clients.elasticsearch.ElasticsearchClient; + +import org.elasticsearch.client.RestClient; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.annotation.Id; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.client.ClientConfiguration; +import org.springframework.data.elasticsearch.client.elc.ElasticsearchConfiguration; +import org.springframework.data.elasticsearch.client.elc.ElasticsearchLegacyRestClientConfiguration; +import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; +import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +/** + * Tests for {@link ElasticsearchConfiguration}. + * + * @author Peter-Josef Meisch + * @since 4.4 + */ +@ExtendWith(SpringExtension.class) +@ContextConfiguration +public class ElasticsearchLegacyRestClientConfigurationELCTests { + + @Configuration + @EnableElasticsearchRepositories(basePackages = { "org.springframework.data.elasticsearch.config.configuration" }, + considerNestedRepositories = true) + static class Config extends ElasticsearchLegacyRestClientConfiguration { + @Override + public @NonNull ClientConfiguration clientConfiguration() { + return ClientConfiguration.builder() // + .connectedTo("localhost:9200") // + .build(); + } + } + + /* + * using a repository with an entity that is set to createIndex = false as we have no elastic running for this test + * and just check that all the necessary beans are created. + */ + @Autowired private RestClient restClient; + @Autowired private ElasticsearchClient elasticsearchClient; + @Autowired private ElasticsearchOperations elasticsearchOperations; + + @Autowired private CreateIndexFalseRepository repository; + + @Test + public void providesRequiredBeans() { + assertThat(restClient).isNotNull(); + assertThat(elasticsearchClient).isNotNull(); + assertThat(elasticsearchOperations).isNotNull(); + assertThat(repository).isNotNull(); + } + + @Document(indexName = "test-index-config-configuration", createIndex = false) + static class CreateIndexFalseEntity { + + @Nullable + @Id private String id; + } + + interface CreateIndexFalseRepository extends ElasticsearchRepository {} +} diff --git a/src/test/java/org/springframework/data/elasticsearch/config/configuration/ReactiveElasticsearchConfigurationELCTests.java b/src/test/java/org/springframework/data/elasticsearch/config/configuration/ReactiveElasticsearchConfigurationELCTests.java index 77b290eec..b0bab91c0 100644 --- a/src/test/java/org/springframework/data/elasticsearch/config/configuration/ReactiveElasticsearchConfigurationELCTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/config/configuration/ReactiveElasticsearchConfigurationELCTests.java @@ -66,7 +66,6 @@ public class ReactiveElasticsearchConfigurationELCTests { @Test public void providesRequiredBeans() { - // assertThat(webClient).isNotNull(); assertThat(reactiveElasticsearchClient).isNotNull(); assertThat(reactiveElasticsearchOperations).isNotNull(); assertThat(repository).isNotNull(); diff --git a/src/test/java/org/springframework/data/elasticsearch/config/configuration/ReactiveElasticsearchLegacyRestClientConfigurationELCTests.java b/src/test/java/org/springframework/data/elasticsearch/config/configuration/ReactiveElasticsearchLegacyRestClientConfigurationELCTests.java new file mode 100644 index 000000000..445309e5e --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/config/configuration/ReactiveElasticsearchLegacyRestClientConfigurationELCTests.java @@ -0,0 +1,85 @@ +/* + * Copyright 2019-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.data.elasticsearch.config.configuration; + +import static org.assertj.core.api.Assertions.*; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.annotation.Id; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.client.ClientConfiguration; +import org.springframework.data.elasticsearch.client.elc.ReactiveElasticsearchClient; +import org.springframework.data.elasticsearch.client.elc.ReactiveElasticsearchConfiguration; +import org.springframework.data.elasticsearch.client.elc.ReactiveElasticsearchLegacyRestClientConfiguration; +import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations; +import org.springframework.data.elasticsearch.repository.ReactiveElasticsearchRepository; +import org.springframework.data.elasticsearch.repository.config.EnableReactiveElasticsearchRepositories; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +/** + * @author Peter-Josef Meisch + * @since 4.4 + */ +@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") +@ExtendWith(SpringExtension.class) +@ContextConfiguration +public class ReactiveElasticsearchLegacyRestClientConfigurationELCTests { + + @Configuration + @EnableReactiveElasticsearchRepositories( + basePackages = { "org.springframework.data.elasticsearch.config.configuration" }, + considerNestedRepositories = true) + static class Config extends ReactiveElasticsearchLegacyRestClientConfiguration { + + @Override + public @NonNull ClientConfiguration clientConfiguration() { + return ClientConfiguration.builder() // + .connectedTo("localhost:9200") // + .build(); + } + } + + /* + * using a repository with an entity that is set to createIndex = false as we have no elastic running for this test + * and just check that all the necessary beans are created. + */ + @Autowired private ReactiveElasticsearchClient reactiveElasticsearchClient; + @Autowired private ReactiveElasticsearchOperations reactiveElasticsearchOperations; + @Autowired private CreateIndexFalseRepository repository; + + @Test + public void providesRequiredBeans() { + // assertThat(webClient).isNotNull(); + assertThat(reactiveElasticsearchClient).isNotNull(); + assertThat(reactiveElasticsearchOperations).isNotNull(); + assertThat(repository).isNotNull(); + } + + @Document(indexName = "test-index-config-configuration", createIndex = false) + static class CreateIndexFalseEntity { + + @Nullable + @Id private String id; + } + + interface CreateIndexFalseRepository extends ReactiveElasticsearchRepository {} +}