diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/spi/ElasticsearchClientFactory.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/spi/ElasticsearchClientFactory.java index f68d477b8a9..fa40ce27940 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/spi/ElasticsearchClientFactory.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/spi/ElasticsearchClientFactory.java @@ -19,6 +19,9 @@ public interface ElasticsearchClientFactory { @Incubating String DEFAULT_BEAN_NAME = "default"; + @Incubating + String SIMPLE_JDK_CLIENT_BEAN_NAME = "jdk-rest-client"; + ElasticsearchClientImplementor create(BeanResolver beanResolver, ConfigurationPropertySource propertySource, ThreadProvider threadProvider, String threadNamePrefix, diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/ClientJdkElasticsearchClient.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/ClientJdkElasticsearchClient.java new file mode 100644 index 00000000000..fe76c888249 --- /dev/null +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/ClientJdkElasticsearchClient.java @@ -0,0 +1,274 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.impl; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.regex.Pattern; + +import org.hibernate.search.backend.elasticsearch.client.common.gson.spi.JsonLogHelper; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchClientImplementor; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchRequest; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchResponse; +import org.hibernate.search.backend.elasticsearch.client.common.util.spi.ElasticsearchClientUtils; +import org.hibernate.search.backend.elasticsearch.logging.spi.ElasticsearchClientLog; +import org.hibernate.search.backend.elasticsearch.logging.spi.ElasticsearchRequestLog; +import org.hibernate.search.engine.common.execution.spi.SimpleScheduledExecutor; +import org.hibernate.search.engine.common.timing.Deadline; +import org.hibernate.search.engine.environment.bean.BeanHolder; +import org.hibernate.search.util.common.impl.Closer; +import org.hibernate.search.util.common.impl.Futures; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; + +public class ClientJdkElasticsearchClient implements ElasticsearchClientImplementor { + + private static final Pattern CHARSET_PATTERN = + Pattern.compile( "(?:;|^)\\s*charset\\s*=\\s*\"?([^\"\\s;]+)\"?", Pattern.CASE_INSENSITIVE ); + + private final BeanHolder restClientHolder; + + private final SimpleScheduledExecutor timeoutExecutorService; + + private final Optional requestTimeoutMs; + + private final Gson gson; + private final JsonLogHelper jsonLogHelper; + + private final List requestInterceptors; + + ClientJdkElasticsearchClient(BeanHolder restClientHolder, + SimpleScheduledExecutor timeoutExecutorService, + Optional requestTimeoutMs, + Gson gson, JsonLogHelper jsonLogHelper, List requestInterceptors + ) { + this.restClientHolder = restClientHolder; + this.timeoutExecutorService = timeoutExecutorService; + this.requestTimeoutMs = requestTimeoutMs; + this.gson = gson; + this.jsonLogHelper = jsonLogHelper; + this.requestInterceptors = requestInterceptors; + } + + @Override + public CompletableFuture submit(ElasticsearchRequest request) { + CompletableFuture result = Futures.create( () -> send( request ) ) + .thenApply( this::convertResponse ); + if ( ElasticsearchRequestLog.INSTANCE.isDebugEnabled() ) { + long startTime = System.nanoTime(); + result.thenAccept( response -> log( request, startTime, response ) ); + } + return result; + } + + @Override + @SuppressWarnings("unchecked") + public T unwrap(Class clientClass) { + if ( HttpClient.class.isAssignableFrom( clientClass ) ) { + return (T) restClientHolder.get(); + } + throw ElasticsearchClientLog.INSTANCE.clientUnwrappingWithUnknownType( clientClass, HttpClient.class ); + } + + private ElasticsearchResponse convertResponse(HttpResponse response) { + try { + return new ElasticsearchResponse( + response.request().uri().getHost(), + response.statusCode(), + null, + response.body() ); + } + catch (RuntimeException e) { + throw ElasticsearchClientLog.INSTANCE.failedToParseElasticsearchResponse( response.statusCode(), + null, e.getMessage(), e ); + } + } + + private static Charset getCharset(HttpHeaders headers) { + Optional contentType = headers.firstValue( "Content-Type" ); + if ( contentType.isPresent() ) { + var matcher = CHARSET_PATTERN.matcher( contentType.get() ); + if ( matcher.find() ) { + return Charset.forName( matcher.group( 1 ) ); + } + } + return StandardCharsets.UTF_8; + } + + private CompletableFuture> send(ElasticsearchRequest elasticsearchRequest) { + HttpRequest request; + try { + HttpRequest.BodyPublisher entity = ClientJdkGsonHttpEntity.toEntity( gson, elasticsearchRequest ); + request = toRequest( elasticsearchRequest, entity ); + } + catch (IOException | RuntimeException e) { + CompletableFuture> completableFuture = new CompletableFuture<>(); + completableFuture.completeExceptionally( e ); + return completableFuture; + } + + CompletableFuture> completableFuture = restClientHolder.get().sendAsync( + request, + new JsonObjectBodyHandler() + ); + + Deadline deadline = elasticsearchRequest.deadline(); + if ( deadline == null && requestTimeoutMs.isEmpty() ) { + // no need to schedule a client side timeout + return completableFuture; + } + + long currentTimeoutValue = + deadline == null ? Long.valueOf( requestTimeoutMs.get() ) : deadline.checkRemainingTimeMillis(); + + /* + * TODO HSEARCH-3590 maybe the callback should also cancel the request? + */ + ScheduledFuture timeout = timeoutExecutorService.schedule( + () -> { + if ( !completableFuture.isDone() ) { + RuntimeException cause = ElasticsearchClientLog.INSTANCE.requestTimedOut( + Duration.ofNanos( TimeUnit.MILLISECONDS.toNanos( currentTimeoutValue ) ), + elasticsearchRequest ); + completableFuture.completeExceptionally( + deadline != null ? deadline.forceTimeoutAndCreateException( cause ) : cause + ); + } + }, + currentTimeoutValue, TimeUnit.MILLISECONDS + ); + completableFuture.thenRun( () -> timeout.cancel( false ) ); + + return completableFuture; + } + + private HttpRequest toRequest(ElasticsearchRequest elasticsearchRequest, HttpRequest.BodyPublisher bodyPublisher) + throws IOException { + URI uri = toUri( elasticsearchRequest ); + HttpRequest.Builder request = HttpRequest.newBuilder( uri ) + .method( elasticsearchRequest.method(), bodyPublisher ); + setPerRequestSocketTimeout( elasticsearchRequest, request ); + if ( !ClientJdkGsonHttpEntity.isNoBodyPublisher( bodyPublisher ) ) { + request.header( "Content-Type", "application/json" ); + } + + HttpRequestInterceptorContext context = new HttpRequestInterceptorContext( elasticsearchRequest.method() ); + for ( HttpRequestInterceptor requestInterceptor : requestInterceptors ) { + requestInterceptor.process( request, bodyPublisher, context ); + } + + return request.build(); + } + + private URI toUri(ElasticsearchRequest elasticsearchRequest) { + return restClientHolder.get().nextNode().createRequestURI( elasticsearchRequest.path(), + elasticsearchRequest.parameters() ); + } + + private void setPerRequestSocketTimeout(ElasticsearchRequest elasticsearchRequest, HttpRequest.Builder request) { + Deadline deadline = elasticsearchRequest.deadline(); + if ( deadline == null ) { + return; + } + + long timeToHardTimeout = deadline.checkRemainingTimeMillis(); + + // set a per-request socket timeout + int generalRequestTimeoutMs = ( timeToHardTimeout <= Integer.MAX_VALUE ) ? Math.toIntExact( timeToHardTimeout ) : -1; + request.timeout( Duration.of( generalRequestTimeoutMs, ChronoUnit.MILLIS ) ); + } + + private void log(ElasticsearchRequest request, long start, ElasticsearchResponse response) { + boolean successCode = ElasticsearchClientUtils.isSuccessCode( response.statusCode() ); + if ( !ElasticsearchRequestLog.INSTANCE.isTraceEnabled() && successCode ) { + return; + } + long executionTimeNs = System.nanoTime() - start; + long executionTimeMs = TimeUnit.NANOSECONDS.toMillis( executionTimeNs ); + if ( successCode ) { + ElasticsearchRequestLog.INSTANCE.executedRequest( request.method(), response.hostAndPort(), request.path(), + request.parameters(), + request.bodyParts().size(), executionTimeMs, + response.statusCode(), response.statusMessage(), + jsonLogHelper.toString( request.bodyParts() ), + jsonLogHelper.toString( response.body() ) ); + } + else { + ElasticsearchRequestLog.INSTANCE.executedRequestWithFailure( request.method(), response.hostAndPort(), + request.path(), + request.parameters(), + request.bodyParts().size(), executionTimeMs, + response.statusCode(), response.statusMessage(), + jsonLogHelper.toString( request.bodyParts() ), + jsonLogHelper.toString( response.body() ) ); + } + } + + @Override + public void close() { + try ( Closer closer = new Closer<>() ) { + /* + * There's no point waiting for timeouts: we'll just expect the RestClient to cancel all + * currently running requests when closing. + */ + // The BeanHolder is responsible for calling close() on the client if necessary. + closer.push( BeanHolder::close, this.restClientHolder ); + } + catch (RuntimeException | IOException e) { + throw ElasticsearchClientLog.INSTANCE.unableToShutdownClient( e.getMessage(), e ); + } + } + + private class GsonJsonMapper implements Function { + private final Charset charset; + private final int statusCode; + + public GsonJsonMapper(Charset charset, int statusCode) { + this.charset = charset; + this.statusCode = statusCode; + } + + @Override + public JsonObject apply(InputStream inputStream) { + try ( inputStream; Reader reader = new InputStreamReader( inputStream, charset ) ) { + return gson.fromJson( reader, JsonObject.class ); + } + catch (IOException e) { + throw ElasticsearchClientLog.INSTANCE.failedToParseElasticsearchResponse( statusCode, null, e.getMessage(), e ); + } + } + } + + private class JsonObjectBodyHandler implements HttpResponse.BodyHandler { + + @Override + public HttpResponse.BodySubscriber apply(HttpResponse.ResponseInfo responseInfo) { + Charset charset = getCharset( responseInfo.headers() ); + + return HttpResponse.BodySubscribers.mapping( + HttpResponse.BodySubscribers.ofInputStream(), + new GsonJsonMapper( charset, responseInfo.statusCode() ) + ); + } + } +} diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/ClientJdkElasticsearchClientFactory.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/ClientJdkElasticsearchClientFactory.java new file mode 100644 index 00000000000..16ad0901afc --- /dev/null +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/ClientJdkElasticsearchClientFactory.java @@ -0,0 +1,257 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.impl; + +import java.net.Authenticator; +import java.net.PasswordAuthentication; +import java.net.http.HttpClient; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.Executors; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +import org.hibernate.search.backend.elasticsearch.cfg.ElasticsearchBackendSettings; +import org.hibernate.search.backend.elasticsearch.client.common.gson.spi.GsonProvider; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchClientFactory; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchClientImplementor; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchRequestInterceptor; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchRequestInterceptorProvider; +import org.hibernate.search.backend.elasticsearch.client.jdk.ElasticsearchHttpClientConfigurer; +import org.hibernate.search.backend.elasticsearch.client.jdk.cfg.ClientJdkElasticsearchBackendClientSettings; +import org.hibernate.search.backend.elasticsearch.client.jdk.cfg.spi.ClientJdkElasticsearchBackendClientSpiSettings; +import org.hibernate.search.backend.elasticsearch.logging.spi.ConfigurationLog; +import org.hibernate.search.engine.cfg.ConfigurationPropertySource; +import org.hibernate.search.engine.cfg.spi.ConfigurationProperty; +import org.hibernate.search.engine.cfg.spi.OptionalConfigurationProperty; +import org.hibernate.search.engine.common.execution.spi.SimpleScheduledExecutor; +import org.hibernate.search.engine.environment.bean.BeanHolder; +import org.hibernate.search.engine.environment.bean.BeanReference; +import org.hibernate.search.engine.environment.bean.BeanResolver; +import org.hibernate.search.engine.environment.thread.spi.ThreadProvider; +import org.hibernate.search.util.common.impl.SuppressingCloser; + + +public class ClientJdkElasticsearchClientFactory implements ElasticsearchClientFactory { + + private static final OptionalConfigurationProperty> CLIENT_INSTANCE = + ConfigurationProperty.forKey( ClientJdkElasticsearchBackendClientSpiSettings.CLIENT_INSTANCE ) + .asBeanReference( HttpClient.class ) + .build(); + + private static final OptionalConfigurationProperty> HOSTS = + ConfigurationProperty.forKey( ElasticsearchBackendSettings.HOSTS ) + .asString().multivalued() + .build(); + + private static final OptionalConfigurationProperty PROTOCOL = + ConfigurationProperty.forKey( ElasticsearchBackendSettings.PROTOCOL ) + .asString() + .build(); + + private static final OptionalConfigurationProperty> URIS = + ConfigurationProperty.forKey( ElasticsearchBackendSettings.URIS ) + .asString().multivalued() + .build(); + + private static final ConfigurationProperty PATH_PREFIX = + ConfigurationProperty.forKey( ElasticsearchBackendSettings.PATH_PREFIX ) + .asString() + .withDefault( ElasticsearchBackendSettings.Defaults.PATH_PREFIX ) + .build(); + + private static final OptionalConfigurationProperty USERNAME = + ConfigurationProperty.forKey( ElasticsearchBackendSettings.USERNAME ) + .asString() + .build(); + + private static final OptionalConfigurationProperty PASSWORD = + ConfigurationProperty.forKey( ElasticsearchBackendSettings.PASSWORD ) + .asString() + .build(); + + private static final OptionalConfigurationProperty REQUEST_TIMEOUT = + ConfigurationProperty.forKey( ClientJdkElasticsearchBackendClientSettings.REQUEST_TIMEOUT ) + .asIntegerStrictlyPositive() + .build(); + + private static final ConfigurationProperty CONNECTION_TIMEOUT = + ConfigurationProperty.forKey( ClientJdkElasticsearchBackendClientSettings.CONNECTION_TIMEOUT ) + .asIntegerPositiveOrZeroOrNegative() + .withDefault( ClientJdkElasticsearchBackendClientSettings.Defaults.CONNECTION_TIMEOUT ) + .build(); + + private static final OptionalConfigurationProperty< + BeanReference> CLIENT_CONFIGURER = + ConfigurationProperty.forKey( ClientJdkElasticsearchBackendClientSettings.CLIENT_CONFIGURER ) + .asBeanReference( ElasticsearchHttpClientConfigurer.class ) + .build(); + + @Override + public ElasticsearchClientImplementor create(BeanResolver beanResolver, ConfigurationPropertySource propertySource, + ThreadProvider threadProvider, String threadNamePrefix, + SimpleScheduledExecutor timeoutExecutorService, + GsonProvider gsonProvider) { + Optional requestTimeoutMs = REQUEST_TIMEOUT.get( propertySource ); + + NodeProvider nodeProvider = NodeProvider.fromOptionalStrings( PROTOCOL.get( propertySource ), + HOSTS.get( propertySource ), URIS.get( propertySource ), PATH_PREFIX.get( propertySource ) ); + BeanHolder restClientHolder = + createClient( beanResolver, propertySource, threadProvider, threadNamePrefix, nodeProvider ); + + return new ClientJdkElasticsearchClient( + restClientHolder, timeoutExecutorService, requestTimeoutMs, + gsonProvider.getGson(), gsonProvider.getLogHelper(), + createRequestInterceptors( beanResolver, propertySource ) + ); + } + + private static List createRequestInterceptors(BeanResolver beanResolver, + ConfigurationPropertySource propertySource) { + List interceptors = new ArrayList<>(); + List> requestInterceptorProviderReferences = + beanResolver.allConfiguredForRole( ElasticsearchRequestInterceptorProvider.class ); + try ( BeanHolder> requestInterceptorProvidersHodler = + beanResolver.resolve( requestInterceptorProviderReferences ) ) { + ClientJdkElasticsearchHttpClientConfigurationContext clientConfigurationContext = + new ClientJdkElasticsearchHttpClientConfigurationContext( beanResolver, propertySource, null ); + for ( ElasticsearchRequestInterceptorProvider interceptorProvider : requestInterceptorProvidersHodler.get() ) { + Optional requestInterceptor = + interceptorProvider.provide( clientConfigurationContext ); + if ( requestInterceptor.isPresent() ) { + interceptors.add( new ClientJdkHttpRequestInterceptor( requestInterceptor.get() ) ); + } + } + } + return interceptors; + } + + private BeanHolder createClient(BeanResolver beanResolver, + ConfigurationPropertySource propertySource, + ThreadProvider threadProvider, String threadNamePrefix, NodeProvider nodeProvider) { + Optional> providedHttpClientHolder = CLIENT_INSTANCE.getAndMap( + propertySource, beanResolver::resolve ); + if ( providedHttpClientHolder.isPresent() ) { + return BeanHolder.ofCloseable( new RestJdkClient( nodeProvider, providedHttpClientHolder.get().get() ) ); + } + + HttpClient.Builder builder = HttpClient.newBuilder() + // NOTE: ES does not work ok with HTTP 2 if we don't send the content length and that can happen so let's stick to 1.1 for now ? + // (we end up with Caused by: java.io.IOException: Received RST_STREAM: Stream cancelled) + .version( HttpClient.Version.HTTP_1_1 ); + + Optional> customConfig = CLIENT_CONFIGURER + .getAndMap( propertySource, beanResolver::resolve ); + + RestJdkClient client = null; + List> httpClientConfigurerReferences = + beanResolver.allConfiguredForRole( ElasticsearchHttpClientConfigurer.class ); + try ( BeanHolder> httpClientConfigurersHolder = + beanResolver.resolve( httpClientConfigurerReferences ) ) { + customizeHttpClientConfig( + builder, + beanResolver, propertySource, + threadProvider, threadNamePrefix, + nodeProvider, httpClientConfigurersHolder.get(), + customConfig + ); + client = new RestJdkClient( nodeProvider, builder.build() ); + return BeanHolder.ofCloseable( client ); + } + catch (RuntimeException e) { + new SuppressingCloser( e ) + .push( client ); + throw e; + } + finally { + if ( customConfig.isPresent() ) { + // Assuming that #customizeHttpClientConfig has been already executed + // and therefore the bean has been already used. + customConfig.get().close(); + } + } + } + + private HttpClient.Builder customizeHttpClientConfig(HttpClient.Builder builder, + BeanResolver beanResolver, ConfigurationPropertySource propertySource, + ThreadProvider threadProvider, String threadNamePrefix, + NodeProvider nodeProvider, Iterable configurers, + Optional> customConfig) { + builder.executor( Executors.newCachedThreadPool( + threadProvider.createThreadFactory( threadNamePrefix + " - Transport thread" ) ) ); + + if ( !nodeProvider.isSslEnabled() ) { + SSLContext sslContext = null; + try { + sslContext = SSLContext.getInstance( "TLS" ); + sslContext.init( null, TRUST_ALL_CERTS, new SecureRandom() ); + builder.sslContext( sslContext ); + } + catch (NoSuchAlgorithmException | KeyManagementException e) { + throw new RuntimeException( e ); + } + } + + builder.connectTimeout( Duration.ofMillis( CONNECTION_TIMEOUT.get( propertySource ) ) ); + + + Optional username = USERNAME.get( propertySource ); + if ( username.isPresent() ) { + Optional password = PASSWORD.get( propertySource ).map( String::toCharArray ); + if ( password.isPresent() && !nodeProvider.isSslEnabled() ) { + ConfigurationLog.INSTANCE.usingPasswordOverHttp(); + } + builder.authenticator( new Authenticator() { + private final PasswordAuthentication passwordAuthentication = + new PasswordAuthentication( username.get(), password.orElseGet( () -> new char[0] ) ); + + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return passwordAuthentication; + } + } ); + } + + ClientJdkElasticsearchHttpClientConfigurationContext clientConfigurationContext = + new ClientJdkElasticsearchHttpClientConfigurationContext( beanResolver, propertySource, builder ); + + for ( ElasticsearchHttpClientConfigurer configurer : configurers ) { + configurer.configure( clientConfigurationContext ); + } + if ( customConfig.isPresent() ) { + BeanHolder customConfigBeanHolder = customConfig.get(); + customConfigBeanHolder.get().configure( clientConfigurationContext ); + } + + return builder; + } + + private static final TrustManager[] TRUST_ALL_CERTS = new TrustManager[] { + new X509TrustManager() { + + private static final X509Certificate[] X_509_CERTIFICATES = new X509Certificate[0]; + + public X509Certificate[] getAcceptedIssuers() { + return X_509_CERTIFICATES; + } + + public void checkClientTrusted(X509Certificate[] certs, String authType) { + // Do nothing: trust client certificates + } + + public void checkServerTrusted(X509Certificate[] certs, String authType) { + // Do nothing: trust server certificates + } + } + }; +} diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/ClientJdkElasticsearchHttpClientConfigurationContext.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/ClientJdkElasticsearchHttpClientConfigurationContext.java new file mode 100644 index 00000000000..931441d0217 --- /dev/null +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/ClientJdkElasticsearchHttpClientConfigurationContext.java @@ -0,0 +1,44 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.impl; + +import java.net.http.HttpClient; + +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchRequestInterceptorProviderContext; +import org.hibernate.search.backend.elasticsearch.client.jdk.ElasticsearchHttpClientConfigurationContext; +import org.hibernate.search.engine.cfg.ConfigurationPropertySource; +import org.hibernate.search.engine.environment.bean.BeanResolver; + +final class ClientJdkElasticsearchHttpClientConfigurationContext + implements ElasticsearchHttpClientConfigurationContext, ElasticsearchRequestInterceptorProviderContext { + private final BeanResolver beanResolver; + private final ConfigurationPropertySource configurationPropertySource; + private final HttpClient.Builder clientBuilder; + + ClientJdkElasticsearchHttpClientConfigurationContext( + BeanResolver beanResolver, + ConfigurationPropertySource configurationPropertySource, + HttpClient.Builder clientBuilder) { + this.beanResolver = beanResolver; + this.configurationPropertySource = configurationPropertySource; + this.clientBuilder = clientBuilder; + } + + @Override + public BeanResolver beanResolver() { + return beanResolver; + } + + @Override + public ConfigurationPropertySource configurationPropertySource() { + return configurationPropertySource; + } + + @Override + public HttpClient.Builder clientBuilder() { + return clientBuilder; + } + +} diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/ClientJdkGsonHttpEntity.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/ClientJdkGsonHttpEntity.java new file mode 100644 index 00000000000..b7b59be0fff --- /dev/null +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/ClientJdkGsonHttpEntity.java @@ -0,0 +1,51 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.impl; + +import java.io.IOException; +import java.net.http.HttpRequest; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.concurrent.Flow; + +import org.hibernate.search.backend.elasticsearch.client.common.gson.entity.spi.GsonHttpEntityContentProvider; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchRequest; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; + +public class ClientJdkGsonHttpEntity extends GsonHttpEntityContentProvider implements HttpRequest.BodyPublisher { + + private static final HttpRequest.BodyPublisher NO_BODY_PUBLISHER = HttpRequest.BodyPublishers.noBody(); + + public static HttpRequest.BodyPublisher toEntity(Gson gson, ElasticsearchRequest request) throws IOException { + final List bodyParts = request.bodyParts(); + if ( bodyParts.isEmpty() ) { + return NO_BODY_PUBLISHER; + } + return new ClientJdkGsonHttpEntity( gson, bodyParts ); + } + + private final HttpRequest.BodyPublisher delegate; + + public ClientJdkGsonHttpEntity(Gson gson, List bodyParts) throws IOException { + super( gson, bodyParts ); + delegate = HttpRequest.BodyPublishers.ofInputStream( this::getContent ); + } + + @Override + public long contentLength() { + return getContentLength(); + } + + @Override + public void subscribe(Flow.Subscriber subscriber) { + delegate.subscribe( subscriber ); + } + + static boolean isNoBodyPublisher(HttpRequest.BodyPublisher bodyPublisher) { + return bodyPublisher == NO_BODY_PUBLISHER || bodyPublisher.contentLength() == 0; + } +} diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/ClientJdkHttpRequestInterceptor.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/ClientJdkHttpRequestInterceptor.java new file mode 100644 index 00000000000..e6bf3d47e81 --- /dev/null +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/ClientJdkHttpRequestInterceptor.java @@ -0,0 +1,135 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.impl; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URLDecoder; +import java.net.http.HttpRequest; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchRequestInterceptor; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchRequestInterceptorContext; + + +record ClientJdkHttpRequestInterceptor(ElasticsearchRequestInterceptor elasticsearchRequestInterceptor) + implements HttpRequestInterceptor { + + // https://docs.oracle.com/en/java/javase/25/docs/api/java.net.http/module-summary.html + // + // Host header is one of the restricted ^ so we skip it here: + private static final Set HEADERS_TO_IGNORE = Set.of( "host" ); + + @Override + public void process(HttpRequest.Builder request, HttpRequest.BodyPublisher bodyPublisher, + HttpRequestInterceptorContext context) + throws IOException { + elasticsearchRequestInterceptor.intercept( + new ClientJavaRequestContext( request.copy().build(), request, bodyPublisher, context ) + ); + } + + private record ClientJavaRequestContext(HttpRequest request, HttpRequest.Builder original, + HttpRequest.BodyPublisher bodyPublisher, HttpRequestInterceptorContext context) + implements ElasticsearchRequestInterceptorContext { + + @Override + public boolean hasContent() { + return !ClientJdkGsonHttpEntity.isNoBodyPublisher( bodyPublisher ); + } + + @Override + public InputStream content() { + if ( bodyPublisher instanceof ClientJdkGsonHttpEntity publisher ) { + return publisher.getContent(); + } + return null; + } + + @Override + public String scheme() { + return request.uri().getScheme(); + } + + @Override + public String host() { + return request.uri().getHost(); + } + + @Override + public Integer port() { + return request.uri().getPort(); + } + + @Override + public String method() { + return context().method(); + } + + @Override + public String path() { + return request.uri().getPath(); + } + + @Override + public Map queryParameters() { + String query = request.uri().getQuery(); + if ( query == null || query.isEmpty() ) { + return Map.of(); + } + + Map map = new HashMap<>(); + + String[] params = query.split( "&" ); + + for ( String param : params ) { + String[] pair = param.split( "=", 2 ); + + if ( pair.length == 2 ) { + map.put( + URLDecoder.decode( pair[0], StandardCharsets.UTF_8 ), + URLDecoder.decode( pair[1], StandardCharsets.UTF_8 ) + ); + } + else { + map.put( URLDecoder.decode( pair[0], StandardCharsets.UTF_8 ), "" ); + } + } + + return map; + } + + @Override + public void overrideHeaders(Map> headers) { + for ( Map.Entry> header : headers.entrySet() ) { + String name = header.getKey(); + // To prevent java.lang.IllegalArgumentException: restricted header name: "Header-name" + if ( HEADERS_TO_IGNORE.contains( name.toLowerCase( Locale.ROOT ) ) ) { + continue; + } + boolean first = true; + for ( String value : header.getValue() ) { + if ( first ) { + original.setHeader( name, value ); + first = false; + } + else { + original.header( name, value ); + } + } + } + } + + @Override + public String toString() { + return request.toString(); + } + } +} diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/ElasticsearchClientBeanConfigurer.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/ElasticsearchClientBeanConfigurer.java index 0b3fb7fba70..dee2b1c129e 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/ElasticsearchClientBeanConfigurer.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/ElasticsearchClientBeanConfigurer.java @@ -16,5 +16,9 @@ public void configure(BeanConfigurationContext context) { ElasticsearchClientFactory.class, ElasticsearchClientFactory.DEFAULT_BEAN_NAME, beanResolver -> BeanHolder.of( new ClientRest4ElasticsearchClientFactory() ) ); + context.define( + ElasticsearchClientFactory.class, ElasticsearchClientFactory.SIMPLE_JDK_CLIENT_BEAN_NAME, + beanResolver -> BeanHolder.of( new ClientJdkElasticsearchClientFactory() ) + ); } } diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/HttpRequestInterceptor.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/HttpRequestInterceptor.java new file mode 100644 index 00000000000..818b36ec30d --- /dev/null +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/HttpRequestInterceptor.java @@ -0,0 +1,13 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.impl; + +import java.io.IOException; +import java.net.http.HttpRequest; + +interface HttpRequestInterceptor { + void process(HttpRequest.Builder request, HttpRequest.BodyPublisher bodyPublisher, HttpRequestInterceptorContext context) + throws IOException; +} diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/HttpRequestInterceptorContext.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/HttpRequestInterceptorContext.java new file mode 100644 index 00000000000..64bd39bf768 --- /dev/null +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/HttpRequestInterceptorContext.java @@ -0,0 +1,8 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.impl; + +record HttpRequestInterceptorContext(String method) { +} diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/NodeProvider.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/NodeProvider.java new file mode 100644 index 00000000000..3354b9ad277 --- /dev/null +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/NodeProvider.java @@ -0,0 +1,228 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.impl; + +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; + +import org.hibernate.search.backend.elasticsearch.cfg.ElasticsearchBackendSettings; +import org.hibernate.search.backend.elasticsearch.logging.spi.ConfigurationLog; +import org.hibernate.search.util.common.annotation.Incubating; + +@Incubating +public class NodeProvider { + + public static NodeProvider fromOptionalStrings(Optional protocol, Optional> hostAndPortStrings, + Optional> uris, String pathPrefix) { + if ( !uris.isPresent() ) { + String protocolValue = + ( protocol.isPresent() ) ? protocol.get() : ElasticsearchBackendSettings.Defaults.PROTOCOL; + List hostAndPortValues = + ( hostAndPortStrings.isPresent() ) + ? hostAndPortStrings.get() + : ElasticsearchBackendSettings.Defaults.HOSTS; + return fromStrings( protocolValue, hostAndPortValues, pathPrefix ); + } + + if ( protocol.isPresent() ) { + throw ConfigurationLog.INSTANCE.uriAndProtocol( uris.get(), protocol.get() ); + } + + if ( hostAndPortStrings.isPresent() ) { + throw ConfigurationLog.INSTANCE.uriAndHosts( uris.get(), hostAndPortStrings.get() ); + } + + return fromStrings( uris.get(), pathPrefix ); + } + + private final AtomicInteger currentNode = new AtomicInteger( 0 ); + private final int numberOfNodes; + private final List serverNodes; + private final boolean httpsEnabled; + + public NodeProvider(List serverNodes, boolean httpsEnabled) { + this.serverNodes = serverNodes; + this.numberOfNodes = serverNodes.size(); + this.httpsEnabled = httpsEnabled; + // TODO: we can add a discovery of other nodes here later? + } + + public ServerNode nextNode() { + return serverNodes.get( currentNode.getAndUpdate( this::updateCounter ) ); + } + + private int updateCounter(int i) { + return ( i + 1 ) % numberOfNodes; + } + + private static NodeProvider fromStrings(String protocol, List hostAndPortStrings, String pathPrefix) { + if ( hostAndPortStrings.isEmpty() ) { + throw ConfigurationLog.INSTANCE.emptyListOfHosts(); + } + + List serverNodes = new ArrayList<>( hostAndPortStrings.size() ); + // Note: protocol and URI scheme are not the same thing, + // but for HTTP/HTTPS both the protocol and URI scheme are named HTTP/HTTPS. + String scheme = protocol.toLowerCase( Locale.ROOT ); + for ( int i = 0; i < hostAndPortStrings.size(); ++i ) { + serverNodes.add( createServerNode( scheme, hostAndPortStrings.get( i ), pathPrefix ) ); + } + return new NodeProvider( serverNodes, "https".equals( scheme ) ); + } + + private static ServerNode createServerNode(String scheme, String hostAndPort, String pathPrefix) { + if ( hostAndPort.indexOf( "://" ) >= 0 ) { + throw ConfigurationLog.INSTANCE.invalidHostAndPort( hostAndPort, null ); + } + String host; + int port = -1; + final int portIdx = hostAndPort.lastIndexOf( ':' ); + if ( portIdx < 0 ) { + host = hostAndPort; + } + else { + try { + port = Integer.parseInt( hostAndPort.substring( portIdx + 1 ) ); + } + catch (final NumberFormatException e) { + throw ConfigurationLog.INSTANCE.invalidHostAndPort( hostAndPort, e ); + } + host = hostAndPort.substring( 0, portIdx ); + } + return new ServerNode( scheme, host, pathPrefix, port ); + } + + private static NodeProvider fromStrings(List serverUrisStrings, String pathPrefix) { + if ( serverUrisStrings.isEmpty() ) { + throw ConfigurationLog.INSTANCE.emptyListOfUris(); + } + + List serverNodes = new ArrayList<>( serverUrisStrings.size() ); + Boolean https = null; + for ( int i = 0; i < serverUrisStrings.size(); ++i ) { + String uri = serverUrisStrings.get( i ); + try { + final int schemeIdx = uri.indexOf( "://" ); + if ( schemeIdx < 0 ) { + uri = "http://" + uri; + } + URI actual = URI.create( uri ); + String host = actual.getHost(); + if ( actual.getPort() != -1 ) { + host = host + ":" + actual.getPort(); + } + serverNodes.add( createServerNode( actual.getScheme(), host, pathPrefix ) ); + boolean currentHttps = "https".equals( actual.getScheme() ); + if ( https == null ) { + https = currentHttps; + } + else if ( currentHttps != https ) { + throw ConfigurationLog.INSTANCE.differentProtocolsOnUris( serverUrisStrings ); + } + } + catch (IllegalArgumentException e) { + throw ConfigurationLog.INSTANCE.invalidUri( uri, e.getMessage(), e ); + } + } + + return new NodeProvider( serverNodes, https ); + } + + public boolean isSslEnabled() { + return httpsEnabled; + } + + public static final class ServerNode { + private final String protocol; + private final String host; + private final String basePath; + private final int port; + + public ServerNode(String protocol, String host, String basePath, int port) { + this.protocol = protocol; + this.host = host; + this.basePath = normalizeBasePath( basePath ); + this.port = port; + } + + private String normalizeBasePath(String basePath) { + if ( "".equals( basePath ) ) { + return ""; + } + else if ( basePath.endsWith( "/" ) ) { + basePath = basePath.substring( 0, basePath.length() - 1 ); + } + if ( !basePath.startsWith( "/" ) ) { + basePath = "/" + basePath; + } + return basePath; + } + + private String buildQueryString(Map parameters) { + if ( parameters == null || parameters.isEmpty() ) { + return ""; + } + + StringBuilder queryString = new StringBuilder(); + boolean first = true; + + for ( Map.Entry entry : parameters.entrySet() ) { + if ( !first ) { + queryString.append( "&" ); + } + + String encodedKey = URLEncoder.encode( entry.getKey(), StandardCharsets.UTF_8 ); + String encodedValue = URLEncoder.encode( entry.getValue(), StandardCharsets.UTF_8 ); + + queryString.append( encodedKey ).append( "=" ).append( encodedValue ); + first = false; + } + + return queryString.toString(); + } + + public URI createRequestURI(String path, Map parameters) { + if ( !path.isEmpty() && !path.startsWith( "/" ) ) { + throw new IllegalArgumentException( "Path must start with '/': " + path ); + } + String fullpath = basePath + path; + String queryString = buildQueryString( parameters ); + try { + return new URI( protocol, null, host, port, fullpath, queryString.isEmpty() ? null : queryString, null ); + } + catch (URISyntaxException e) { + throw new IllegalArgumentException( "Invalid URI: " + e.getMessage(), e ); + } + } + + @Override + public boolean equals(Object o) { + if ( !( o instanceof ServerNode that ) ) { + return false; + } + return port == that.port + && Objects.equals( protocol, that.protocol ) && Objects.equals( host, that.host ) + && Objects.equals( basePath, that.basePath ); + } + + @Override + public int hashCode() { + return Objects.hash( protocol, host, basePath, port ); + } + } + + public enum Status { + ACTIVE, FAILING; + } +} diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/RestJdkClient.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/RestJdkClient.java new file mode 100644 index 00000000000..917b48ba008 --- /dev/null +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/RestJdkClient.java @@ -0,0 +1,56 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.impl; + +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +import org.hibernate.search.util.common.annotation.Incubating; +import org.hibernate.search.util.common.impl.Closer; + +@Incubating +public class RestJdkClient implements AutoCloseable { + + private final NodeProvider nodeProvider; + + private HttpClient httpClient; + + public RestJdkClient(NodeProvider nodeProvider, HttpClient httpClient) { + this.nodeProvider = nodeProvider; + this.httpClient = httpClient; + } + + public NodeProvider.ServerNode nextNode() { + return nodeProvider.nextNode(); + } + + public CompletableFuture> sendAsync(HttpRequest request, + HttpResponse.BodyHandler responseBodyHandler) { + return httpClient.sendAsync( request, responseBodyHandler ); + } + + @Override + public void close() throws Exception { + if ( httpClient != null ) { + try ( Closer closer = new Closer<>() ) { + Optional executor = httpClient.executor(); + // May look a bit silly ... but close was only added in JDK 21: + if ( ( (Object) httpClient ) instanceof AutoCloseable closeable ) { + closer.push( AutoCloseable::close, closeable ); + } + if ( executor.isPresent() && executor.get() instanceof AutoCloseable closeable ) { + closer.push( AutoCloseable::close, closeable ); + } + } + finally { + httpClient = null; + } + } + } +} diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/jdk/ElasticsearchHttpClientConfigurationContext.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/jdk/ElasticsearchHttpClientConfigurationContext.java new file mode 100644 index 00000000000..de5dc755042 --- /dev/null +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/jdk/ElasticsearchHttpClientConfigurationContext.java @@ -0,0 +1,39 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.jdk; + + +import java.net.http.HttpClient; + +import org.hibernate.search.engine.cfg.ConfigurationPropertySource; +import org.hibernate.search.engine.environment.bean.BeanResolver; +import org.hibernate.search.util.common.annotation.Incubating; + +/** + * The context passed to {@link ElasticsearchHttpClientConfigurer}. + */ +@Incubating +public interface ElasticsearchHttpClientConfigurationContext { + + /** + * @return A {@link BeanResolver}. + */ + BeanResolver beanResolver(); + + /** + * @return A configuration property source, appropriately masked so that the factory + * doesn't need to care about Hibernate Search prefixes (hibernate.search.*, etc.). All the properties + * can be accessed at the root. + * CAUTION: the property key "type" is reserved for use by the engine. + */ + ConfigurationPropertySource configurationPropertySource(); + + /** + * @return A JDK HTTP client builder, to set the configuration. + * @see HttpClient.Builder + */ + HttpClient.Builder clientBuilder(); + +} diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/jdk/ElasticsearchHttpClientConfigurer.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/jdk/ElasticsearchHttpClientConfigurer.java new file mode 100644 index 00000000000..cd405d3ec7b --- /dev/null +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/jdk/ElasticsearchHttpClientConfigurer.java @@ -0,0 +1,40 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.jdk; + +import org.hibernate.search.util.common.annotation.Incubating; + +/** + * An extension point allowing fine-tuning of the JDK HTTP Client used by the Elasticsearch integration. + *

+ * This enables in particular connecting to cloud services that require a particular authentication method, + * such as request signing on Amazon Web Services. + *

+ * The ElasticsearchHttpClientConfigurer implementation will be given access to the HTTP client builder + * on startup. + *

+ * Note that you don't have to configure the client unless you have specific needs: + * the default configuration should work just fine for an on-premise Elasticsearch server. + */ +@Incubating +public interface ElasticsearchHttpClientConfigurer { + + /** + * Configure the HTTP Client. + *

+ * This method is called once for every configurer, each time an Elasticsearch client is set up. + *

+ * Implementors should take care of only applying configuration if relevant: + * there may be multiple, conflicting configurers in the path, so implementors should first check + * (through a configuration property) whether they are needed or not before applying any modification. + * For example an authentication configurer could decide not to do anything if no username is provided, + * or if the configuration property {@code my.configurer.enabled} is {@code false}. + * + * @param context A configuration context giving access to the JDK HTTP client builder + * and configuration properties in particular. + */ + void configure(ElasticsearchHttpClientConfigurationContext context); + +} diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/jdk/cfg/ClientJdkElasticsearchBackendClientSettings.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/jdk/cfg/ClientJdkElasticsearchBackendClientSettings.java new file mode 100644 index 00000000000..f4d2fb1eb75 --- /dev/null +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/jdk/cfg/ClientJdkElasticsearchBackendClientSettings.java @@ -0,0 +1,69 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.jdk.cfg; + +import org.hibernate.search.backend.elasticsearch.cfg.ElasticsearchBackendSettings; +import org.hibernate.search.backend.elasticsearch.client.jdk.ElasticsearchHttpClientConfigurer; +import org.hibernate.search.util.common.annotation.Incubating; + +/** + * Specific configuration properties for the Elasticsearch backend's rest client based on the Elasticsearch's low-level rest client. + *

+ * Constants in this class are to be appended to a prefix to form a property key; + * see {@link org.hibernate.search.engine.cfg.BackendSettings} for details. + * + * @author Gunnar Morling + */ +@Incubating +public final class ClientJdkElasticsearchBackendClientSettings { + + private ClientJdkElasticsearchBackendClientSettings() { + } + + /** + * The timeout when executing a request to an Elasticsearch server. + *

+ * This includes the time needed to establish a connection, send the request and read the response. + *

+ * Expects a positive Integer value in milliseconds, such as 60000, + * or a String that can be parsed into such Integer value. + *

+ * Defaults to no request timeout. + */ + public static final String REQUEST_TIMEOUT = "request_timeout"; + + /** + * The timeout when establishing a connection to an Elasticsearch server. + *

+ * Expects a positive Integer value in milliseconds, such as {@code 3000}, + * or a String that can be parsed into such Integer value. + *

+ * Defaults to {@link Defaults#CONNECTION_TIMEOUT}. + */ + public static final String CONNECTION_TIMEOUT = "connection_timeout"; + + /** + * A {@link ElasticsearchHttpClientConfigurer} that defines custom HTTP client configuration. + *

+ * It can be used for example to tune the SSL context to accept self-signed certificates. + * It allows overriding other HTTP client settings, such as {@link ElasticsearchBackendSettings#USERNAME}. + *

+ * Expects a reference to a bean of type {@link ElasticsearchHttpClientConfigurer}. + *

+ * Defaults to no value. + */ + public static final String CLIENT_CONFIGURER = "client.configurer"; + + /** + * Default values for the different settings if no values are given. + */ + public static final class Defaults { + + private Defaults() { + } + + public static final int CONNECTION_TIMEOUT = 1000; + } +} diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/jdk/cfg/spi/ClientJdkElasticsearchBackendClientSpiSettings.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/jdk/cfg/spi/ClientJdkElasticsearchBackendClientSpiSettings.java new file mode 100644 index 00000000000..f779481e02c --- /dev/null +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/jdk/cfg/spi/ClientJdkElasticsearchBackendClientSpiSettings.java @@ -0,0 +1,59 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.jdk.cfg.spi; + +import org.hibernate.search.engine.cfg.EngineSettings; +import org.hibernate.search.util.common.annotation.Incubating; + +/** + * Configuration properties for the Elasticsearch backend that are considered SPI (and not API). + */ +@Incubating +public final class ClientJdkElasticsearchBackendClientSpiSettings { + + /** + * The prefix expected for the key of every Hibernate Search configuration property. + */ + public static final String PREFIX = EngineSettings.PREFIX + "backend."; + + /** + * An external HTTP client instance that Hibernate Search should use for all requests to Elasticsearch. + *

+ * If this is set, Hibernate Search will not attempt to create its own {@link java.net.http.HttpClient}, + * and all client-related configuration properties (authentication, timeouts, configurer, ...) + * will be ignored. + *

+ * Hibernate Search will still apply some configuration properties (e.g. hosts/uris) + * that are not handled by the {@link java.net.http.HttpClient} itself. + *

+ * Expects a reference to a bean of type {@link java.net.http.HttpClient}. + *

+ * Defaults to nothing: if no client instance is provided, Hibernate Search will create its own. + *

+ * WARNING - Incubating API: the underlying client class may change without prior notice. + * + * @see org.hibernate.search.engine.cfg The core documentation of configuration properties, + * which includes a description of the "bean reference" properties and accepted values. + */ + public static final String CLIENT_INSTANCE = "client.instance"; + + private ClientJdkElasticsearchBackendClientSpiSettings() { + } + + /** + * Configuration property keys without the {@link #PREFIX prefix}. + */ + public static class Radicals { + + private Radicals() { + } + } + + public static final class Defaults { + + private Defaults() { + } + } +} diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/impl/ElasticsearchBackendFactory.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/impl/ElasticsearchBackendFactory.java index f2891520c8a..abb58ace98e 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/impl/ElasticsearchBackendFactory.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/impl/ElasticsearchBackendFactory.java @@ -4,9 +4,12 @@ */ package org.hibernate.search.backend.elasticsearch.impl; +import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Optional; +import java.util.Set; import org.hibernate.search.backend.elasticsearch.ElasticsearchVersion; import org.hibernate.search.backend.elasticsearch.cfg.ElasticsearchBackendSettings; @@ -58,7 +61,7 @@ public class ElasticsearchBackendFactory implements BackendFactory { .build(); private static final OptionalConfigurationProperty> CLIENT_FACTORY = - ConfigurationProperty.forKey( ElasticsearchBackendSpiSettings.CLIENT_FACTORY ) + ConfigurationProperty.forKey( ElasticsearchBackendSpiSettings.Radicals.CLIENT_FACTORY ) .asBeanReference( ElasticsearchClientFactory.class ) .build(); @@ -95,7 +98,7 @@ public BackendImplementor create(EventContext eventContext, BackendBuildContext clientFactoryHolder = customClientFactoryHolderOptional.get(); } else { - // otherwise let's find all client factories and pick + // otherwise let's find all client factories and pick one of them: List> clientFactoryReferences = beanResolver.allConfiguredForRole( ElasticsearchClientFactory.class ); if ( clientFactoryReferences.isEmpty() ) { @@ -105,18 +108,34 @@ public BackendImplementor create(EventContext eventContext, BackendBuildContext else if ( clientFactoryReferences.size() == 1 ) { clientFactoryHolder = clientFactoryReferences.get( 0 ).resolve( beanResolver ); } - // if there are 2 of them, maybe one is the "default" one, if so -- use the other one - else if ( clientFactoryReferences.size() == 2 ) { - var defaultFactoryReference = beanResolver.namedConfiguredForRole( ElasticsearchClientFactory.class ) - .get( ElasticsearchClientFactory.DEFAULT_BEAN_NAME ); - - var first = clientFactoryReferences.get( 0 ); - var second = clientFactoryReferences.get( 1 ); - if ( first == defaultFactoryReference ) { - clientFactoryHolder = second.resolve( beanResolver ); + // if there are more of them, maybe one is the "default" and the other one is JDK based, if so -- use the 3rd one + else { + Map> namedFactoryReferences = + beanResolver.namedConfiguredForRole( ElasticsearchClientFactory.class ); + if ( clientFactoryReferences.size() == 2 + && namedFactoryReferences.containsKey( ElasticsearchClientFactory.DEFAULT_BEAN_NAME ) + && namedFactoryReferences.containsKey( ElasticsearchClientFactory.SIMPLE_JDK_CLIENT_BEAN_NAME ) ) { + clientFactoryHolder = namedFactoryReferences.get( ElasticsearchClientFactory.DEFAULT_BEAN_NAME ) + .resolve( beanResolver ); } - else if ( second == defaultFactoryReference ) { - clientFactoryHolder = first.resolve( beanResolver ); + else { + Set> skippable = new HashSet<>( 2 ); + skippable.add( namedFactoryReferences.get( ElasticsearchClientFactory.DEFAULT_BEAN_NAME ) ); + skippable.add( namedFactoryReferences.get( ElasticsearchClientFactory.SIMPLE_JDK_CLIENT_BEAN_NAME ) ); + for ( BeanReference factoryReference : clientFactoryReferences ) { + if ( skippable.contains( factoryReference ) ) { + continue; + } + if ( clientFactoryHolder == null ) { + clientFactoryHolder = factoryReference.resolve( beanResolver ); + } + else { + throw ConfigurationLog.INSTANCE.backendClientFactoryMultipleConfigured( + clientFactoryReferences.stream().map( ref -> ref.resolve( beanResolver ) ).toList(), + eventContext + ); + } + } } } if ( clientFactoryHolder == null ) { diff --git a/distribution/pom.xml b/distribution/pom.xml index 704635af21f..695c0383e4b 100644 --- a/distribution/pom.xml +++ b/distribution/pom.xml @@ -275,6 +275,7 @@ ${basedir}/../backend/elasticsearch-client/elasticsearch-rest4-client/src/main/java; ${basedir}/../backend/elasticsearch-client/elasticsearch-rest5-client/src/main/java; ${basedir}/../backend/elasticsearch-client/opensearch-rest-client/src/main/java; + ${basedir}/../backend/elasticsearch-client/jdk-rest-client/src/main/java; ${basedir}/../backend/elasticsearch-aws/src/main/java; ${basedir}/../backend/lucene/src/main/java; ${basedir}/../mapper/orm-outbox-polling/src/main/java; @@ -289,8 +290,9 @@ ${basedir}/../backend/elasticsearch/target/generated-sources/annotations; ${basedir}/../backend/elasticsearch-client/common/target/generated-sources/annotations; ${basedir}/../backend/elasticsearch-client/elasticsearch-rest4-client/target/generated-sources/annotations; - ${basedir}/../backend/elasticsearch-client/opensearch-rest-client/target/generated-sources/annotations; ${basedir}/../backend/elasticsearch-client/elasticsearch-rest5-client/target/generated-sources/annotations; + ${basedir}/../backend/elasticsearch-client/opensearch-rest-client/target/generated-sources/annotations; + ${basedir}/../backend/elasticsearch-client/jdk-rest-client/target/generated-sources/annotations; ${basedir}/../backend/elasticsearch-aws/target/generated-sources/annotations; ${basedir}/../backend/lucene/target/generated-sources/annotations; ${basedir}/../mapper/orm-outbox-polling/src/main/avro/generated; diff --git a/documentation/pom.xml b/documentation/pom.xml index eb88781db83..8b5676a8490 100644 --- a/documentation/pom.xml +++ b/documentation/pom.xml @@ -26,10 +26,13 @@ ${failsafe.elasticsearch.rest5.reportsDirectory}/failsafe-summary.xml ${project.build.directory}/failsafe-reports/elasticsearch-opensearch ${failsafe.elasticsearch.opensearch.reportsDirectory}/failsafe-summary.xml + ${project.build.directory}/failsafe-reports/elasticsearch-jdk + ${failsafe.elasticsearch.jdk.reportsDirectory}/failsafe-summary.xml + ${project.build.directory}/asciidoctor/ ${project.build.directory}/hibernate-asciidoctor-theme/asciidoc @@ -227,8 +230,9 @@ **/Lucene*IT - **/client/java/*IT + **/client/rest5/*IT **/client/opensearch/*IT + **/client/jdk/*IT @@ -252,7 +256,7 @@ elasticsearch - **/client/java/*IT + **/client/rest5/*IT @@ -280,6 +284,32 @@ + + it-elasticsearch-jdk + + integration-test + + + ${test.elasticsearch.skip} + ${failsafe.jvm.args.no-jacoco} @{failsafe.jvm.args.jacoco.elasticsearch.jdk} + ${surefire.executionIdentifier}-elasticsearch-jdk + ${failsafe.elasticsearch.jdk.reportsDirectory} + ${failsafe.elasticsearch.jdk.summaryFile} + + org.hibernate.search:hibernate-search-backend-lucene + org.hibernate.search:hibernate-search-backend-elasticsearch-client-rest4 + org.hibernate.search:hibernate-search-backend-elasticsearch-client-rest5 + org.hibernate.search:hibernate-search-backend-elasticsearch-client-opensearch-rest + + + elasticsearch + jdk-rest-client + + + **/client/jdk/*IT + + + it-verify @@ -291,6 +321,7 @@ ${failsafe.elasticsearch.summaryFile} ${failsafe.elasticsearch.rest5.summaryFile} ${failsafe.elasticsearch.opensearch.summaryFile} + ${failsafe.elasticsearch.jdk.summaryFile} @@ -613,6 +644,7 @@ ${rootProject.empty.failsafe.summaryFile} ${rootProject.empty.failsafe.summaryFile} ${rootProject.empty.failsafe.summaryFile} + ${rootProject.empty.failsafe.summaryFile} diff --git a/documentation/src/main/asciidoc/migration/index.adoc b/documentation/src/main/asciidoc/migration/index.adoc index 0da686a3d4a..1c8b9903198 100644 --- a/documentation/src/main/asciidoc/migration/index.adoc +++ b/documentation/src/main/asciidoc/migration/index.adoc @@ -61,6 +61,7 @@ Currently available options are: * `org.hibernate.search:hibernate-search-backend-elasticsearch-client-rest4` backed by `org.elasticsearch.client:elasticsearch-rest-client` * `org.hibernate.search:hibernate-search-backend-elasticsearch-client-rest5` backed by `co.elastic.clients:elasticsearch-rest5-client` * `org.hibernate.search:hibernate-search-backend-elasticsearch-client-opensearch-rest` backed by `org.opensearch.client:opensearch-rest-client` +// * `org.hibernate.search:hibernate-search-backend-elasticsearch-client-jdk` backed by `java.net.http.HttpClient` [[data-format]] == Data format and schema diff --git a/documentation/src/main/asciidoc/public/components/elasticsearch-client-compatibility/_all.adoc b/documentation/src/main/asciidoc/public/components/elasticsearch-client-compatibility/_all.adoc index 4eb21392087..eaa4cc2ac37 100644 --- a/documentation/src/main/asciidoc/public/components/elasticsearch-client-compatibility/_all.adoc +++ b/documentation/src/main/asciidoc/public/components/elasticsearch-client-compatibility/_all.adoc @@ -17,5 +17,8 @@ Configuration properties from this section are applicable to the following clien |<> ^| ✔ +|<> +^| ✔ {jdk-client-warn} + |=== ==== diff --git a/documentation/src/main/asciidoc/public/components/elasticsearch-client-compatibility/_apache_based.adoc b/documentation/src/main/asciidoc/public/components/elasticsearch-client-compatibility/_apache_based.adoc new file mode 100644 index 00000000000..6cd196f209e --- /dev/null +++ b/documentation/src/main/asciidoc/public/components/elasticsearch-client-compatibility/_apache_based.adoc @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Red Hat Inc. and Hibernate Authors +[NOTE] +==== +Configuration properties from this section are applicable to the following clients: + +[cols="3,2",options="header"] +|=== +|Client |Configuration property applicability + +|<> +^| ✔ + +|<> +^| ✔ + +|<> +^| ✔ + +|<> +^| ❌ {jdk-client-warn} + +|=== +==== diff --git a/documentation/src/main/asciidoc/public/reference/_backend-elasticsearch.adoc b/documentation/src/main/asciidoc/public/reference/_backend-elasticsearch.adoc index 03b9a9fc289..5105f629645 100644 --- a/documentation/src/main/asciidoc/public/reference/_backend-elasticsearch.adoc +++ b/documentation/src/main/asciidoc/public/reference/_backend-elasticsearch.adoc @@ -170,6 +170,7 @@ which may change in future versions of Hibernate Search. You should explicitly define the REST client in the application dependencies, if it matters which one the Elasticsearch backend will pick. +:jdk-client-warn: === Available clients [[backend-elasticsearch-configuration-client-elasticsearch-client-rest4]] @@ -208,6 +209,23 @@ Underlying HTTP Client::: Apache HTTP Client 5 This Elasticsearch backend REST client is based on the OpenSearch low level client (`org.opensearch.client:opensearch-rest-client`). +[[backend-elasticsearch-configuration-client-elasticsearch-client-jdk]] +==== Simple REST client based on the JDKs `HttpClient` + +include::../components/_incubating-warning.adoc[] + +Coordinates:: +GroupID::: `org.hibernate.search` +ArtifactID::: `hibernate-search-backend-elasticsearch-client-jdk` +Underlying HTTP Client::: JDKs Http Client (`java.net.http.HttpClient`) + +This Elasticsearch backend REST client is based on the Http client that is part of the JDKs `java.net` API. +Mostly intended for very basic use cases, and may lack some features, +e.g. automatic node discovery, compared to the other clients. + +NOTE: JDKs Http client allows configuration though the system properties. +Consult the list of available properties for your JDK version, e.g. https://docs.oracle.com/en/java/javase/25/docs/api/java.net.http/module-summary.html[JDK 25] + [[backend-elasticsearch-configuration-hosts]] === Target hosts @@ -297,7 +315,7 @@ Expects a boolean value. The default for this property is `false`. * `discovery.refresh_interval` defines the interval between two executions of the automatic discovery. Expects a positive integer, in seconds. The default for this property is `10`. -include::../components/elasticsearch-client-compatibility/_all.adoc[] +include::../components/elasticsearch-client-compatibility/_apache_based.adoc[] [[backend-elasticsearch-authentication-http]] === HTTP authentication @@ -395,6 +413,7 @@ include::../components/elasticsearch-client-compatibility/_all.adoc[] [[backend-elasticsearch-configuration-connection-tuning]] === [[_connection_tuning]] Connection tuning +:jdk-client-warn: (Read timeout is not supported) Timeouts:: + [source, properties] @@ -418,6 +437,7 @@ These properties expect a positive < + include::../components/elasticsearch-client-compatibility/_all.adoc[] +:jdk-client-warn: (Configurable through JDKs Http client system properties) Connection pool:: + [source, properties] @@ -453,8 +473,9 @@ To prevent that from happening consider having the maximum number of connections ==== + -include::../components/elasticsearch-client-compatibility/_all.adoc[] +include::../components/elasticsearch-client-compatibility/_apache_based.adoc[] +:jdk-client-warn: "(Configurable through JDKs Http client system properties)" Keep Alive:: + [source, properties] @@ -472,7 +493,7 @@ the duration from the `Keep-Alive` header or the value of this property (if set) If this property is not set, only the `Keep-Alive` header is considered, and if it's absent, idle connections will be kept forever. + -include::../components/elasticsearch-client-compatibility/_all.adoc[] +include::../components/elasticsearch-client-compatibility/_apache_based.adoc[] [[backend-elasticsearch-configuration-http-client]] === [[_custom_http_client_configurations]] Custom HTTP client configurations @@ -485,7 +506,7 @@ See the following sections to learn how each of the clients can be configured. [[backend-elasticsearch-configuration-http-client-elasticsearch-rest]] === Elasticsearch low level client It is possible to directly configure the underlying Apache HTTP 4 client of the -<> +<> using an instance of `org.apache.http.impl.nio.client.HttpAsyncClientBuilder`. With this API you can add interceptors, change the keep alive, the max connections, @@ -503,7 +524,7 @@ for example `class:org.hibernate.search.documentation.backend.elasticsearch.clie ==== [source, java, indent=0, subs="+callouts"] ---- -include::{sourcedir}/org/hibernate/search/documentation/backend/elasticsearch/client/rest/HttpClientConfigurer.java[tags=include] +include::{sourcedir}/org/hibernate/search/documentation/backend/elasticsearch/client/rest4/HttpClientConfigurer.java[tags=include] ---- <1> The class has to implement the `ElasticsearchHttpClientConfigurer` interface. <2> The `configure` method provides the access to the `ElasticsearchHttpClientConfigurationContext`. @@ -546,7 +567,7 @@ for example `class:org.hibernate.search.documentation.backend.elasticsearch.clie ==== [source, java, indent=0, subs="+callouts"] ---- -include::{sourcedir}/org/hibernate/search/documentation/backend/elasticsearch/client/java/HttpClientConfigurer.java[tags=include] +include::{sourcedir}/org/hibernate/search/documentation/backend/elasticsearch/client/rest5/HttpClientConfigurer.java[tags=include] ---- <1> The class has to implement the `ElasticsearchHttpClientConfigurer` interface. <2> The `configure` method provides the access to the `ElasticsearchHttpClientConfigurationContext`. @@ -558,7 +579,7 @@ include::{sourcedir}/org/hibernate/search/documentation/backend/elasticsearch/cl ==== [source, xml, indent=0, subs="+callouts"] ---- -include::{resourcesdir}/configuration/http-client-configurer-java.properties[tags=include] +include::{resourcesdir}/configuration/http-client-configurer-rest5.properties[tags=include] ---- <1> Specify the HTTP client configurer. ==== @@ -589,7 +610,7 @@ for example `class:org.hibernate.search.documentation.backend.elasticsearch.clie ==== [source, java, indent=0, subs="+callouts"] ---- -include::{sourcedir}/org/hibernate/search/documentation/backend/elasticsearch/client/java/HttpClientConfigurer.java[tags=include] +include::{sourcedir}/org/hibernate/search/documentation/backend/elasticsearch/client/opensearch/HttpClientConfigurer.java[tags=include] ---- <1> The class has to implement the `ElasticsearchHttpClientConfigurer` interface. <2> The `configure` method provides the access to the `ElasticsearchHttpClientConfigurationContext`. @@ -611,6 +632,46 @@ include::{resourcesdir}/configuration/http-client-configurer-opensearch.properti Any setting defined by a custom http client configurer will override any other setting defined by Hibernate Search. ==== +[[backend-elasticsearch-configuration-http-client-elasticsearch-jdk]] +=== JDK `HttpClient` based REST client +It is possible to directly configure the underlying +<> +using an instance of `java.net.http.HttpClient.Builder`. + +Configuring the HTTP client directly requires two steps: + +. Define a class that implements the `org.hibernate.search.backend.elasticsearch.client.jdk.ElasticsearchHttpClientConfigurer` interface. +. Configure Hibernate Search to use that implementation by setting the configuration property +`hibernate.search.backend.client.configurer` +to a <> pointing to the implementation, +for example `class:org.hibernate.search.documentation.backend.elasticsearch.client.jdk.HttpClientConfigurer`. + +.Implementing and using a `ElasticsearchHttpClientConfigurer` +==== +[source, java, indent=0, subs="+callouts"] +---- +include::{sourcedir}/org/hibernate/search/documentation/backend/elasticsearch/client/jdk/HttpClientConfigurer.java[tags=include] +---- +<1> The class has to implement the `ElasticsearchHttpClientConfigurer` interface. +<2> The `configure` method provides the access to the `ElasticsearchHttpClientConfigurationContext`. +<3> From the context it is possible to get the `HttpClient.Builder`. +<4> Finally, you can use the builder to configure the client with your custom settings. +==== + +.Define a custom http client configurer in the properties +==== +[source, xml, indent=0, subs="+callouts"] +---- +include::{resourcesdir}/configuration/http-client-configurer-jdk.properties[tags=include] +---- +<1> Specify the HTTP client configurer. +==== + +[NOTE] +==== +Any setting defined by a custom http client configurer will override any other setting defined by Hibernate Search. +==== + [[backend-elasticsearch-configuration-version]] == [[backend-elasticsearch-configuration-dialect]] Version compatibility diff --git a/documentation/src/test/java/org/hibernate/search/documentation/backend/elasticsearch/client/java/ElasticsearchHttpClientConfigurerIT.java b/documentation/src/test/java/org/hibernate/search/documentation/backend/elasticsearch/client/jdk/ElasticsearchHttpClientConfigurerIT.java similarity index 94% rename from documentation/src/test/java/org/hibernate/search/documentation/backend/elasticsearch/client/java/ElasticsearchHttpClientConfigurerIT.java rename to documentation/src/test/java/org/hibernate/search/documentation/backend/elasticsearch/client/jdk/ElasticsearchHttpClientConfigurerIT.java index 7c60c61dfcb..643c1243a47 100644 --- a/documentation/src/test/java/org/hibernate/search/documentation/backend/elasticsearch/client/java/ElasticsearchHttpClientConfigurerIT.java +++ b/documentation/src/test/java/org/hibernate/search/documentation/backend/elasticsearch/client/jdk/ElasticsearchHttpClientConfigurerIT.java @@ -2,7 +2,7 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright Red Hat Inc. and Hibernate Authors */ -package org.hibernate.search.documentation.backend.elasticsearch.client.java; +package org.hibernate.search.documentation.backend.elasticsearch.client.jdk; import static org.assertj.core.api.Assertions.assertThat; @@ -33,7 +33,7 @@ void smoke() { assertThat( staticCounters.get( HttpClientConfigurer.INSTANCES ) ).isZero(); setupHelper.start() - .withProperties( "/configuration/http-client-configurer-java.properties" ) + .withProperties( "/configuration/http-client-configurer-jdk.properties" ) .setup( IndexedEntity.class ); assertThat( staticCounters.get( HttpClientConfigurer.INSTANCES ) ).isEqualTo( 1 ); diff --git a/documentation/src/test/java/org/hibernate/search/documentation/backend/elasticsearch/client/jdk/HttpClientConfigurer.java b/documentation/src/test/java/org/hibernate/search/documentation/backend/elasticsearch/client/jdk/HttpClientConfigurer.java new file mode 100644 index 00000000000..bc4e9b65dd6 --- /dev/null +++ b/documentation/src/test/java/org/hibernate/search/documentation/backend/elasticsearch/client/jdk/HttpClientConfigurer.java @@ -0,0 +1,34 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.documentation.backend.elasticsearch.client.jdk; + + +import java.net.http.HttpClient; +import java.time.Duration; + +import org.hibernate.search.backend.elasticsearch.client.jdk.ElasticsearchHttpClientConfigurationContext; +import org.hibernate.search.backend.elasticsearch.client.jdk.ElasticsearchHttpClientConfigurer; +import org.hibernate.search.util.impl.test.extension.StaticCounters; + + +// tag::include[] +public class HttpClientConfigurer implements ElasticsearchHttpClientConfigurer { // <1> + // end::include[] + static final StaticCounters.Key INSTANCES = StaticCounters.createKey(); + + public HttpClientConfigurer() { + StaticCounters.get().increment( INSTANCES ); + } + + @SuppressWarnings("unused") + // tag::include[] + + @Override + public void configure(ElasticsearchHttpClientConfigurationContext context) { // <2> + HttpClient.Builder builder = context.clientBuilder(); // <3> + builder.connectTimeout( Duration.ofSeconds( 2 ) ); // <4> + } +} +// end::include[] diff --git a/documentation/src/test/java/org/hibernate/search/documentation/backend/elasticsearch/client/rest/ElasticsearchHttpClientConfigurerIT.java b/documentation/src/test/java/org/hibernate/search/documentation/backend/elasticsearch/client/rest4/ElasticsearchHttpClientConfigurerIT.java similarity index 99% rename from documentation/src/test/java/org/hibernate/search/documentation/backend/elasticsearch/client/rest/ElasticsearchHttpClientConfigurerIT.java rename to documentation/src/test/java/org/hibernate/search/documentation/backend/elasticsearch/client/rest4/ElasticsearchHttpClientConfigurerIT.java index b1cefb90307..0a12f290196 100644 --- a/documentation/src/test/java/org/hibernate/search/documentation/backend/elasticsearch/client/rest/ElasticsearchHttpClientConfigurerIT.java +++ b/documentation/src/test/java/org/hibernate/search/documentation/backend/elasticsearch/client/rest4/ElasticsearchHttpClientConfigurerIT.java @@ -2,7 +2,7 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright Red Hat Inc. and Hibernate Authors */ -package org.hibernate.search.documentation.backend.elasticsearch.client.rest; +package org.hibernate.search.documentation.backend.elasticsearch.client.rest4; import static org.assertj.core.api.Assertions.assertThat; diff --git a/documentation/src/test/java/org/hibernate/search/documentation/backend/elasticsearch/client/rest/HttpClientConfigurer.java b/documentation/src/test/java/org/hibernate/search/documentation/backend/elasticsearch/client/rest4/HttpClientConfigurer.java similarity index 98% rename from documentation/src/test/java/org/hibernate/search/documentation/backend/elasticsearch/client/rest/HttpClientConfigurer.java rename to documentation/src/test/java/org/hibernate/search/documentation/backend/elasticsearch/client/rest4/HttpClientConfigurer.java index ed27e585b33..0d0d8f2430a 100644 --- a/documentation/src/test/java/org/hibernate/search/documentation/backend/elasticsearch/client/rest/HttpClientConfigurer.java +++ b/documentation/src/test/java/org/hibernate/search/documentation/backend/elasticsearch/client/rest4/HttpClientConfigurer.java @@ -2,7 +2,7 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright Red Hat Inc. and Hibernate Authors */ -package org.hibernate.search.documentation.backend.elasticsearch.client.rest; +package org.hibernate.search.documentation.backend.elasticsearch.client.rest4; import org.hibernate.search.backend.elasticsearch.client.rest4.ElasticsearchHttpClientConfigurationContext; import org.hibernate.search.backend.elasticsearch.client.rest4.ElasticsearchHttpClientConfigurer; diff --git a/documentation/src/test/java/org/hibernate/search/documentation/backend/elasticsearch/client/rest5/ElasticsearchHttpClientConfigurerIT.java b/documentation/src/test/java/org/hibernate/search/documentation/backend/elasticsearch/client/rest5/ElasticsearchHttpClientConfigurerIT.java new file mode 100644 index 00000000000..c286513a5a1 --- /dev/null +++ b/documentation/src/test/java/org/hibernate/search/documentation/backend/elasticsearch/client/rest5/ElasticsearchHttpClientConfigurerIT.java @@ -0,0 +1,64 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.documentation.backend.elasticsearch.client.rest5; + +import static org.assertj.core.api.Assertions.assertThat; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +import org.hibernate.search.documentation.testsupport.BackendConfigurations; +import org.hibernate.search.documentation.testsupport.DocumentationSetupHelper; +import org.hibernate.search.mapper.pojo.mapping.definition.annotation.Indexed; +import org.hibernate.search.mapper.pojo.mapping.definition.annotation.KeywordField; +import org.hibernate.search.util.impl.test.extension.StaticCounters; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class ElasticsearchHttpClientConfigurerIT { + + @RegisterExtension + public DocumentationSetupHelper setupHelper = DocumentationSetupHelper.withSingleBackend( + BackendConfigurations.simple() ); + + @RegisterExtension + public StaticCounters staticCounters = StaticCounters.create(); + + @Test + void smoke() { + assertThat( staticCounters.get( HttpClientConfigurer.INSTANCES ) ).isZero(); + + setupHelper.start() + .withProperties( "/configuration/http-client-configurer-rest5.properties" ) + .setup( IndexedEntity.class ); + + assertThat( staticCounters.get( HttpClientConfigurer.INSTANCES ) ).isEqualTo( 1 ); + } + + @Entity(name = IndexedEntity.NAME) + @Indexed + static class IndexedEntity { + + static final String NAME = "indexed"; + + @Id + @GeneratedValue + private Integer id; + + @KeywordField + private String text; + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + } + +} diff --git a/documentation/src/test/java/org/hibernate/search/documentation/backend/elasticsearch/client/java/HttpClientConfigurer.java b/documentation/src/test/java/org/hibernate/search/documentation/backend/elasticsearch/client/rest5/HttpClientConfigurer.java similarity index 99% rename from documentation/src/test/java/org/hibernate/search/documentation/backend/elasticsearch/client/java/HttpClientConfigurer.java rename to documentation/src/test/java/org/hibernate/search/documentation/backend/elasticsearch/client/rest5/HttpClientConfigurer.java index f4c1ea55972..15f27ea27ae 100644 --- a/documentation/src/test/java/org/hibernate/search/documentation/backend/elasticsearch/client/java/HttpClientConfigurer.java +++ b/documentation/src/test/java/org/hibernate/search/documentation/backend/elasticsearch/client/rest5/HttpClientConfigurer.java @@ -2,7 +2,7 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright Red Hat Inc. and Hibernate Authors */ -package org.hibernate.search.documentation.backend.elasticsearch.client.java; +package org.hibernate.search.documentation.backend.elasticsearch.client.rest5; import org.hibernate.search.backend.elasticsearch.client.rest5.ElasticsearchHttpClientConfigurationContext; import org.hibernate.search.backend.elasticsearch.client.rest5.ElasticsearchHttpClientConfigurer; diff --git a/documentation/src/test/resources/configuration/http-client-configurer-java.properties b/documentation/src/test/resources/configuration/http-client-configurer-jdk.properties similarity index 71% rename from documentation/src/test/resources/configuration/http-client-configurer-java.properties rename to documentation/src/test/resources/configuration/http-client-configurer-jdk.properties index a1d0816c717..6ebf3db372a 100644 --- a/documentation/src/test/resources/configuration/http-client-configurer-java.properties +++ b/documentation/src/test/resources/configuration/http-client-configurer-jdk.properties @@ -3,5 +3,5 @@ # tag::include[] # <1> -hibernate.search.backend.client.configurer = class:org.hibernate.search.documentation.backend.elasticsearch.client.java.HttpClientConfigurer +hibernate.search.backend.client.configurer = class:org.hibernate.search.documentation.backend.elasticsearch.client.jdk.HttpClientConfigurer # end::include[] diff --git a/documentation/src/test/resources/configuration/http-client-configurer-rest5.properties b/documentation/src/test/resources/configuration/http-client-configurer-rest5.properties new file mode 100644 index 00000000000..9c19ecc5264 --- /dev/null +++ b/documentation/src/test/resources/configuration/http-client-configurer-rest5.properties @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Red Hat Inc. and Hibernate Authors + +# tag::include[] +# <1> +hibernate.search.backend.client.configurer = class:org.hibernate.search.documentation.backend.elasticsearch.client.rest5.HttpClientConfigurer +# end::include[] diff --git a/documentation/src/test/resources/configuration/http-client-configurer.properties b/documentation/src/test/resources/configuration/http-client-configurer.properties index 39254336939..cbbd123eb95 100644 --- a/documentation/src/test/resources/configuration/http-client-configurer.properties +++ b/documentation/src/test/resources/configuration/http-client-configurer.properties @@ -3,5 +3,5 @@ # tag::include[] # <1> -hibernate.search.backend.client.configurer = class:org.hibernate.search.documentation.backend.elasticsearch.client.rest.HttpClientConfigurer +hibernate.search.backend.client.configurer = class:org.hibernate.search.documentation.backend.elasticsearch.client.rest4.HttpClientConfigurer # end::include[] diff --git a/integrationtest/backend/elasticsearch/pom.xml b/integrationtest/backend/elasticsearch/pom.xml index 0bcbb60d786..fa79eaf12db 100644 --- a/integrationtest/backend/elasticsearch/pom.xml +++ b/integrationtest/backend/elasticsearch/pom.xml @@ -27,6 +27,10 @@ ${failsafe.client.elasticsearch.rest5.reportsDirectory}/failsafe-summary.xml + ${project.build.directory}/failsafe-reports/elasticsearch-jdk + ${failsafe.client.jdk.reportsDirectory}/failsafe-summary.xml + + ${test.mockito.agent.jvm.args} @@ -163,6 +167,37 @@ + + it-jdk-client + + integration-test + verify + + + false + ${test.elasticsearch.skip} + ${surefire.executionIdentifier}-jdk + ${failsafe.client.jdk.reportsDirectory} + ${failsafe.client.jdk.summaryFile} + ${failsafe.jvm.args.no-jacoco} @{failsafe.jvm.args.jacoco.client.jdk} + + jdk-rest-client + + + org.hibernate.search:hibernate-search-backend-elasticsearch-client-rest4 + org.hibernate.search:hibernate-search-backend-elasticsearch-client-rest5 + org.hibernate.search:hibernate-search-backend-elasticsearch-client-opensearch-rest + + + org.hibernate.search.integrationtest.backend.elasticsearch.client.ClientRest4ElasticsearchClientFactoryIT + org.hibernate.search.integrationtest.backend.elasticsearch.client.ClientRest5ElasticsearchClientFactoryIT + org.hibernate.search.integrationtest.backend.elasticsearch.client.ClientOpenSearchElasticsearchClientFactoryIT + org.hibernate.search.integrationtest.backend.elasticsearch.ClientRest4ElasticsearchExtensionIT + org.hibernate.search.integrationtest.backend.elasticsearch.ClientRest5ElasticsearchExtensionIT + org.hibernate.search.integrationtest.backend.elasticsearch.ClientOpenSearchElasticsearchExtensionIT + + + @@ -234,6 +269,17 @@ ${project.build.directory}/${jacoco.environment.sub-directory}/opensearch/jacoco.exec + + jacoco-prepare-agent-integration-jdk + initialize + + prepare-agent-integration + + + failsafe.jvm.args.jacoco.client.jdk + ${project.build.directory}/${jacoco.environment.sub-directory}/jdk/jacoco.exec + + diff --git a/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/bootstrap/ElasticsearchBootstrapFailureIT.java b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/bootstrap/ElasticsearchBootstrapFailureIT.java index d6c56ed14ae..32312e3a6b8 100644 --- a/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/bootstrap/ElasticsearchBootstrapFailureIT.java +++ b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/bootstrap/ElasticsearchBootstrapFailureIT.java @@ -55,8 +55,8 @@ void cannotConnect() { .defaultBackendContext() .failure( "Unable to detect the Elasticsearch version running on the cluster", - "Elasticsearch request failed", - "Connection refused" + "Elasticsearch request failed" + // , "Connection refused" ) ); } diff --git a/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/bootstrap/ElasticsearchBootstrapIT.java b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/bootstrap/ElasticsearchBootstrapIT.java index f5c2d15233e..516054fa35d 100644 --- a/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/bootstrap/ElasticsearchBootstrapIT.java +++ b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/bootstrap/ElasticsearchBootstrapIT.java @@ -62,7 +62,7 @@ void explicitModelDialect() { ElasticsearchBackendSettings.VERSION, ElasticsearchTestDialect.getActualVersion().toString() ) .withBackendProperty( - ElasticsearchBackendSpiSettings.CLIENT_FACTORY, + ElasticsearchBackendSpiSettings.Radicals.CLIENT_FACTORY, elasticsearchClientSpy.factoryReference() ) .withSchemaManagement( StubMappingSchemaManagementStrategy.DROP_ON_SHUTDOWN_ONLY ) @@ -104,7 +104,7 @@ void noVersionCheck_missingVersion() { ElasticsearchBackendSettings.VERSION_CHECK_ENABLED, false ) .withBackendProperty( - ElasticsearchBackendSpiSettings.CLIENT_FACTORY, + ElasticsearchBackendSpiSettings.Radicals.CLIENT_FACTORY, elasticsearchClientSpy.factoryReference() ) .withSchemaManagement( StubMappingSchemaManagementStrategy.DROP_ON_SHUTDOWN_ONLY ) @@ -146,7 +146,7 @@ void noVersionCheck_incompleteVersion() { ElasticsearchBackendSettings.VERSION, versionWithMajorOnly ) .withBackendProperty( - ElasticsearchBackendSpiSettings.CLIENT_FACTORY, + ElasticsearchBackendSpiSettings.Radicals.CLIENT_FACTORY, elasticsearchClientSpy.factoryReference() ) .withSchemaManagement( StubMappingSchemaManagementStrategy.DROP_ON_SHUTDOWN_ONLY ) @@ -192,7 +192,7 @@ void noVersionCheck_completeVersion() { ElasticsearchBackendSettings.VERSION, configuredVersion ) .withBackendProperty( - ElasticsearchBackendSpiSettings.CLIENT_FACTORY, + ElasticsearchBackendSpiSettings.Radicals.CLIENT_FACTORY, elasticsearchClientSpy.factoryReference() ) .withSchemaManagement( StubMappingSchemaManagementStrategy.DROP_ON_SHUTDOWN_ONLY ) @@ -243,7 +243,7 @@ void noVersionCheck_versionOverrideOnStart_incompatibleVersion() { ElasticsearchBackendSettings.VERSION, configuredVersionOnBackendCreation ) .withBackendProperty( - ElasticsearchBackendSpiSettings.CLIENT_FACTORY, + ElasticsearchBackendSpiSettings.Radicals.CLIENT_FACTORY, elasticsearchClientSpy.factoryReference() ) .withSchemaManagement( StubMappingSchemaManagementStrategy.DROP_ON_SHUTDOWN_ONLY ) @@ -296,7 +296,7 @@ void noVersionCheck_versionOverrideOnStart_compatibleVersion() { ElasticsearchBackendSettings.VERSION, versionWithMajorOnly ) .withBackendProperty( - ElasticsearchBackendSpiSettings.CLIENT_FACTORY, + ElasticsearchBackendSpiSettings.Radicals.CLIENT_FACTORY, elasticsearchClientSpy.factoryReference() ) .withSchemaManagement( StubMappingSchemaManagementStrategy.DROP_ON_SHUTDOWN_ONLY ) @@ -341,7 +341,7 @@ void noVersionCheck_customSettingsAndMapping() { SearchSetupHelper.PartialSetup partialSetup = setupHelper.start() .withBackendProperty( ElasticsearchBackendSettings.VERSION, configuredVersion ) - .withBackendProperty( ElasticsearchBackendSpiSettings.CLIENT_FACTORY, + .withBackendProperty( ElasticsearchBackendSpiSettings.Radicals.CLIENT_FACTORY, elasticsearchClientSpy.factoryReference() ) .withBackendProperty( ElasticsearchIndexSettings.SCHEMA_MANAGEMENT_SETTINGS_FILE, "bootstrap-it/custom-settings.json" ) diff --git a/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/mapping/ElasticsearchFieldAttributesIT.java b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/mapping/ElasticsearchFieldAttributesIT.java index 8f7f9bc94e1..ebee12353da 100644 --- a/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/mapping/ElasticsearchFieldAttributesIT.java +++ b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/mapping/ElasticsearchFieldAttributesIT.java @@ -110,7 +110,7 @@ private void matchMapping(Consumer mapping, JsonObject prope ); setupHelper.start() - .withBackendProperty( ElasticsearchBackendSpiSettings.CLIENT_FACTORY, clientSpy.factoryReference() ) + .withBackendProperty( ElasticsearchBackendSpiSettings.Radicals.CLIENT_FACTORY, clientSpy.factoryReference() ) .withIndex( index ) .setup(); diff --git a/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/mapping/ElasticsearchFieldTypesIT.java b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/mapping/ElasticsearchFieldTypesIT.java index 76f759b78f7..d96ec260678 100644 --- a/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/mapping/ElasticsearchFieldTypesIT.java +++ b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/mapping/ElasticsearchFieldTypesIT.java @@ -62,7 +62,7 @@ void test() { setupHelper.start() .withBackendProperty( - ElasticsearchBackendSpiSettings.CLIENT_FACTORY, clientSpy.factoryReference() + ElasticsearchBackendSpiSettings.Radicals.CLIENT_FACTORY, clientSpy.factoryReference() ) .withIndex( index ) .setup(); diff --git a/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/mapping/ElasticsearchTypeNameMappingSchemaIT.java b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/mapping/ElasticsearchTypeNameMappingSchemaIT.java index 6691dc84bfa..3251e68da1c 100644 --- a/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/mapping/ElasticsearchTypeNameMappingSchemaIT.java +++ b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/mapping/ElasticsearchTypeNameMappingSchemaIT.java @@ -75,7 +75,7 @@ void schema(String strategyName, JsonObject expectedMappingContent) { setupHelper.start() .withBackendProperty( - ElasticsearchBackendSpiSettings.CLIENT_FACTORY, clientSpy.factoryReference() + ElasticsearchBackendSpiSettings.Radicals.CLIENT_FACTORY, clientSpy.factoryReference() ) .withBackendProperty( // Don't contribute any analysis definitions, it messes with our assertions diff --git a/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/search/query/ElasticsearchSearchQueryIT.java b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/search/query/ElasticsearchSearchQueryIT.java index df03220623f..b125162e139 100644 --- a/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/search/query/ElasticsearchSearchQueryIT.java +++ b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/search/query/ElasticsearchSearchQueryIT.java @@ -58,7 +58,7 @@ public static List params() { public void init(Object layoutStrategy, URLEncodedString readName) { setupHelper.start() .withBackendProperty( - ElasticsearchBackendSpiSettings.CLIENT_FACTORY, clientSpy.factoryReference() + ElasticsearchBackendSpiSettings.Radicals.CLIENT_FACTORY, clientSpy.factoryReference() ) .withBackendProperty( ElasticsearchBackendSettings.LAYOUT_STRATEGY, layoutStrategy diff --git a/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/search/query/ElasticsearchSearchQueryRequestTransformerIT.java b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/search/query/ElasticsearchSearchQueryRequestTransformerIT.java index 33d435fad6c..68087a46842 100644 --- a/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/search/query/ElasticsearchSearchQueryRequestTransformerIT.java +++ b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/search/query/ElasticsearchSearchQueryRequestTransformerIT.java @@ -53,7 +53,7 @@ class ElasticsearchSearchQueryRequestTransformerIT { void setup() { setupHelper.start() .withBackendProperty( - ElasticsearchBackendSpiSettings.CLIENT_FACTORY, clientSpy.factoryReference() + ElasticsearchBackendSpiSettings.Radicals.CLIENT_FACTORY, clientSpy.factoryReference() ) .withIndexes( mainIndex, otherIndex ) .setup(); diff --git a/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/testsupport/util/ElasticsearchTckBackendSetupStrategy.java b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/testsupport/util/ElasticsearchTckBackendSetupStrategy.java index ab5f613f89c..a84333b0317 100644 --- a/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/testsupport/util/ElasticsearchTckBackendSetupStrategy.java +++ b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/testsupport/util/ElasticsearchTckBackendSetupStrategy.java @@ -4,6 +4,7 @@ */ package org.hibernate.search.integrationtest.backend.elasticsearch.testsupport.util; +import org.hibernate.search.backend.elasticsearch.cfg.spi.ElasticsearchBackendSpiSettings; import org.hibernate.search.engine.environment.bean.BeanReference; import org.hibernate.search.integrationtest.backend.elasticsearch.testsupport.configuration.DefaultITAnalysisConfigurer; import org.hibernate.search.integrationtest.backend.tck.testsupport.util.TckBackendAccessor; @@ -13,10 +14,33 @@ import org.hibernate.search.util.impl.integrationtest.common.TestConfigurationProvider; class ElasticsearchTckBackendSetupStrategy extends TckBackendSetupStrategy { + private static final String ELASTICSEARCH_BACKEND_CLIENT_TYPE_PROPERTY_KEY = + "org.hibernate.search.integrationtest.backend.elasticsearch.client.type"; + private static final String ELASTICSEARCH_BACKEND_CLIENT_TYPE; + + // Uncomment one of the following lines to set the backend type when running tests from the IDE + public static final String IDE_ELASTICSEARCH_BACKEND_CLIENT_TYPE = null; + // public static final String IDE_ELASTICSEARCH_BACKEND_CLIENT_TYPE = "default"; + // public static final String IDE_ELASTICSEARCH_BACKEND_CLIENT_TYPE = "jdk-rest-client"; + // public static final String IDE_ELASTICSEARCH_BACKEND_CLIENT_TYPE = "opensearch-rest-client"; + // public static final String IDE_ELASTICSEARCH_BACKEND_CLIENT_TYPE = "elasticsearch-rest5"; + + static { + String property = System.getProperty( ELASTICSEARCH_BACKEND_CLIENT_TYPE_PROPERTY_KEY ); + if ( property == null ) { + ELASTICSEARCH_BACKEND_CLIENT_TYPE = IDE_ELASTICSEARCH_BACKEND_CLIENT_TYPE; + } + else { + ELASTICSEARCH_BACKEND_CLIENT_TYPE = property; + } + } ElasticsearchTckBackendSetupStrategy() { super( new ElasticsearchBackendConfiguration() ); setProperty( "analysis.configurer", BeanReference.ofInstance( new DefaultITAnalysisConfigurer() ) ); + if ( ELASTICSEARCH_BACKEND_CLIENT_TYPE != null ) { + setProperty( ElasticsearchBackendSpiSettings.Radicals.CLIENT_FACTORY, ELASTICSEARCH_BACKEND_CLIENT_TYPE ); + } } @Override diff --git a/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/work/ElasticsearchIndexingIT.java b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/work/ElasticsearchIndexingIT.java index d45cc714ad3..cd7b40df0cc 100644 --- a/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/work/ElasticsearchIndexingIT.java +++ b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/work/ElasticsearchIndexingIT.java @@ -61,7 +61,7 @@ public static List params() { public void init(Object layoutStrategy, URLEncodedString writeName) { setupHelper.start() .withBackendProperty( - ElasticsearchBackendSpiSettings.CLIENT_FACTORY, clientSpy.factoryReference() + ElasticsearchBackendSpiSettings.Radicals.CLIENT_FACTORY, clientSpy.factoryReference() ) .withBackendProperty( ElasticsearchBackendSettings.LAYOUT_STRATEGY, layoutStrategy diff --git a/util/internal/integrationtest/common/src/main/java/org/hibernate/search/util/impl/integrationtest/common/extension/BackendConfiguration.java b/util/internal/integrationtest/common/src/main/java/org/hibernate/search/util/impl/integrationtest/common/extension/BackendConfiguration.java index 8b0d043c802..97b4321c9e6 100644 --- a/util/internal/integrationtest/common/src/main/java/org/hibernate/search/util/impl/integrationtest/common/extension/BackendConfiguration.java +++ b/util/internal/integrationtest/common/src/main/java/org/hibernate/search/util/impl/integrationtest/common/extension/BackendConfiguration.java @@ -15,12 +15,22 @@ public abstract class BackendConfiguration { private static final String BACKEND_TYPE_PROPERTY_KEY = "org.hibernate.search.integrationtest.backend.type"; + private static final String ELASTICSEARCH_BACKEND_CLIENT_TYPE_PROPERTY_KEY = + "org.hibernate.search.integrationtest.backend.elasticsearch.client.type"; // Uncomment one of the following lines to set the backend type when running tests from the IDE public static final String IDE_BACKEND_TYPE = "lucene"; // public static final String IDE_BACKEND_TYPE = "elasticsearch"; + // Uncomment one of the following lines to set the backend type when running tests from the IDE + public static final String IDE_ELASTICSEARCH_BACKEND_CLIENT_TYPE = null; + // public static final String IDE_ELASTICSEARCH_BACKEND_CLIENT_TYPE = "default"; + // public static final String IDE_ELASTICSEARCH_BACKEND_CLIENT_TYPE = "jdk-rest-client"; + // public static final String IDE_ELASTICSEARCH_BACKEND_CLIENT_TYPE = "opensearch-rest-client"; + // public static final String IDE_ELASTICSEARCH_BACKEND_CLIENT_TYPE = "elasticsearch-rest5"; + public static final String BACKEND_TYPE; + public static final String ELASTICSEARCH_BACKEND_CLIENT_TYPE; public static final boolean IS_IDE; static { String property = System.getProperty( BACKEND_TYPE_PROPERTY_KEY ); @@ -38,6 +48,18 @@ public abstract class BackendConfiguration { BACKEND_TYPE = property; IS_IDE = false; } + if ( isElasticsearch() ) { + property = System.getProperty( ELASTICSEARCH_BACKEND_CLIENT_TYPE_PROPERTY_KEY ); + if ( property == null ) { + ELASTICSEARCH_BACKEND_CLIENT_TYPE = IDE_ELASTICSEARCH_BACKEND_CLIENT_TYPE; + } + else { + ELASTICSEARCH_BACKEND_CLIENT_TYPE = property; + } + } + else { + ELASTICSEARCH_BACKEND_CLIENT_TYPE = null; + } } public static boolean isElasticsearch() { @@ -62,6 +84,9 @@ public final Map backendProperties(TestConfigurationProvider con // More than one backend type in the classpath, we have to set it explicitly. rawBackendProperties.put( BackendSettings.TYPE, BACKEND_TYPE ); } + if ( ELASTICSEARCH_BACKEND_CLIENT_TYPE != null ) { + rawBackendProperties.put( "client_factory", ELASTICSEARCH_BACKEND_CLIENT_TYPE ); + } return configurationProvider.interpolateProperties( rawBackendProperties ); }