diff --git a/backend/elasticsearch-aws/pom.xml b/backend/elasticsearch-aws/pom.xml index 70e76c57b55..7b23e913a40 100644 --- a/backend/elasticsearch-aws/pom.xml +++ b/backend/elasticsearch-aws/pom.xml @@ -26,7 +26,7 @@ org.hibernate.search - hibernate-search-backend-elasticsearch + hibernate-search-backend-elasticsearch-client-common software.amazon.awssdk diff --git a/backend/elasticsearch-aws/src/main/java/org/hibernate/search/backend/elasticsearch/aws/impl/AwsSigningRequestInterceptor.java b/backend/elasticsearch-aws/src/main/java/org/hibernate/search/backend/elasticsearch/aws/impl/AwsSigningRequestInterceptor.java deleted file mode 100644 index 2f58f68ef45..00000000000 --- a/backend/elasticsearch-aws/src/main/java/org/hibernate/search/backend/elasticsearch/aws/impl/AwsSigningRequestInterceptor.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * Copyright Red Hat Inc. and Hibernate Authors - */ -package org.hibernate.search.backend.elasticsearch.aws.impl; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -import org.hibernate.search.backend.elasticsearch.aws.logging.impl.AwsLog; -import org.hibernate.search.util.common.AssertionFailure; - -import org.apache.http.HttpEntity; -import org.apache.http.HttpEntityEnclosingRequest; -import org.apache.http.HttpHost; -import org.apache.http.HttpRequest; -import org.apache.http.HttpRequestInterceptor; -import org.apache.http.NameValuePair; -import org.apache.http.RequestLine; -import org.apache.http.client.utils.URLEncodedUtils; -import org.apache.http.protocol.HttpContext; -import org.apache.http.protocol.HttpCoreContext; -import software.amazon.awssdk.auth.credentials.AwsCredentials; -import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; -import software.amazon.awssdk.http.ContentStreamProvider; -import software.amazon.awssdk.http.SdkHttpFullRequest; -import software.amazon.awssdk.http.SdkHttpMethod; -import software.amazon.awssdk.http.auth.aws.signer.AwsV4HttpSigner; -import software.amazon.awssdk.http.auth.spi.signer.SignedRequest; -import software.amazon.awssdk.regions.Region; - -class AwsSigningRequestInterceptor implements HttpRequestInterceptor { - - private final AwsV4HttpSigner signer; - private final Region region; - private final String service; - private final AwsCredentialsProvider credentialsProvider; - - AwsSigningRequestInterceptor(Region region, String service, AwsCredentialsProvider credentialsProvider) { - this.signer = AwsV4HttpSigner.create(); - this.region = region; - this.service = service; - this.credentialsProvider = credentialsProvider; - } - - @Override - public void process(HttpRequest request, HttpContext context) throws IOException { - try ( HttpEntityContentStreamProvider contentStreamProvider = extractEntityContent( request ) ) { - sign( request, context, contentStreamProvider ); - } - } - - private void sign(HttpRequest request, HttpContext context, HttpEntityContentStreamProvider contentStreamProvider) { - SdkHttpFullRequest awsRequest = toAwsRequest( request, context, contentStreamProvider ); - - if ( AwsLog.INSTANCE.isTraceEnabled() ) { - AwsLog.INSTANCE.httpRequestBeforeSigning( request ); - AwsLog.INSTANCE.awsRequestBeforeSigning( awsRequest ); - } - - AwsCredentials credentials = credentialsProvider.resolveCredentials(); - AwsLog.INSTANCE.awsCredentials( credentials ); - - SignedRequest signedRequest = signer.sign( r -> r.identity( credentials ) - .request( awsRequest ) - .payload( awsRequest.contentStreamProvider().orElse( null ) ) - .putProperty( AwsV4HttpSigner.SERVICE_SIGNING_NAME, service ) - .putProperty( AwsV4HttpSigner.REGION_NAME, region.id() ) ); - - // The AWS SDK added some headers. - // Let's just override the existing headers with whatever the AWS SDK came up with. - // We don't expect signing to affect anything else (path, query, content, ...). - for ( Map.Entry> header : signedRequest.request().headers().entrySet() ) { - String name = header.getKey(); - boolean first = true; - for ( String value : header.getValue() ) { - if ( first ) { - request.setHeader( name, value ); - first = false; - } - else { - request.addHeader( name, value ); - } - } - } - - if ( AwsLog.INSTANCE.isTraceEnabled() ) { - AwsLog.INSTANCE.httpRequestAfterSigning( signedRequest ); - AwsLog.INSTANCE.awsRequestAfterSigning( request ); - } - } - - private SdkHttpFullRequest toAwsRequest(HttpRequest request, HttpContext context, - ContentStreamProvider contentStreamProvider) { - SdkHttpFullRequest.Builder awsRequestBuilder = SdkHttpFullRequest.builder(); - - HttpCoreContext coreContext = HttpCoreContext.adapt( context ); - HttpHost targetHost = coreContext.getTargetHost(); - awsRequestBuilder.host( targetHost.getHostName() ); - awsRequestBuilder.port( targetHost.getPort() ); - awsRequestBuilder.protocol( targetHost.getSchemeName() ); - - RequestLine requestLine = request.getRequestLine(); - awsRequestBuilder.method( SdkHttpMethod.fromValue( requestLine.getMethod() ) ); - - String pathAndQuery = requestLine.getUri(); - String path; - List queryParameters; - int queryStart = pathAndQuery.indexOf( '?' ); - if ( queryStart >= 0 ) { - path = pathAndQuery.substring( 0, queryStart ); - queryParameters = URLEncodedUtils.parse( pathAndQuery.substring( queryStart + 1 ), StandardCharsets.UTF_8 ); - } - else { - path = pathAndQuery; - queryParameters = Collections.emptyList(); - } - - // For some reason this is needed on Amazon OpenSearch Serverless - if ( "aoss".equals( service ) ) { - awsRequestBuilder.appendHeader( "x-amz-content-sha256", "required" ); - } - - awsRequestBuilder.encodedPath( path ); - for ( NameValuePair param : queryParameters ) { - awsRequestBuilder.appendRawQueryParameter( param.getName(), param.getValue() ); - } - - // Do NOT copy the headers, as the AWS SDK will sometimes sign some headers - // that are not properly taken into account by the AWS servers (e.g. content-length). - - awsRequestBuilder.contentStreamProvider( contentStreamProvider ); - - return awsRequestBuilder.build(); - } - - private HttpEntityContentStreamProvider extractEntityContent(HttpRequest request) { - if ( request instanceof HttpEntityEnclosingRequest ) { - HttpEntity entity = ( (HttpEntityEnclosingRequest) request ).getEntity(); - if ( entity == null ) { - return null; - } - if ( !entity.isRepeatable() ) { - throw new AssertionFailure( "Cannot sign AWS requests with non-repeatable entities" ); - } - return new HttpEntityContentStreamProvider( entity ); - } - else { - return null; - } - } - -} diff --git a/backend/elasticsearch-aws/src/main/java/org/hibernate/search/backend/elasticsearch/aws/impl/ElasticsearchAwsBeanConfigurer.java b/backend/elasticsearch-aws/src/main/java/org/hibernate/search/backend/elasticsearch/aws/impl/ElasticsearchAwsBeanConfigurer.java index 9f8c035159b..2182b37bcdf 100644 --- a/backend/elasticsearch-aws/src/main/java/org/hibernate/search/backend/elasticsearch/aws/impl/ElasticsearchAwsBeanConfigurer.java +++ b/backend/elasticsearch-aws/src/main/java/org/hibernate/search/backend/elasticsearch/aws/impl/ElasticsearchAwsBeanConfigurer.java @@ -6,7 +6,7 @@ import org.hibernate.search.backend.elasticsearch.aws.cfg.ElasticsearchAwsCredentialsTypeNames; import org.hibernate.search.backend.elasticsearch.aws.spi.ElasticsearchAwsCredentialsProvider; -import org.hibernate.search.backend.elasticsearch.client.ElasticsearchHttpClientConfigurer; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchRequestInterceptorProvider; import org.hibernate.search.engine.environment.bean.BeanHolder; import org.hibernate.search.engine.environment.bean.spi.BeanConfigurationContext; import org.hibernate.search.engine.environment.bean.spi.BeanConfigurer; @@ -17,8 +17,8 @@ public class ElasticsearchAwsBeanConfigurer implements BeanConfigurer { @Override public void configure(BeanConfigurationContext context) { context.define( - ElasticsearchHttpClientConfigurer.class, - beanResolver -> BeanHolder.of( new ElasticsearchAwsHttpClientConfigurer() ) + ElasticsearchRequestInterceptorProvider.class, + beanResolver -> BeanHolder.of( new ElasticsearchAwsSigningInterceptorProvider() ) ); context.define( ElasticsearchAwsCredentialsProvider.class, ElasticsearchAwsCredentialsTypeNames.DEFAULT, diff --git a/backend/elasticsearch-aws/src/main/java/org/hibernate/search/backend/elasticsearch/aws/impl/ElasticsearchAwsHttpClientConfigurer.java b/backend/elasticsearch-aws/src/main/java/org/hibernate/search/backend/elasticsearch/aws/impl/ElasticsearchAwsSigningInterceptorProvider.java similarity index 72% rename from backend/elasticsearch-aws/src/main/java/org/hibernate/search/backend/elasticsearch/aws/impl/ElasticsearchAwsHttpClientConfigurer.java rename to backend/elasticsearch-aws/src/main/java/org/hibernate/search/backend/elasticsearch/aws/impl/ElasticsearchAwsSigningInterceptorProvider.java index 03fdb970efe..008ad7247fc 100644 --- a/backend/elasticsearch-aws/src/main/java/org/hibernate/search/backend/elasticsearch/aws/impl/ElasticsearchAwsHttpClientConfigurer.java +++ b/backend/elasticsearch-aws/src/main/java/org/hibernate/search/backend/elasticsearch/aws/impl/ElasticsearchAwsSigningInterceptorProvider.java @@ -4,14 +4,16 @@ */ package org.hibernate.search.backend.elasticsearch.aws.impl; -import org.hibernate.search.backend.elasticsearch.ElasticsearchDistributionName; -import org.hibernate.search.backend.elasticsearch.ElasticsearchVersion; +import java.util.Locale; +import java.util.Optional; +import java.util.regex.Pattern; + import org.hibernate.search.backend.elasticsearch.aws.cfg.ElasticsearchAwsBackendSettings; import org.hibernate.search.backend.elasticsearch.aws.cfg.ElasticsearchAwsCredentialsTypeNames; import org.hibernate.search.backend.elasticsearch.aws.logging.impl.AwsLog; import org.hibernate.search.backend.elasticsearch.aws.spi.ElasticsearchAwsCredentialsProvider; -import org.hibernate.search.backend.elasticsearch.client.ElasticsearchHttpClientConfigurationContext; -import org.hibernate.search.backend.elasticsearch.client.ElasticsearchHttpClientConfigurer; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchRequestInterceptor; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchRequestInterceptorProvider; import org.hibernate.search.engine.cfg.ConfigurationPropertySource; import org.hibernate.search.engine.cfg.spi.ConfigurationProperty; import org.hibernate.search.engine.cfg.spi.OptionalConfigurationProperty; @@ -22,8 +24,8 @@ import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.regions.Region; -public class ElasticsearchAwsHttpClientConfigurer implements ElasticsearchHttpClientConfigurer { - +public class ElasticsearchAwsSigningInterceptorProvider implements ElasticsearchRequestInterceptorProvider { + private static final Pattern DISTRIBUTION_NAME_PATTERN = Pattern.compile( "([^\\d]+)?(?:(?<=^)|(?=$)|(?<=.):(?=.))(.+)?" ); private static final ConfigurationProperty SIGNING_ENABLED = ConfigurationProperty.forKey( ElasticsearchAwsBackendSettings.SIGNING_ENABLED ) .asBoolean() @@ -52,36 +54,48 @@ public class ElasticsearchAwsHttpClientConfigurer implements ElasticsearchHttpCl .asString() .build(); + static final OptionalConfigurationProperty DISTRIBUTION_NAME = + ConfigurationProperty.forKey( "version" ) + .asString() + .build(); + @Override - public void configure(ElasticsearchHttpClientConfigurationContext context) { + public Optional provide(Context context) { ConfigurationPropertySource propertySource = context.configurationPropertySource(); if ( !SIGNING_ENABLED.get( propertySource ) ) { AwsLog.INSTANCE.signingDisabled(); - return; + return Optional.empty(); } Region region = REGION.getAndMapOrThrow( propertySource, Region::of, AwsLog.INSTANCE::missingPropertyForSigning ); String service; - switch ( context.configuredVersion().map( ElasticsearchVersion::distribution ) - .orElse( ElasticsearchDistributionName.OPENSEARCH ) ) { - case AMAZON_OPENSEARCH_SERVERLESS: - service = "aoss"; - break; - case ELASTIC: - case OPENSEARCH: - default: - service = "es"; - break; + + String distributionName = DISTRIBUTION_NAME.getAndTransform( propertySource, + v -> v.map( ver -> ver.toLowerCase( Locale.ROOT ) ) + .map( DISTRIBUTION_NAME_PATTERN::matcher ) + .map( matcher -> { + if ( matcher.matches() ) { + return matcher.group( 1 ); + } + return null; + } ).orElse( "opensearch" ) ); + + if ( "amazon-opensearch-serverless".equals( distributionName ) ) { + service = "aoss"; } + else { + service = "es"; + } + AwsCredentialsProvider credentialsProvider = createCredentialsProvider( context.beanResolver(), propertySource ); AwsLog.INSTANCE.signingEnabled( region, service, credentialsProvider ); - AwsSigningRequestInterceptor signingInterceptor = - new AwsSigningRequestInterceptor( region, service, credentialsProvider ); + ElasticsearchAwsSigningRequestInterceptor signingInterceptor = + new ElasticsearchAwsSigningRequestInterceptor( region, service, credentialsProvider ); - context.clientBuilder().addInterceptorLast( signingInterceptor ); + return Optional.of( signingInterceptor ); } private AwsCredentialsProvider createCredentialsProvider(BeanResolver beanResolver, diff --git a/backend/elasticsearch-aws/src/main/java/org/hibernate/search/backend/elasticsearch/aws/impl/ElasticsearchAwsSigningRequestInterceptor.java b/backend/elasticsearch-aws/src/main/java/org/hibernate/search/backend/elasticsearch/aws/impl/ElasticsearchAwsSigningRequestInterceptor.java new file mode 100644 index 00000000000..ffb244003d1 --- /dev/null +++ b/backend/elasticsearch-aws/src/main/java/org/hibernate/search/backend/elasticsearch/aws/impl/ElasticsearchAwsSigningRequestInterceptor.java @@ -0,0 +1,101 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.aws.impl; + +import java.io.IOException; + +import org.hibernate.search.backend.elasticsearch.aws.logging.impl.AwsLog; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchRequestInterceptor; + +import software.amazon.awssdk.auth.credentials.AwsCredentials; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.http.ContentStreamProvider; +import software.amazon.awssdk.http.SdkHttpFullRequest; +import software.amazon.awssdk.http.SdkHttpMethod; +import software.amazon.awssdk.http.auth.aws.signer.AwsV4HttpSigner; +import software.amazon.awssdk.http.auth.spi.signer.SignedRequest; +import software.amazon.awssdk.regions.Region; + +class ElasticsearchAwsSigningRequestInterceptor implements ElasticsearchRequestInterceptor { + + private final AwsV4HttpSigner signer; + private final Region region; + private final String service; + private final AwsCredentialsProvider credentialsProvider; + + ElasticsearchAwsSigningRequestInterceptor(Region region, String service, AwsCredentialsProvider credentialsProvider) { + this.signer = AwsV4HttpSigner.create(); + this.region = region; + this.service = service; + this.credentialsProvider = credentialsProvider; + } + + @Override + public void intercept(RequestContext requestContext) throws IOException { + try ( HttpEntityContentStreamProvider contentStreamProvider = + HttpEntityContentStreamProvider.create( requestContext ) ) { + sign( requestContext, contentStreamProvider ); + } + } + + private void sign(RequestContext requestContext, HttpEntityContentStreamProvider contentStreamProvider) { + SdkHttpFullRequest awsRequest = toAwsRequest( requestContext, contentStreamProvider ); + + if ( AwsLog.INSTANCE.isTraceEnabled() ) { + AwsLog.INSTANCE.httpRequestBeforeSigning( requestContext ); + AwsLog.INSTANCE.awsRequestBeforeSigning( awsRequest ); + } + + AwsCredentials credentials = credentialsProvider.resolveCredentials(); + AwsLog.INSTANCE.awsCredentials( credentials ); + + SignedRequest signedRequest = signer.sign( r -> r.identity( credentials ) + .request( awsRequest ) + .payload( awsRequest.contentStreamProvider().orElse( null ) ) + .putProperty( AwsV4HttpSigner.SERVICE_SIGNING_NAME, service ) + .putProperty( AwsV4HttpSigner.REGION_NAME, region.id() ) ); + + // The AWS SDK added some headers. + // Let's just override the existing headers with whatever the AWS SDK came up with. + // We don't expect signing to affect anything else (path, query, content, ...). + requestContext.overrideHeaders( signedRequest.request().headers() ); + + if ( AwsLog.INSTANCE.isTraceEnabled() ) { + AwsLog.INSTANCE.httpRequestAfterSigning( signedRequest ); + AwsLog.INSTANCE.awsRequestAfterSigning( requestContext ); + } + } + + private SdkHttpFullRequest toAwsRequest(RequestContext requestContext, + ContentStreamProvider contentStreamProvider) { + SdkHttpFullRequest.Builder awsRequestBuilder = SdkHttpFullRequest.builder(); + + awsRequestBuilder.host( requestContext.host() ); + awsRequestBuilder.port( requestContext.port() ); + awsRequestBuilder.protocol( requestContext.scheme() ); + + awsRequestBuilder.method( SdkHttpMethod.fromValue( requestContext.method() ) ); + + String path = requestContext.path(); + + // For some reason this is needed on Amazon OpenSearch Serverless + if ( "aoss".equals( service ) ) { + awsRequestBuilder.appendHeader( "x-amz-content-sha256", "required" ); + } + + awsRequestBuilder.encodedPath( path ); + for ( var param : requestContext.queryParameters().entrySet() ) { + awsRequestBuilder.appendRawQueryParameter( param.getKey(), param.getValue() ); + } + + // Do NOT copy the headers, as the AWS SDK will sometimes sign some headers + // that are not properly taken into account by the AWS servers (e.g. content-length). + + awsRequestBuilder.contentStreamProvider( contentStreamProvider ); + + return awsRequestBuilder.build(); + } + +} diff --git a/backend/elasticsearch-aws/src/main/java/org/hibernate/search/backend/elasticsearch/aws/impl/HttpEntityContentStreamProvider.java b/backend/elasticsearch-aws/src/main/java/org/hibernate/search/backend/elasticsearch/aws/impl/HttpEntityContentStreamProvider.java index 4ec2996b6aa..9474a53e00f 100644 --- a/backend/elasticsearch-aws/src/main/java/org/hibernate/search/backend/elasticsearch/aws/impl/HttpEntityContentStreamProvider.java +++ b/backend/elasticsearch-aws/src/main/java/org/hibernate/search/backend/elasticsearch/aws/impl/HttpEntityContentStreamProvider.java @@ -9,15 +9,23 @@ import java.io.InputStream; import java.io.UncheckedIOException; -import org.apache.http.HttpEntity; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchRequestInterceptor; + import software.amazon.awssdk.http.ContentStreamProvider; public class HttpEntityContentStreamProvider implements ContentStreamProvider, Closeable { - private final HttpEntity entity; + private final ElasticsearchRequestInterceptor.RequestContext requestContext; private InputStream previousStream; - public HttpEntityContentStreamProvider(HttpEntity entity) { - this.entity = entity; + public HttpEntityContentStreamProvider(ElasticsearchRequestInterceptor.RequestContext requestContext) { + this.requestContext = requestContext; + } + + public static HttpEntityContentStreamProvider create(ElasticsearchRequestInterceptor.RequestContext requestContext) { + if ( requestContext.hasContent() ) { + return new HttpEntityContentStreamProvider( requestContext ); + } + return null; } @Override @@ -25,7 +33,7 @@ public InputStream newStream() { try { // Believe it or not, the AWS SDK expects us to close previous streams ourselves... close(); - InputStream newStream = entity.getContent(); + InputStream newStream = requestContext.content(); previousStream = newStream; return newStream; } diff --git a/backend/elasticsearch-aws/src/main/java/org/hibernate/search/backend/elasticsearch/aws/logging/impl/AwsLog.java b/backend/elasticsearch-aws/src/main/java/org/hibernate/search/backend/elasticsearch/aws/logging/impl/AwsLog.java index 74a06359bc4..b3283068ad3 100644 --- a/backend/elasticsearch-aws/src/main/java/org/hibernate/search/backend/elasticsearch/aws/logging/impl/AwsLog.java +++ b/backend/elasticsearch-aws/src/main/java/org/hibernate/search/backend/elasticsearch/aws/logging/impl/AwsLog.java @@ -22,7 +22,6 @@ import org.jboss.logging.annotations.ValidIdRange; import org.jboss.logging.annotations.ValidIdRanges; -import org.apache.http.HttpRequest; import software.amazon.awssdk.auth.credentials.AwsCredentials; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.http.SdkHttpFullRequest; @@ -72,7 +71,7 @@ SearchException obsoleteAccessKeyIdOrSecretAccessKeyForSigning(String legacyAcce @LogMessage(level = TRACE) @Message(id = ID_OFFSET + 3, value = "HTTP request (before signing): %s") - void httpRequestBeforeSigning(HttpRequest request); + void httpRequestBeforeSigning(Object request); @LogMessage(level = TRACE) @Message(id = ID_OFFSET + 4, value = "AWS request (before signing): %s") @@ -88,7 +87,7 @@ SearchException obsoleteAccessKeyIdOrSecretAccessKeyForSigning(String legacyAcce @LogMessage(level = TRACE) @Message(id = ID_OFFSET + 7, value = "HTTP request (after signing): %s") - void awsRequestAfterSigning(HttpRequest request); + void awsRequestAfterSigning(Object request); @LogMessage(level = DEBUG) @Message(id = ID_OFFSET + 8, value = "AWS request signing is disabled.") diff --git a/backend/elasticsearch-client/common/pom.xml b/backend/elasticsearch-client/common/pom.xml new file mode 100644 index 00000000000..0dc4bf9eca5 --- /dev/null +++ b/backend/elasticsearch-client/common/pom.xml @@ -0,0 +1,53 @@ + + + + 4.0.0 + + org.hibernate.search + hibernate-search-parent-public + 8.2.0-SNAPSHOT + ../../../build/parents/public + + hibernate-search-backend-elasticsearch-client-common + + Hibernate Search Backend - Elasticsearch Client Common + Rest client SPI for the Elasticsearch backend + + + + false + org.hibernate.search.backend.elasticsearch.client.common + + + + + org.hibernate.search + hibernate-search-engine + + + org.jboss.logging + jboss-logging + + + org.jboss.logging + jboss-logging-annotations + + + com.google.code.gson + gson + + + + + + + org.moditect + moditect-maven-plugin + + + + diff --git a/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/cfg/ElasticsearchBackendClientCommonSettings.java b/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/cfg/ElasticsearchBackendClientCommonSettings.java new file mode 100644 index 00000000000..79e269450ff --- /dev/null +++ b/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/cfg/ElasticsearchBackendClientCommonSettings.java @@ -0,0 +1,103 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.common.cfg; + +import java.util.Collections; +import java.util.List; + +/** + * Common configuration properties for the Elasticsearch backend's 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 + */ +public final class ElasticsearchBackendClientCommonSettings { + + private ElasticsearchBackendClientCommonSettings() { + } + + /** + * The hostname and ports of the Elasticsearch servers to connect to. + *

+ * Expects a String representing a hostname and port such as {@code localhost} or {@code es.mycompany.com:4400}, + * or a String containing multiple such hostname-and-port strings separated by commas, + * or a {@code Collection} containing such hostname-and-port strings. + *

+ * Multiple servers may be specified for load-balancing: requests will be assigned to each host in turns. + *

+ * Setting this property at the same time as {@link #URIS} will lead to an exception being thrown on startup. + *

+ * Defaults to {@link Defaults#HOSTS}. + */ + public static final String HOSTS = "hosts"; + + /** + * The protocol to use when connecting to the Elasticsearch servers. + *

+ * Expects a String: either {@code http} or {@code https}. + *

+ * Setting this property at the same time as {@link #URIS} will lead to an exception being thrown on startup. + *

+ * Defaults to {@link Defaults#PROTOCOL}. + */ + public static final String PROTOCOL = "protocol"; + + /** + * The protocol, hostname and ports of the Elasticsearch servers to connect to. + *

+ * Expects either a String representing an URI such as {@code http://localhost} + * or {@code https://es.mycompany.com:4400}, + * or a String containing multiple such URIs separated by commas, + * or a {@code Collection} containing such URIs. + *

+ * All the URIs must specify the same protocol. + *

+ * Setting this property at the same time as {@link #HOSTS} or {@link #PROTOCOL} will lead to an exception being thrown on startup. + *

+ * Defaults to {@code http://localhost:9200}, unless {@link #HOSTS} or {@link #PROTOCOL} are set, in which case they take precedence. + */ + public static final String URIS = "uris"; + + /** + * Property for specifying the path prefix prepended to the request end point. + * Use the path prefix if your Elasticsearch instance is located at a specific context path. + *

+ * Defaults to {@link Defaults#PATH_PREFIX}. + */ + public static final String PATH_PREFIX = "path_prefix"; + + /** + * The username to send when connecting to the Elasticsearch servers (HTTP authentication). + *

+ * Expects a String. + *

+ * Defaults to no username (anonymous access). + */ + public static final String USERNAME = "username"; + + /** + * The password to send when connecting to the Elasticsearch servers (HTTP authentication). + *

+ * Expects a String. + *

+ * Defaults to no username (anonymous access). + */ + public static final String PASSWORD = "password"; + + /** + * Default values for the different settings if no values are given. + */ + public static final class Defaults { + + private Defaults() { + } + + public static final List HOSTS = Collections.singletonList( "localhost:9200" ); + public static final String PROTOCOL = "http"; + public static final String PATH_PREFIX = ""; + } +} diff --git a/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/cfg/spi/ElasticsearchBackendClientSpiSettings.java b/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/cfg/spi/ElasticsearchBackendClientSpiSettings.java new file mode 100644 index 00000000000..6cdf9e7d899 --- /dev/null +++ b/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/cfg/spi/ElasticsearchBackendClientSpiSettings.java @@ -0,0 +1,20 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.common.cfg.spi; + +import org.hibernate.search.util.common.annotation.Incubating; + +/** + * Implementation-related settings. + */ +@Incubating +public final class ElasticsearchBackendClientSpiSettings { + + private ElasticsearchBackendClientSpiSettings() { + } + + public static final String CLIENT_FACTORY = "client_factory"; + +} diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/gson/spi/GsonProvider.java b/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/gson/spi/GsonProvider.java similarity index 54% rename from backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/gson/spi/GsonProvider.java rename to backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/gson/spi/GsonProvider.java index c6b767be61f..a0b2fc2a2bf 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/gson/spi/GsonProvider.java +++ b/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/gson/spi/GsonProvider.java @@ -2,15 +2,11 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright Red Hat Inc. and Hibernate Authors */ -package org.hibernate.search.backend.elasticsearch.gson.spi; +package org.hibernate.search.backend.elasticsearch.client.common.gson.spi; import java.util.Set; import java.util.function.Supplier; -import org.hibernate.search.backend.elasticsearch.lowlevel.index.mapping.impl.RootTypeMapping; -import org.hibernate.search.backend.elasticsearch.lowlevel.index.settings.impl.IndexSettings; -import org.hibernate.search.util.common.impl.CollectionHelper; - import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; @@ -21,24 +17,18 @@ public final class GsonProvider { /* - * See https://github.com/google/gson/issues/764. - * - * Only composite type adapters (referring to another type adapter) - * should be affected by this bug, so we'll list the corresponding types here. - * Maybe it's even narrower, and only type adapters that indirectly refer to themselves are affected, - * but I'm not entirely sure about that. - * - * Note we only need to list "root" types: - * all types they refer to will also have their type adapter initialized. + * https://github.com/google/gson/issues/764 is supposedly fixed. + * Hence, we may not need the workaround anymore. + * This version will be used in the test so we'll notice if things are still broken, for the main code + * the workaround is in place through the GsonProviderHelper. */ - private static final Set> TYPES_CAUSING_GSON_CONCURRENT_INITIALIZATION_BUG = - CollectionHelper.asImmutableSet( - TypeToken.get( IndexSettings.class ), - TypeToken.get( RootTypeMapping.class ) - ); - public static GsonProvider create(Supplier builderBaseSupplier, boolean logPrettyPrinting) { - return new GsonProvider( builderBaseSupplier, logPrettyPrinting ); + return create( builderBaseSupplier, logPrettyPrinting, Set.of() ); + } + + public static GsonProvider create(Supplier builderBaseSupplier, boolean logPrettyPrinting, + Set> typeTokensToInit) { + return new GsonProvider( builderBaseSupplier, logPrettyPrinting, typeTokensToInit ); } private final Gson gson; @@ -47,16 +37,17 @@ public static GsonProvider create(Supplier builderBaseSupplier, boo private final JsonLogHelper logHelper; - private GsonProvider(Supplier builderBaseSupplier, boolean logPrettyPrinting) { + private GsonProvider(Supplier builderBaseSupplier, boolean logPrettyPrinting, + Set> typeTokensToInit) { // Null serialization needs to be enabled to index null fields gson = builderBaseSupplier.get() .serializeNulls() .create(); - initializeTypeAdapters( gson ); + initializeTypeAdapters( gson, typeTokensToInit ); gsonNoSerializeNulls = builderBaseSupplier.get() .create(); - initializeTypeAdapters( gsonNoSerializeNulls ); + initializeTypeAdapters( gsonNoSerializeNulls, typeTokensToInit ); logHelper = JsonLogHelper.create( builderBaseSupplier.get(), logPrettyPrinting ); } @@ -81,8 +72,8 @@ public JsonLogHelper getLogHelper() { * We just initialize every adapter known to cause problems before we make the Gson object * available to multiple threads. */ - private static void initializeTypeAdapters(Gson gson) { - for ( TypeToken typeToken : TYPES_CAUSING_GSON_CONCURRENT_INITIALIZATION_BUG ) { + private static void initializeTypeAdapters(Gson gson, Set> typeTokensToInit) { + for ( TypeToken typeToken : typeTokensToInit ) { gson.getAdapter( typeToken ); } } diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/gson/spi/JsonLogHelper.java b/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/gson/spi/JsonLogHelper.java similarity index 96% rename from backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/gson/spi/JsonLogHelper.java rename to backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/gson/spi/JsonLogHelper.java index 40c22c826e4..028aac011ea 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/gson/spi/JsonLogHelper.java +++ b/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/gson/spi/JsonLogHelper.java @@ -2,7 +2,7 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright Red Hat Inc. and Hibernate Authors */ -package org.hibernate.search.backend.elasticsearch.gson.spi; +package org.hibernate.search.backend.elasticsearch.client.common.gson.spi; import java.io.PrintWriter; import java.io.StringWriter; diff --git a/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/logging/spi/ElasticsearchClientCommonLog.java b/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/logging/spi/ElasticsearchClientCommonLog.java new file mode 100644 index 00000000000..3d45c776b4e --- /dev/null +++ b/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/logging/spi/ElasticsearchClientCommonLog.java @@ -0,0 +1,57 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ + +package org.hibernate.search.backend.elasticsearch.client.common.logging.spi; + +import static org.jboss.logging.Logger.Level.TRACE; + +import org.hibernate.search.util.common.annotation.impl.SuppressJQAssistant; +import org.hibernate.search.util.common.logging.impl.MessageConstants; + +import org.jboss.logging.annotations.LogMessage; +import org.jboss.logging.annotations.Message; +import org.jboss.logging.annotations.MessageLogger; +import org.jboss.logging.annotations.ValidIdRange; +import org.jboss.logging.annotations.ValidIdRanges; + +@MessageLogger(projectCode = MessageConstants.PROJECT_CODE) +@ValidIdRanges({ + @ValidIdRange(min = MessageConstants.BACKEND_ES_CLIENT_ID_RANGE_MIN, + max = MessageConstants.BACKEND_ES_CLIENT_ID_RANGE_MAX), + // Exceptions for legacy messages from Search 5 (engine module) + @ValidIdRange(min = 35, max = 35), +}) +@SuppressJQAssistant( + reason = "Apache HTTP Client 5 uses a lot of classes/interfaces in the impl packages to create builders/instances etc. " + + + "So while it is bad to expose impl types ... in this case it's what Apache Client expects users to do?") +public interface ElasticsearchClientCommonLog + extends ElasticsearchRequestLog, ElasticsearchClientLog { + + // ----------------------------------- + // Pre-existing messages from Search 5 (ES module) + // DO NOT ADD ANY NEW MESSAGES HERE + // ----------------------------------- + int ID_OFFSET_LEGACY_ES = MessageConstants.BACKEND_ES_ID_RANGE_MIN; + + // ----------------------------------- + // New (old) messages from Search 6 onwards up until Search 8.2 + // ----------------------------------- + int ID_BACKEND_OFFSET = MessageConstants.BACKEND_ES_ID_RANGE_MIN + 500; + + // ----------------------------------- + // New messages from Search 8.2 onwards + // ----------------------------------- + int ID_OFFSET = MessageConstants.BACKEND_ES_CLIENT_ID_RANGE_MIN; + + /** + * Only here as a way to track the highest "already used id". + * When adding a new exception or log message use this id and bump the one + * here to the next value. + */ + @LogMessage(level = TRACE) + @Message(id = ID_OFFSET + 2, value = "") + void nextLoggerIdForConvenience(); +} diff --git a/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/logging/spi/ElasticsearchClientConfigurationLog.java b/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/logging/spi/ElasticsearchClientConfigurationLog.java new file mode 100644 index 00000000000..8816d58356a --- /dev/null +++ b/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/logging/spi/ElasticsearchClientConfigurationLog.java @@ -0,0 +1,101 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.common.logging.spi; + +import static org.hibernate.search.backend.elasticsearch.client.common.logging.spi.ElasticsearchClientCommonLog.ID_BACKEND_OFFSET; +import static org.hibernate.search.backend.elasticsearch.client.common.logging.spi.ElasticsearchClientCommonLog.ID_OFFSET; +import static org.hibernate.search.backend.elasticsearch.client.common.logging.spi.ElasticsearchClientCommonLog.ID_OFFSET_LEGACY_ES; + +import java.lang.invoke.MethodHandles; +import java.util.List; + +import org.hibernate.search.util.common.SearchException; +import org.hibernate.search.util.common.annotation.impl.SuppressJQAssistant; +import org.hibernate.search.util.common.logging.CategorizedLogger; +import org.hibernate.search.util.common.logging.impl.LoggerFactory; +import org.hibernate.search.util.common.logging.impl.MessageConstants; + +import org.jboss.logging.Logger; +import org.jboss.logging.annotations.Cause; +import org.jboss.logging.annotations.LogMessage; +import org.jboss.logging.annotations.Message; +import org.jboss.logging.annotations.MessageLogger; + +@CategorizedLogger( + category = ElasticsearchClientConfigurationLog.CATEGORY_NAME, + description = """ + Logs related to the Elasticsearch-specific backend configuration. + """ +) +@MessageLogger(projectCode = MessageConstants.PROJECT_CODE) +@SuppressJQAssistant( + reason = "Apache HTTP Client 5 uses a lot of classes/interfaces in the impl packages to create builders/instances etc. " + + + "So while it is bad to expose impl types ... in this case it's what Apache Client expects users to do?") +public interface ElasticsearchClientConfigurationLog { + String CATEGORY_NAME = "org.hibernate.search.configuration.elasticsearch.client"; + + ElasticsearchClientConfigurationLog INSTANCE = + LoggerFactory.make( ElasticsearchClientConfigurationLog.class, CATEGORY_NAME, MethodHandles.lookup() ); + + // ----------------------------------- + // Pre-existing messages from Search 5 (engine module) + // DO NOT ADD ANY NEW MESSAGES HERE + // ----------------------------------- + + @Message(id = ID_OFFSET_LEGACY_ES + 22, value = "Invalid index status: '%1$s'." + + " Valid statuses are: %2$s.") + SearchException invalidIndexStatus(String invalidRepresentation, List validRepresentations); + + @LogMessage(level = Logger.Level.WARN) + @Message(id = ID_OFFSET_LEGACY_ES + 73, + value = "Hibernate Search will connect to Elasticsearch with authentication over plain HTTP (not HTTPS)." + + " The password will be sent in clear text over the network.") + void usingPasswordOverHttp(); + + // ----------------------------------- + // New messages from Search 6 onwards + // ----------------------------------- + + @Message(id = ID_BACKEND_OFFSET + 89, value = "Invalid host/port: '%1$s'." + + " The host/port string must use the format 'host:port', for example 'mycompany.com:9200'" + + " The URI scheme ('http://', 'https://') must not be included.") + SearchException invalidHostAndPort(String hostAndPort, @Cause Exception e); + + @Message(id = ID_BACKEND_OFFSET + 126, value = "Invalid target hosts configuration:" + + " both the 'uris' property and the 'protocol' property are set." + + " Uris: '%1$s'. Protocol: '%2$s'." + + " Either set the protocol and hosts simultaneously using the 'uris' property," + + " or set them separately using the 'protocol' property and the 'hosts' property.") + SearchException uriAndProtocol(List uris, String protocol); + + @Message(id = ID_BACKEND_OFFSET + 127, value = "Invalid target hosts configuration:" + + " both the 'uris' property and the 'hosts' property are set." + + " Uris: '%1$s'. Hosts: '%2$s'." + + " Either set the protocol and hosts simultaneously using the 'uris' property," + + " or set them separately using the 'protocol' property and the 'hosts' property.") + SearchException uriAndHosts(List uris, List hosts); + + @Message(id = ID_BACKEND_OFFSET + 128, + value = "Invalid target hosts configuration: the 'uris' use different protocols (http, https)." + + " All URIs must use the same protocol. Uris: '%1$s'.") + SearchException differentProtocolsOnUris(List uris); + + @Message(id = ID_BACKEND_OFFSET + 129, + value = "Invalid target hosts configuration: the list of hosts must not be empty.") + SearchException emptyListOfHosts(); + + @Message(id = ID_BACKEND_OFFSET + 130, + value = "Invalid target hosts configuration: the list of URIs must not be empty.") + SearchException emptyListOfUris(); + + // ----------------------------------- + // New messages from Search 8.2 onwards + // ----------------------------------- + + @Message(id = ID_OFFSET + 1, value = "Invalid uri: '%1$s'. Reason: %2$s") + SearchException invalidUri(String uri, String reason, @Cause Exception e); + +} diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/logging/impl/ElasticsearchClientLog.java b/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/logging/spi/ElasticsearchClientLog.java similarity index 78% rename from backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/logging/impl/ElasticsearchClientLog.java rename to backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/logging/spi/ElasticsearchClientLog.java index 1fc7681d47a..9eee10e7c6e 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/logging/impl/ElasticsearchClientLog.java +++ b/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/logging/spi/ElasticsearchClientLog.java @@ -2,23 +2,25 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright Red Hat Inc. and Hibernate Authors */ -package org.hibernate.search.backend.elasticsearch.logging.impl; +package org.hibernate.search.backend.elasticsearch.client.common.logging.spi; -import static org.hibernate.search.backend.elasticsearch.logging.impl.ElasticsearchLog.ID_OFFSET; -import static org.hibernate.search.backend.elasticsearch.logging.impl.ElasticsearchLog.ID_OFFSET_LEGACY_ES; +import static org.hibernate.search.backend.elasticsearch.client.common.logging.spi.ElasticsearchClientCommonLog.ID_BACKEND_OFFSET; +import static org.hibernate.search.backend.elasticsearch.client.common.logging.spi.ElasticsearchClientCommonLog.ID_OFFSET_LEGACY_ES; import java.lang.invoke.MethodHandles; import java.time.Duration; import java.util.Set; import java.util.regex.Pattern; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchRequest; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchResponse; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; +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.URLEncodedString; import org.hibernate.search.util.common.AssertionFailure; import org.hibernate.search.util.common.SearchException; import org.hibernate.search.util.common.SearchTimeoutException; +import org.hibernate.search.util.common.annotation.impl.SuppressJQAssistant; import org.hibernate.search.util.common.logging.CategorizedLogger; +import org.hibernate.search.util.common.logging.impl.ClassFormatter; import org.hibernate.search.util.common.logging.impl.DurationInSecondsAndFractionsFormatter; import org.hibernate.search.util.common.logging.impl.LoggerFactory; import org.hibernate.search.util.common.logging.impl.MessageConstants; @@ -41,6 +43,10 @@ """ ) @MessageLogger(projectCode = MessageConstants.PROJECT_CODE) +@SuppressJQAssistant( + reason = "Apache HTTP Client 5 uses a lot of classes/interfaces in the impl packages to create builders/instances etc. " + + + "So while it is bad to expose impl types ... in this case it's what Apache Client expects users to do?") public interface ElasticsearchClientLog { String CATEGORY_NAME = "org.hibernate.search.elasticsearch.client"; @@ -103,12 +109,6 @@ SearchException unexpectedIndexStatus(URLEncodedString indexName, String expecte value = "Opened Elasticsearch index '%1$s' automatically.") void openedIndex(Object indexName); - @LogMessage(level = Logger.Level.WARN) - @Message(id = ID_OFFSET_LEGACY_ES + 73, - value = "Hibernate Search will connect to Elasticsearch with authentication over plain HTTP (not HTTPS)." - + " The password will be sent in clear text over the network.") - void usingPasswordOverHttp(); - @Message(id = ID_OFFSET_LEGACY_ES + 89, value = "Unable to parse Elasticsearch response. Status code was '%1$d', status phrase was '%2$s'." + " Nested exception: %3$s") @@ -122,49 +122,58 @@ SearchException failedToParseElasticsearchResponse(int statusCode, String status // ----------------------------------- // New messages from Search 6 onwards // ----------------------------------- - @Message(id = ID_OFFSET + 25, + + @Message(id = ID_BACKEND_OFFSET + 18, + value = "Invalid requested type for client: '%1$s'." + + " The Elasticsearch low-level client can only be unwrapped to '%2$s'.") + SearchException clientUnwrappingWithUnknownType(@FormatWith(ClassFormatter.class) Class requestedClass, + @FormatWith(ClassFormatter.class) Class actualClass); + + @Message(id = ID_BACKEND_OFFSET + 25, value = "Invalid field reference for this document element:" + " this document element has path '%1$s', but the referenced field has a parent with path '%2$s'.") SearchException invalidFieldForDocumentElement(String expectedPath, String actualPath); - @Message(id = ID_OFFSET + 26, + @Message(id = ID_BACKEND_OFFSET + 26, value = "Missing data in the Elasticsearch response.") AssertionFailure elasticsearchResponseMissingData(); - @Message(id = ID_OFFSET + 31, + @Message(id = ID_BACKEND_OFFSET + 31, value = "Unable to resolve index name '%1$s' to an entity type: %2$s") SearchException elasticsearchResponseUnknownIndexName(String elasticsearchIndexName, String causeMessage, @Cause Exception e); - @Message(id = ID_OFFSET + 44, value = "Unable to shut down the Elasticsearch client: %1$s") + @Message(id = ID_BACKEND_OFFSET + 44, value = "Unable to shut down the Elasticsearch client: %1$s") SearchException unableToShutdownClient(String causeMessage, @Cause Exception cause); - @Message(id = ID_OFFSET + 88, value = "Call to the bulk REST API failed: %1$s") + @Message(id = ID_BACKEND_OFFSET + 88, value = "Call to the bulk REST API failed: %1$s") SearchException elasticsearchFailedBecauseOfBulkFailure(String causeMessage, @Cause Throwable cause); - @Message(id = ID_OFFSET + 90, value = "Request execution exceeded the timeout of %1$s. Request was %2$s") + @Message(id = ID_BACKEND_OFFSET + 90, value = "Request execution exceeded the timeout of %1$s. Request was %2$s") SearchTimeoutException requestTimedOut(@FormatWith(DurationInSecondsAndFractionsFormatter.class) Duration timeout, @FormatWith(ElasticsearchRequestFormatter.class) ElasticsearchRequest request); - @Message(id = ID_OFFSET + 93, + @Message(id = ID_BACKEND_OFFSET + 93, value = "Invalid Elasticsearch index layout:" + " index names [%1$s, %2$s] resolve to multiple distinct indexes %3$s." + " These names must resolve to a single index.") SearchException elasticsearchIndexNameAndAliasesMatchMultipleIndexes(URLEncodedString write, URLEncodedString read, Set matchingIndexes); - @Message(id = ID_OFFSET + 94, + @Message(id = ID_BACKEND_OFFSET + 94, value = "Invalid Elasticsearch index layout:" + " primary (non-alias) name for existing Elasticsearch index '%1$s'" + " does not match the expected pattern '%2$s'.") SearchException invalidIndexPrimaryName(String elasticsearchIndexName, Pattern pattern); - @Message(id = ID_OFFSET + 95, + @Message(id = ID_BACKEND_OFFSET + 95, value = "Invalid Elasticsearch index layout:" + " unique key '%1$s' extracted from the index name does not match any of %2$s.") SearchException invalidIndexUniqueKey(String uniqueKey, Set knownKeys); - @Message(id = ID_OFFSET + 125, + @Message(id = ID_BACKEND_OFFSET + 125, value = "Unable to update aliases for index '%1$s': %2$s") SearchException elasticsearchAliasUpdateFailed(Object indexName, String causeMessage, @Cause Exception cause); + + } diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/logging/impl/ElasticsearchJsonObjectFormatter.java b/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/logging/spi/ElasticsearchJsonObjectFormatter.java similarity index 76% rename from backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/logging/impl/ElasticsearchJsonObjectFormatter.java rename to backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/logging/spi/ElasticsearchJsonObjectFormatter.java index 862faf27d71..54e3f6bfcb2 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/logging/impl/ElasticsearchJsonObjectFormatter.java +++ b/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/logging/spi/ElasticsearchJsonObjectFormatter.java @@ -2,9 +2,9 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright Red Hat Inc. and Hibernate Authors */ -package org.hibernate.search.backend.elasticsearch.logging.impl; +package org.hibernate.search.backend.elasticsearch.client.common.logging.spi; -import org.hibernate.search.backend.elasticsearch.gson.spi.JsonLogHelper; +import org.hibernate.search.backend.elasticsearch.client.common.gson.spi.JsonLogHelper; import com.google.gson.JsonObject; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/logging/impl/ElasticsearchRequestFormatter.java b/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/logging/spi/ElasticsearchRequestFormatter.java similarity index 85% rename from backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/logging/impl/ElasticsearchRequestFormatter.java rename to backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/logging/spi/ElasticsearchRequestFormatter.java index 4eab62a9cd8..22d128b04ff 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/logging/impl/ElasticsearchRequestFormatter.java +++ b/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/logging/spi/ElasticsearchRequestFormatter.java @@ -2,9 +2,9 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright Red Hat Inc. and Hibernate Authors */ -package org.hibernate.search.backend.elasticsearch.logging.impl; +package org.hibernate.search.backend.elasticsearch.client.common.logging.spi; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchRequest; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchRequest; /** * Used with JBoss Logging's {@link org.jboss.logging.annotations.FormatWith} diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/logging/impl/ElasticsearchRequestLog.java b/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/logging/spi/ElasticsearchRequestLog.java similarity index 75% rename from backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/logging/impl/ElasticsearchRequestLog.java rename to backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/logging/spi/ElasticsearchRequestLog.java index adf053d0eaa..a9d156a32ba 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/logging/impl/ElasticsearchRequestLog.java +++ b/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/logging/spi/ElasticsearchRequestLog.java @@ -2,13 +2,12 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright Red Hat Inc. and Hibernate Authors */ -package org.hibernate.search.backend.elasticsearch.logging.impl; - -import static org.hibernate.search.backend.elasticsearch.logging.impl.ElasticsearchLog.ID_OFFSET_LEGACY_ES; +package org.hibernate.search.backend.elasticsearch.client.common.logging.spi; import java.lang.invoke.MethodHandles; import java.util.Map; +import org.hibernate.search.util.common.annotation.impl.SuppressJQAssistant; import org.hibernate.search.util.common.logging.CategorizedLogger; import org.hibernate.search.util.common.logging.impl.LoggerFactory; import org.hibernate.search.util.common.logging.impl.MessageConstants; @@ -19,8 +18,6 @@ import org.jboss.logging.annotations.Message; import org.jboss.logging.annotations.MessageLogger; -import org.apache.http.HttpHost; - @CategorizedLogger( category = ElasticsearchRequestLog.CATEGORY_NAME, description = """ @@ -29,6 +26,10 @@ """ ) @MessageLogger(projectCode = MessageConstants.PROJECT_CODE) +@SuppressJQAssistant( + reason = "Apache HTTP Client 5 uses a lot of classes/interfaces in the impl packages to create builders/instances etc. " + + + "So while it is bad to expose impl types ... in this case it's what Apache Client expects users to do?") public interface ElasticsearchRequestLog extends BasicLogger { /** * This is the category of the Logger used to print out executed Elasticsearch requests, @@ -47,21 +48,21 @@ public interface ElasticsearchRequestLog extends BasicLogger { // ----------------------------------- @LogMessage(level = Logger.Level.DEBUG) - @Message(id = ID_OFFSET_LEGACY_ES + 82, + @Message(id = ElasticsearchClientCommonLog.ID_OFFSET_LEGACY_ES + 82, value = "Executed Elasticsearch HTTP %s request to '%s' with path '%s'," + " query parameters %s and %d objects in payload in %dms." + " Response had status %d '%s'. Request body: <%s>. Response body: <%s>") - void executedRequestWithFailure(String method, HttpHost host, String path, Map getParameters, + void executedRequestWithFailure(String method, String host, String path, Map getParameters, int bodyParts, long timeInMs, int responseStatusCode, String responseStatusMessage, String requestBodyParts, String responseBody); @LogMessage(level = Logger.Level.TRACE) - @Message(id = ID_OFFSET_LEGACY_ES + 93, + @Message(id = ElasticsearchClientCommonLog.ID_OFFSET_LEGACY_ES + 93, value = "Executed Elasticsearch HTTP %s request to '%s' with path '%s'," + " query parameters %s and %d objects in payload in %dms." + " Response had status %d '%s'. Request body: <%s>. Response body: <%s>") - void executedRequest(String method, HttpHost host, String path, Map getParameters, int bodyParts, + void executedRequest(String method, String host, String path, Map getParameters, int bodyParts, long timeInMs, int responseStatusCode, String responseStatusMessage, String requestBodyParts, String responseBody); diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/logging/impl/ElasticsearchResponseFormatter.java b/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/logging/spi/ElasticsearchResponseFormatter.java similarity index 82% rename from backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/logging/impl/ElasticsearchResponseFormatter.java rename to backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/logging/spi/ElasticsearchResponseFormatter.java index 97c1b9f5a09..432837c2da4 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/logging/impl/ElasticsearchResponseFormatter.java +++ b/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/logging/spi/ElasticsearchResponseFormatter.java @@ -2,10 +2,10 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright Red Hat Inc. and Hibernate Authors */ -package org.hibernate.search.backend.elasticsearch.logging.impl; +package org.hibernate.search.backend.elasticsearch.client.common.logging.spi; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchResponse; -import org.hibernate.search.backend.elasticsearch.gson.spi.JsonLogHelper; +import org.hibernate.search.backend.elasticsearch.client.common.gson.spi.JsonLogHelper; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchResponse; /** * Used with JBoss Logging's {@link org.jboss.logging.annotations.FormatWith} diff --git a/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/package-info.java b/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/package-info.java new file mode 100644 index 00000000000..c4a1719f07a --- /dev/null +++ b/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/package-info.java @@ -0,0 +1 @@ +package org.hibernate.search.backend.elasticsearch.client.common; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/spi/ElasticsearchClient.java b/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/spi/ElasticsearchClient.java similarity index 93% rename from backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/spi/ElasticsearchClient.java rename to backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/spi/ElasticsearchClient.java index 26c68da978b..c6ca6c6344d 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/spi/ElasticsearchClient.java +++ b/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/spi/ElasticsearchClient.java @@ -2,7 +2,7 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright Red Hat Inc. and Hibernate Authors */ -package org.hibernate.search.backend.elasticsearch.client.spi; +package org.hibernate.search.backend.elasticsearch.client.common.spi; import java.util.concurrent.CompletableFuture; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/spi/ElasticsearchClientFactory.java b/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/spi/ElasticsearchClientFactory.java similarity index 64% rename from backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/spi/ElasticsearchClientFactory.java rename to backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/spi/ElasticsearchClientFactory.java index e1dfd0e6652..f68d477b8a9 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/spi/ElasticsearchClientFactory.java +++ b/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/spi/ElasticsearchClientFactory.java @@ -2,27 +2,27 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright Red Hat Inc. and Hibernate Authors */ -package org.hibernate.search.backend.elasticsearch.client.spi; +package org.hibernate.search.backend.elasticsearch.client.common.spi; -import java.util.Optional; - -import org.hibernate.search.backend.elasticsearch.ElasticsearchVersion; -import org.hibernate.search.backend.elasticsearch.gson.spi.GsonProvider; +import org.hibernate.search.backend.elasticsearch.client.common.gson.spi.GsonProvider; import org.hibernate.search.engine.cfg.ConfigurationPropertySource; import org.hibernate.search.engine.common.execution.spi.SimpleScheduledExecutor; import org.hibernate.search.engine.environment.bean.BeanResolver; import org.hibernate.search.engine.environment.thread.spi.ThreadProvider; +import org.hibernate.search.util.common.annotation.Incubating; /** * Creates the Elasticsearch client. - * */ public interface ElasticsearchClientFactory { - ElasticsearchClientImplementor create(BeanResolver beanResolver, ConfigurationPropertySource propertySource, + @Incubating + String DEFAULT_BEAN_NAME = "default"; + + ElasticsearchClientImplementor create(BeanResolver beanResolver, + ConfigurationPropertySource propertySource, ThreadProvider threadProvider, String threadNamePrefix, SimpleScheduledExecutor timeoutExecutorService, - GsonProvider gsonProvider, - Optional configuredVersion); + GsonProvider gsonProvider); } diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/spi/ElasticsearchClientImplementor.java b/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/spi/ElasticsearchClientImplementor.java similarity index 80% rename from backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/spi/ElasticsearchClientImplementor.java rename to backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/spi/ElasticsearchClientImplementor.java index 920f4c10087..78c6e3ae400 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/spi/ElasticsearchClientImplementor.java +++ b/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/spi/ElasticsearchClientImplementor.java @@ -2,7 +2,7 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright Red Hat Inc. and Hibernate Authors */ -package org.hibernate.search.backend.elasticsearch.client.spi; +package org.hibernate.search.backend.elasticsearch.client.common.spi; /** * An interface allowing to close an {@link ElasticsearchClient}. diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/spi/ElasticsearchRequest.java b/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/spi/ElasticsearchRequest.java similarity index 96% rename from backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/spi/ElasticsearchRequest.java rename to backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/spi/ElasticsearchRequest.java index 8566b7c467f..ec811e90344 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/spi/ElasticsearchRequest.java +++ b/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/spi/ElasticsearchRequest.java @@ -2,7 +2,7 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright Red Hat Inc. and Hibernate Authors */ -package org.hibernate.search.backend.elasticsearch.client.spi; +package org.hibernate.search.backend.elasticsearch.client.common.spi; import java.util.ArrayList; import java.util.Collection; @@ -12,7 +12,7 @@ import java.util.Map; import java.util.StringJoiner; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; +import org.hibernate.search.backend.elasticsearch.client.common.util.spi.URLEncodedString; import org.hibernate.search.engine.common.timing.Deadline; import com.google.gson.JsonObject; diff --git a/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/spi/ElasticsearchRequestInterceptor.java b/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/spi/ElasticsearchRequestInterceptor.java new file mode 100644 index 00000000000..722a77b62e6 --- /dev/null +++ b/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/spi/ElasticsearchRequestInterceptor.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.common.spi; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Map; + +import org.hibernate.search.util.common.annotation.Incubating; + +@Incubating +public interface ElasticsearchRequestInterceptor { + + void intercept(RequestContext requestContext) throws IOException; + + interface RequestContext { + + boolean hasContent(); + + InputStream content() throws IOException; + + String scheme(); + + String host(); + + Integer port(); + + String method(); + + String path(); + + Map queryParameters(); + + void overrideHeaders(Map> headers); + + /** + * @return A String representation of the wrapped request. Primarily used for logging the request. + */ + String toString(); + } +} diff --git a/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/spi/ElasticsearchRequestInterceptorProvider.java b/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/spi/ElasticsearchRequestInterceptorProvider.java new file mode 100644 index 00000000000..3a9d030873a --- /dev/null +++ b/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/spi/ElasticsearchRequestInterceptorProvider.java @@ -0,0 +1,33 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.common.spi; + +import java.util.Optional; + +import org.hibernate.search.engine.cfg.ConfigurationPropertySource; +import org.hibernate.search.engine.environment.bean.BeanResolver; +import org.hibernate.search.util.common.annotation.Incubating; + +@Incubating +public interface ElasticsearchRequestInterceptorProvider { + + Optional provide(Context context); + + interface Context { + /** + * @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(); + } + +} diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/spi/ElasticsearchResponse.java b/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/spi/ElasticsearchResponse.java similarity index 68% rename from backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/spi/ElasticsearchResponse.java rename to backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/spi/ElasticsearchResponse.java index aa171dc9e20..4bb709bf107 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/spi/ElasticsearchResponse.java +++ b/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/spi/ElasticsearchResponse.java @@ -2,29 +2,24 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright Red Hat Inc. and Hibernate Authors */ -package org.hibernate.search.backend.elasticsearch.client.spi; +package org.hibernate.search.backend.elasticsearch.client.common.spi; import com.google.gson.JsonObject; -import org.apache.http.HttpHost; - public final class ElasticsearchResponse { - - private final HttpHost host; + private final String host; private final int statusCode; - private final String statusMessage; - private final JsonObject body; - public ElasticsearchResponse(HttpHost host, int statusCode, String statusMessage, JsonObject body) { + public ElasticsearchResponse(String host, int statusCode, String statusMessage, JsonObject body) { this.host = host; this.statusCode = statusCode; this.statusMessage = statusMessage; this.body = body; } - public HttpHost host() { + public String host() { return host; } @@ -39,5 +34,4 @@ public String statusMessage() { public JsonObject body() { return body; } - } diff --git a/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/util/spi/ElasticsearchClientUtils.java b/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/util/spi/ElasticsearchClientUtils.java new file mode 100644 index 00000000000..4ecc247a1ac --- /dev/null +++ b/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/util/spi/ElasticsearchClientUtils.java @@ -0,0 +1,18 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.common.util.spi; + + +public final class ElasticsearchClientUtils { + + private ElasticsearchClientUtils() { + // Private constructor + } + + public static boolean isSuccessCode(int code) { + return 200 <= code && code < 300; + } + +} diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/util/spi/URLEncodedString.java b/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/util/spi/URLEncodedString.java similarity index 95% rename from backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/util/spi/URLEncodedString.java rename to backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/util/spi/URLEncodedString.java index 46017026ec0..5ba628c34bf 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/util/spi/URLEncodedString.java +++ b/backend/elasticsearch-client/common/src/main/java/org/hibernate/search/backend/elasticsearch/client/common/util/spi/URLEncodedString.java @@ -2,7 +2,7 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright Red Hat Inc. and Hibernate Authors */ -package org.hibernate.search.backend.elasticsearch.util.spi; +package org.hibernate.search.backend.elasticsearch.client.common.util.spi; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; diff --git a/backend/elasticsearch-client/elasticsearch-java-client/pom.xml b/backend/elasticsearch-client/elasticsearch-java-client/pom.xml new file mode 100644 index 00000000000..a51b35768cf --- /dev/null +++ b/backend/elasticsearch-client/elasticsearch-java-client/pom.xml @@ -0,0 +1,84 @@ + + + + 4.0.0 + + org.hibernate.search + hibernate-search-parent-public + 8.2.0-SNAPSHOT + ../../../build/parents/public + + hibernate-search-backend-elasticsearch-client-java + + Hibernate Search Backend - Elasticsearch client based on the low-level Elasticsearch java client + Hibernate Search Elasticsearch client based on the low-level Elasticsearch java client + + + + false + org.hibernate.search.backend.elasticsearch.client.java + + + + + org.hibernate.search + hibernate-search-engine + + + org.hibernate.search + hibernate-search-backend-elasticsearch-client-common + + + co.elastic.clients + elasticsearch-java + + + org.apache.httpcomponents.client5 + httpclient5 + + + org.apache.httpcomponents.core5 + httpcore5 + + + org.jboss.logging + jboss-logging + + + org.jboss.logging + jboss-logging-annotations + + + com.google.code.gson + gson + + + + + com.fasterxml.jackson.core + jackson-core + + + + + org.hibernate.search + hibernate-search-util-internal-test-common + test + + + + + + + org.moditect + moditect-maven-plugin + + + + diff --git a/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/ElasticsearchHttpClientConfigurationContext.java b/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/ElasticsearchHttpClientConfigurationContext.java new file mode 100644 index 00000000000..e85f5f0424a --- /dev/null +++ b/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/ElasticsearchHttpClientConfigurationContext.java @@ -0,0 +1,42 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.java; + + +import org.hibernate.search.engine.cfg.ConfigurationPropertySource; +import org.hibernate.search.engine.environment.bean.BeanResolver; +import org.hibernate.search.util.common.annotation.impl.SuppressJQAssistant; + +import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder; + + +/** + * The context passed to {@link ElasticsearchHttpClientConfigurer}. + */ +@SuppressJQAssistant( + reason = "Apache HTTP Client 5 uses a lot of classes/interfaces in the impl packages to create builders/instances etc. " + + "So while it is bad to expose impl types ... in this case it's what Apache Client expects users to do?") +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 An Apache HTTP client builder, to set the configuration. + * @see the Apache HTTP Client documentation + */ + HttpAsyncClientBuilder clientBuilder(); + +} diff --git a/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/ElasticsearchHttpClientConfigurer.java b/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/ElasticsearchHttpClientConfigurer.java new file mode 100644 index 00000000000..1c3c61b59eb --- /dev/null +++ b/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/ElasticsearchHttpClientConfigurer.java @@ -0,0 +1,37 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.java; + +/** + * An extension point allowing fine tuning of the Apache 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. + */ +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 Apache HTTP client builder + * and configuration properties in particular. + */ + void configure(ElasticsearchHttpClientConfigurationContext context); + +} diff --git a/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/cfg/ClientJavaElasticsearchBackendClientSettings.java b/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/cfg/ClientJavaElasticsearchBackendClientSettings.java new file mode 100644 index 00000000000..4cd777bfb3b --- /dev/null +++ b/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/cfg/ClientJavaElasticsearchBackendClientSettings.java @@ -0,0 +1,138 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.java.cfg; + +import org.hibernate.search.backend.elasticsearch.client.common.cfg.ElasticsearchBackendClientCommonSettings; +import org.hibernate.search.backend.elasticsearch.client.java.ElasticsearchHttpClientConfigurer; + +/** + * 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 + */ +public final class ClientJavaElasticsearchBackendClientSettings { + + private ClientJavaElasticsearchBackendClientSettings() { + } + + /** + * 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 reading responses from an Elasticsearch server. + *

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

+ * Defaults to {@link Defaults#READ_TIMEOUT}. + */ + public static final String READ_TIMEOUT = "read_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"; + + /** + * The maximum number of simultaneous connections to the Elasticsearch cluster, + * all hosts taken together. + *

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

+ * Defaults to {@link Defaults#MAX_CONNECTIONS}. + */ + public static final String MAX_CONNECTIONS = "max_connections"; + + /** + * The maximum number of simultaneous connections to each host of the Elasticsearch cluster. + *

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

+ * Defaults to {@link Defaults#MAX_CONNECTIONS_PER_ROUTE}. + */ + public static final String MAX_CONNECTIONS_PER_ROUTE = "max_connections_per_route"; + + /** + * Whether automatic discovery of nodes in the Elasticsearch cluster is enabled. + *

+ * Expects a Boolean value such as {@code true} or {@code false}, + * or a string that can be parsed into a Boolean value. + *

+ * Defaults to {@link Defaults#DISCOVERY_ENABLED}. + */ + public static final String DISCOVERY_ENABLED = "discovery.enabled"; + + /** + * The time interval between two executions of the automatic discovery, if enabled. + *

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

+ * Defaults to {@link Defaults#DISCOVERY_REFRESH_INTERVAL}. + */ + public static final String DISCOVERY_REFRESH_INTERVAL = "discovery.refresh_interval"; + + /** + * How long connections to the Elasticsearch cluster can be kept idle. + *

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

+ * If the response from an Elasticsearch cluster contains a {@code Keep-Alive} header, + * then the effective max idle time will be whichever is lower: + * the duration from the {@code Keep-Alive} header or the value of this property (if set). + *

+ * If this property is not set, only the {@code Keep-Alive} header is considered, + * and if it's absent, idle connections will be kept forever. + */ + public static final String MAX_KEEP_ALIVE = "max_keep_alive"; + + /** + * 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 ElasticsearchBackendClientCommonSettings#USERNAME} or {@link #MAX_CONNECTIONS_PER_ROUTE}. + *

+ * 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 READ_TIMEOUT = 30000; + public static final int CONNECTION_TIMEOUT = 1000; + public static final int MAX_CONNECTIONS = 40; + public static final int MAX_CONNECTIONS_PER_ROUTE = 20; + public static final boolean DISCOVERY_ENABLED = false; + public static final int DISCOVERY_REFRESH_INTERVAL = 10; + } +} diff --git a/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/cfg/spi/ClientJavaElasticsearchBackendClientSpiSettings.java b/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/cfg/spi/ClientJavaElasticsearchBackendClientSpiSettings.java new file mode 100644 index 00000000000..047733f94ac --- /dev/null +++ b/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/cfg/spi/ClientJavaElasticsearchBackendClientSpiSettings.java @@ -0,0 +1,55 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.java.cfg.spi; + +import org.hibernate.search.engine.cfg.EngineSettings; + +/** + * Configuration properties for the Elasticsearch backend that are considered SPI (and not API). + */ +public final class ClientJavaElasticsearchBackendClientSpiSettings { + + /** + * The prefix expected for the key of every Hibernate Search configuration property. + */ + public static final String PREFIX = EngineSettings.PREFIX + "backend."; + + /** + * An external Elasticsearch 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 Elasticsearch, + * and all other client-related configuration properties + * (hosts/uris, authentication, discovery, timeouts, max connections, configurer, ...) + * will be ignored. + *

+ * Expects a reference to a bean of type {@link co.elastic.clients.transport.rest5_client.low_level.Rest5Client}. + *

+ * 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 ClientJavaElasticsearchBackendClientSpiSettings() { + } + + /** + * 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-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/impl/ByteBufferDataStreamChannel.java b/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/impl/ByteBufferDataStreamChannel.java new file mode 100644 index 00000000000..93b8394fd92 --- /dev/null +++ b/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/impl/ByteBufferDataStreamChannel.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.java.impl; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; + +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.nio.ContentEncoder; +import org.apache.hc.core5.http.nio.DataStreamChannel; + + +final class ByteBufferDataStreamChannel implements ContentEncoder, DataStreamChannel { + private final ByteBuffer buffer; + private boolean complete = false; + + ByteBufferDataStreamChannel(ByteBuffer buffer) { + this.buffer = buffer; + if ( !buffer.hasArray() ) { + throw new IllegalArgumentException( getClass().getName() + " requires a ByteBuffer backed by an array." ); + } + } + + @Override + public void requestOutput() { + + } + + @Override + public int write(ByteBuffer src) { + int toWrite = Math.min( src.remaining(), buffer.remaining() ); + src.get( buffer.array(), buffer.arrayOffset() + buffer.position(), toWrite ); + buffer.position( buffer.position() + toWrite ); + return toWrite; + } + + @Override + public void endStream() throws IOException { + complete = true; + } + + @Override + public void endStream(List trailers) throws IOException { + complete = true; + } + + @Override + public void complete(List trailers) throws IOException { + complete = true; + } + + @Override + public boolean isCompleted() { + return complete; + } +} diff --git a/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/impl/ClientJavaElasticsearchClient.java b/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/impl/ClientJavaElasticsearchClient.java new file mode 100644 index 00000000000..a4f4886ff85 --- /dev/null +++ b/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/impl/ClientJavaElasticsearchClient.java @@ -0,0 +1,275 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.java.impl; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Locale; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.hibernate.search.backend.elasticsearch.client.common.gson.spi.JsonLogHelper; +import org.hibernate.search.backend.elasticsearch.client.common.logging.spi.ElasticsearchClientLog; +import org.hibernate.search.backend.elasticsearch.client.common.logging.spi.ElasticsearchRequestLog; +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.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; + +import co.elastic.clients.transport.rest5_client.low_level.Request; +import co.elastic.clients.transport.rest5_client.low_level.RequestOptions; +import co.elastic.clients.transport.rest5_client.low_level.Response; +import co.elastic.clients.transport.rest5_client.low_level.ResponseException; +import co.elastic.clients.transport.rest5_client.low_level.ResponseListener; +import co.elastic.clients.transport.rest5_client.low_level.Rest5Client; +import co.elastic.clients.transport.rest5_client.low_level.sniffer.Sniffer; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.impl.EnglishReasonPhraseCatalog; +import org.apache.hc.core5.util.Timeout; + +public class ClientJavaElasticsearchClient implements ElasticsearchClientImplementor { + + private final BeanHolder restClientHolder; + + private final Sniffer sniffer; + + private final SimpleScheduledExecutor timeoutExecutorService; + + private final Optional requestTimeoutMs; + + private final Gson gson; + private final JsonLogHelper jsonLogHelper; + + ClientJavaElasticsearchClient(BeanHolder restClientHolder, Sniffer sniffer, + SimpleScheduledExecutor timeoutExecutorService, + Optional requestTimeoutMs, + Gson gson, JsonLogHelper jsonLogHelper) { + this.restClientHolder = restClientHolder; + this.sniffer = sniffer; + this.timeoutExecutorService = timeoutExecutorService; + this.requestTimeoutMs = requestTimeoutMs; + this.gson = gson; + this.jsonLogHelper = jsonLogHelper; + } + + @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 ( Rest5Client.class.isAssignableFrom( clientClass ) ) { + return (T) restClientHolder.get(); + } + throw ElasticsearchClientLog.INSTANCE.clientUnwrappingWithUnknownType( clientClass, Rest5Client.class ); + } + + private CompletableFuture send(ElasticsearchRequest elasticsearchRequest) { + CompletableFuture completableFuture = new CompletableFuture<>(); + + HttpEntity entity; + try { + entity = GsonHttpEntity.toEntity( gson, elasticsearchRequest ); + } + catch (IOException | RuntimeException e) { + completableFuture.completeExceptionally( e ); + return completableFuture; + } + + restClientHolder.get().performRequestAsync( + toRequest( elasticsearchRequest, entity ), + new ResponseListener() { + @Override + public void onSuccess(Response response) { + completableFuture.complete( response ); + } + + @Override + public void onFailure(Exception exception) { + if ( exception instanceof ResponseException ) { + /* + * The client tries to guess what's an error and what's not, but it's too naive. + * A 404 on DELETE is not always important to us, for instance. + * Thus we ignore the exception and do our own checks afterwards. + */ + completableFuture.complete( ( (ResponseException) exception ).getResponse() ); + } + else { + completableFuture.completeExceptionally( exception ); + } + } + } + ); + + Deadline deadline = elasticsearchRequest.deadline(); + if ( deadline == null && !requestTimeoutMs.isPresent() ) { + // 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? + * In any case, the RestClient doesn't return the Future from Apache HTTP client, + * so we can't do much until this changes. + */ + 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 Request toRequest(ElasticsearchRequest elasticsearchRequest, HttpEntity entity) { + Request request = new Request( elasticsearchRequest.method(), elasticsearchRequest.path() ); + setPerRequestSocketTimeout( elasticsearchRequest, request ); + + for ( Entry parameter : elasticsearchRequest.parameters().entrySet() ) { + request.addParameter( parameter.getKey(), parameter.getValue() ); + } + + request.setEntity( entity ); + + return request; + } + + private void setPerRequestSocketTimeout(ElasticsearchRequest elasticsearchRequest, Request 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; + RequestConfig requestConfig = RequestConfig.custom() + .setConnectionRequestTimeout( Timeout.DISABLED ) //Disable lease handling for the connection pool! See also HSEARCH-2681 + .setResponseTimeout( generalRequestTimeoutMs, TimeUnit.MILLISECONDS ) + .build(); + + RequestOptions.Builder requestOptions = RequestOptions.DEFAULT.toBuilder() + .setRequestConfig( requestConfig ); + + request.setOptions( requestOptions ); + } + + private ElasticsearchResponse convertResponse(Response response) { + String reason = EnglishReasonPhraseCatalog.INSTANCE.getReason( response.getStatusCode(), Locale.ENGLISH ); + try { + + JsonObject body = parseBody( response ); + return new ElasticsearchResponse( + response.getHost().toHostString(), + response.getStatusCode(), + reason, + body ); + } + catch (IOException | RuntimeException e) { + throw ElasticsearchClientLog.INSTANCE.failedToParseElasticsearchResponse( response.getStatusCode(), + reason, e.getMessage(), e ); + } + } + + private JsonObject parseBody(Response response) throws IOException { + HttpEntity entity = response.getEntity(); + if ( entity == null ) { + return null; + } + + Charset charset = getCharset( entity ); + try ( InputStream inputStream = entity.getContent(); + Reader reader = new InputStreamReader( inputStream, charset ) ) { + return gson.fromJson( reader, JsonObject.class ); + } + } + + private static Charset getCharset(HttpEntity entity) { + ContentType contentType = ContentType.parse( entity.getContentType() ); + Charset charset = contentType.getCharset(); + return charset != null ? charset : StandardCharsets.UTF_8; + } + + 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.host(), 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.host(), 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. + */ + closer.push( Sniffer::close, this.sniffer ); + // 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 ); + } + } + +} diff --git a/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/impl/ClientJavaElasticsearchClientBeanConfigurer.java b/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/impl/ClientJavaElasticsearchClientBeanConfigurer.java new file mode 100644 index 00000000000..dd4777882d5 --- /dev/null +++ b/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/impl/ClientJavaElasticsearchClientBeanConfigurer.java @@ -0,0 +1,20 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.java.impl; + +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchClientFactory; +import org.hibernate.search.engine.environment.bean.BeanHolder; +import org.hibernate.search.engine.environment.bean.spi.BeanConfigurationContext; +import org.hibernate.search.engine.environment.bean.spi.BeanConfigurer; + +public class ClientJavaElasticsearchClientBeanConfigurer implements BeanConfigurer { + @Override + public void configure(BeanConfigurationContext context) { + context.define( + ElasticsearchClientFactory.class, "elasticsearch-java", + beanResolver -> BeanHolder.of( new ClientJavaElasticsearchClientFactory() ) + ); + } +} diff --git a/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/impl/ClientJavaElasticsearchClientFactory.java b/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/impl/ClientJavaElasticsearchClientFactory.java new file mode 100644 index 00000000000..f20a3ed2824 --- /dev/null +++ b/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/impl/ClientJavaElasticsearchClientFactory.java @@ -0,0 +1,360 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.java.impl; + +import java.net.SocketAddress; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import org.hibernate.search.backend.elasticsearch.client.common.cfg.ElasticsearchBackendClientCommonSettings; +import org.hibernate.search.backend.elasticsearch.client.common.gson.spi.GsonProvider; +import org.hibernate.search.backend.elasticsearch.client.common.logging.spi.ElasticsearchClientConfigurationLog; +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.java.ElasticsearchHttpClientConfigurer; +import org.hibernate.search.backend.elasticsearch.client.java.cfg.ClientJavaElasticsearchBackendClientSettings; +import org.hibernate.search.backend.elasticsearch.client.java.cfg.spi.ClientJavaElasticsearchBackendClientSpiSettings; +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; + +import co.elastic.clients.transport.rest5_client.low_level.Rest5Client; +import co.elastic.clients.transport.rest5_client.low_level.Rest5ClientBuilder; +import co.elastic.clients.transport.rest5_client.low_level.sniffer.ElasticsearchNodesSniffer; +import co.elastic.clients.transport.rest5_client.low_level.sniffer.NodesSniffer; +import co.elastic.clients.transport.rest5_client.low_level.sniffer.Sniffer; +import co.elastic.clients.transport.rest5_client.low_level.sniffer.SnifferBuilder; +import org.apache.hc.client5.http.auth.AuthScope; +import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; +import org.apache.hc.client5.http.config.ConnectionConfig; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder; +import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider; +import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder; +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.nio.ssl.TlsStrategy; +import org.apache.hc.core5.net.NamedEndpoint; +import org.apache.hc.core5.reactor.ssl.TransportSecurityLayer; +import org.apache.hc.core5.util.Timeout; + + +/** + * @author Gunnar Morling + */ +public class ClientJavaElasticsearchClientFactory implements ElasticsearchClientFactory { + + private static final OptionalConfigurationProperty> CLIENT_INSTANCE = + ConfigurationProperty.forKey( ClientJavaElasticsearchBackendClientSpiSettings.CLIENT_INSTANCE ) + .asBeanReference( Rest5Client.class ) + .build(); + + private static final OptionalConfigurationProperty> HOSTS = + ConfigurationProperty.forKey( ElasticsearchBackendClientCommonSettings.HOSTS ) + .asString().multivalued() + .build(); + + private static final OptionalConfigurationProperty PROTOCOL = + ConfigurationProperty.forKey( ElasticsearchBackendClientCommonSettings.PROTOCOL ) + .asString() + .build(); + + private static final OptionalConfigurationProperty> URIS = + ConfigurationProperty.forKey( ElasticsearchBackendClientCommonSettings.URIS ) + .asString().multivalued() + .build(); + + private static final ConfigurationProperty PATH_PREFIX = + ConfigurationProperty.forKey( ElasticsearchBackendClientCommonSettings.PATH_PREFIX ) + .asString() + .withDefault( ElasticsearchBackendClientCommonSettings.Defaults.PATH_PREFIX ) + .build(); + + private static final OptionalConfigurationProperty USERNAME = + ConfigurationProperty.forKey( ElasticsearchBackendClientCommonSettings.USERNAME ) + .asString() + .build(); + + private static final OptionalConfigurationProperty PASSWORD = + ConfigurationProperty.forKey( ElasticsearchBackendClientCommonSettings.PASSWORD ) + .asString() + .build(); + + private static final OptionalConfigurationProperty REQUEST_TIMEOUT = + ConfigurationProperty.forKey( ClientJavaElasticsearchBackendClientSettings.REQUEST_TIMEOUT ) + .asIntegerStrictlyPositive() + .build(); + + private static final ConfigurationProperty READ_TIMEOUT = + ConfigurationProperty.forKey( ClientJavaElasticsearchBackendClientSettings.READ_TIMEOUT ) + .asIntegerPositiveOrZeroOrNegative() + .withDefault( ClientJavaElasticsearchBackendClientSettings.Defaults.READ_TIMEOUT ) + .build(); + + private static final ConfigurationProperty CONNECTION_TIMEOUT = + ConfigurationProperty.forKey( ClientJavaElasticsearchBackendClientSettings.CONNECTION_TIMEOUT ) + .asIntegerPositiveOrZeroOrNegative() + .withDefault( ClientJavaElasticsearchBackendClientSettings.Defaults.CONNECTION_TIMEOUT ) + .build(); + + private static final ConfigurationProperty MAX_TOTAL_CONNECTION = + ConfigurationProperty.forKey( ClientJavaElasticsearchBackendClientSettings.MAX_CONNECTIONS ) + .asIntegerStrictlyPositive() + .withDefault( ClientJavaElasticsearchBackendClientSettings.Defaults.MAX_CONNECTIONS ) + .build(); + + private static final ConfigurationProperty MAX_TOTAL_CONNECTION_PER_ROUTE = + ConfigurationProperty.forKey( ClientJavaElasticsearchBackendClientSettings.MAX_CONNECTIONS_PER_ROUTE ) + .asIntegerStrictlyPositive() + .withDefault( ClientJavaElasticsearchBackendClientSettings.Defaults.MAX_CONNECTIONS_PER_ROUTE ) + .build(); + + private static final ConfigurationProperty DISCOVERY_ENABLED = + ConfigurationProperty.forKey( ClientJavaElasticsearchBackendClientSettings.DISCOVERY_ENABLED ) + .asBoolean() + .withDefault( ClientJavaElasticsearchBackendClientSettings.Defaults.DISCOVERY_ENABLED ) + .build(); + + private static final ConfigurationProperty DISCOVERY_REFRESH_INTERVAL = + ConfigurationProperty.forKey( ClientJavaElasticsearchBackendClientSettings.DISCOVERY_REFRESH_INTERVAL ) + .asIntegerStrictlyPositive() + .withDefault( ClientJavaElasticsearchBackendClientSettings.Defaults.DISCOVERY_REFRESH_INTERVAL ) + .build(); + + private static final OptionalConfigurationProperty< + BeanReference> CLIENT_CONFIGURER = + ConfigurationProperty.forKey( ClientJavaElasticsearchBackendClientSettings.CLIENT_CONFIGURER ) + .asBeanReference( ElasticsearchHttpClientConfigurer.class ) + .build(); + + private static final OptionalConfigurationProperty MAX_KEEP_ALIVE = + ConfigurationProperty.forKey( ClientJavaElasticsearchBackendClientSettings.MAX_KEEP_ALIVE ) + .asLongStrictlyPositive() + .build(); + + @Override + public ElasticsearchClientImplementor create(BeanResolver beanResolver, ConfigurationPropertySource propertySource, + ThreadProvider threadProvider, String threadNamePrefix, + SimpleScheduledExecutor timeoutExecutorService, + GsonProvider gsonProvider) { + Optional requestTimeoutMs = REQUEST_TIMEOUT.get( propertySource ); + + Optional> providedRestClientHolder = CLIENT_INSTANCE.getAndMap( + propertySource, beanResolver::resolve ); + + BeanHolder restClientHolder; + Sniffer sniffer; + if ( providedRestClientHolder.isPresent() ) { + restClientHolder = providedRestClientHolder.get(); + sniffer = null; + } + else { + ServerUris hosts = ServerUris.fromOptionalStrings( PROTOCOL.get( propertySource ), + HOSTS.get( propertySource ), URIS.get( propertySource ) ); + restClientHolder = createClient( beanResolver, propertySource, threadProvider, threadNamePrefix, + hosts, PATH_PREFIX.get( propertySource ) ); + sniffer = createSniffer( propertySource, restClientHolder.get(), hosts ); + } + + return new ClientJavaElasticsearchClient( + restClientHolder, sniffer, timeoutExecutorService, + requestTimeoutMs, + gsonProvider.getGson(), gsonProvider.getLogHelper() + ); + } + + private BeanHolder createClient(BeanResolver beanResolver, + ConfigurationPropertySource propertySource, + ThreadProvider threadProvider, String threadNamePrefix, + ServerUris hosts, String pathPrefix) { + Rest5ClientBuilder builder = Rest5Client.builder( hosts.asHostsArray() ); + if ( !pathPrefix.isEmpty() ) { + builder.setPathPrefix( pathPrefix ); + } + + Optional> customConfig = CLIENT_CONFIGURER + .getAndMap( propertySource, beanResolver::resolve ); + + Rest5Client client = null; + List> httpClientConfigurerReferences = + beanResolver.allConfiguredForRole( ElasticsearchHttpClientConfigurer.class ); + List> requestInterceptorProviderReferences = + beanResolver.allConfiguredForRole( ElasticsearchRequestInterceptorProvider.class ); + try ( BeanHolder> httpClientConfigurersHolder = + beanResolver.resolve( httpClientConfigurerReferences ); + BeanHolder> requestInterceptorProvidersHodler = + beanResolver.resolve( requestInterceptorProviderReferences ) ) { + client = builder + .setRequestConfigCallback( b -> customizeRequestConfig( b, propertySource ) ) + .setHttpClientConfigCallback( + b -> customizeHttpClientConfig( + b, + beanResolver, propertySource, + threadProvider, threadNamePrefix, + hosts, httpClientConfigurersHolder.get(), requestInterceptorProvidersHodler.get(), + customConfig + ) + ) + .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 Sniffer createSniffer(ConfigurationPropertySource propertySource, + Rest5Client client, ServerUris hosts) { + boolean discoveryEnabled = DISCOVERY_ENABLED.get( propertySource ); + if ( discoveryEnabled ) { + SnifferBuilder builder = Sniffer.builder( client ) + .setSniffIntervalMillis( + DISCOVERY_REFRESH_INTERVAL.get( propertySource ) * 1_000 // The configured value is in seconds + ); + + // https discovery support + if ( hosts.isSslEnabled() ) { + NodesSniffer hostsSniffer = new ElasticsearchNodesSniffer( + client, + ElasticsearchNodesSniffer.DEFAULT_SNIFF_REQUEST_TIMEOUT, // 1sec + ElasticsearchNodesSniffer.Scheme.HTTPS ); + builder.setNodesSniffer( hostsSniffer ); + } + return builder.build(); + } + else { + return null; + } + } + + private HttpAsyncClientBuilder customizeHttpClientConfig(HttpAsyncClientBuilder builder, + BeanResolver beanResolver, ConfigurationPropertySource propertySource, + ThreadProvider threadProvider, String threadNamePrefix, + ServerUris hosts, Iterable configurers, + Iterable requestInterceptorProviders, + Optional> customConfig) { + builder.setThreadFactory( threadProvider.createThreadFactory( threadNamePrefix + " - Transport thread" ) ); + + PoolingAsyncClientConnectionManagerBuilder connectionManagerBuilder = + PoolingAsyncClientConnectionManagerBuilder.create() + .setMaxConnTotal( MAX_TOTAL_CONNECTION.get( propertySource ) ) + .setMaxConnPerRoute( MAX_TOTAL_CONNECTION_PER_ROUTE.get( propertySource ) ); + + if ( !hosts.isSslEnabled() ) { + // In this case disable the SSL capability as it might have an impact on + // bootstrap time, for example consuming entropy for no reason + // connectionManagerBuilder.setTlsStrategy( + // ClientTlsStrategyBuilder.create() + // .setSslContext( + // SSLContextBuilder.create() + // .loadTrustMaterial(null, new TrustAllStrategy()) + // .buildAsync() + // ) + // .setHostnameVerifier(NoopHostnameVerifier.INSTANCE) + // .build() + // ); + connectionManagerBuilder.setTlsStrategy( NoopTlsStrategy.INSTANCE ); + } + + builder.setConnectionManager( + connectionManagerBuilder + .setDefaultConnectionConfig( + ConnectionConfig.copy( ConnectionConfig.DEFAULT ) + .setConnectTimeout( CONNECTION_TIMEOUT.get( propertySource ), TimeUnit.MILLISECONDS ) + .setSocketTimeout( READ_TIMEOUT.get( propertySource ), TimeUnit.MILLISECONDS ) + .build() + ) + .build() + ); + + Optional username = USERNAME.get( propertySource ); + if ( username.isPresent() ) { + Optional password = PASSWORD.get( propertySource ).map( String::toCharArray ); + if ( password.isPresent() && !hosts.isSslEnabled() ) { + ElasticsearchClientConfigurationLog.INSTANCE.usingPasswordOverHttp(); + } + + BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider(); + credentialsProvider.setCredentials( + new AuthScope( null, null, -1, null, null ), + new UsernamePasswordCredentials( username.get(), password.orElse( null ) ) + ); + + builder.setDefaultCredentialsProvider( credentialsProvider ); + } + + Optional maxKeepAlive = MAX_KEEP_ALIVE.get( propertySource ); + if ( maxKeepAlive.isPresent() ) { + builder.setKeepAliveStrategy( new CustomConnectionKeepAliveStrategy( maxKeepAlive.get() ) ); + } + + ClientJavaElasticsearchHttpClientConfigurationContext clientConfigurationContext = + new ClientJavaElasticsearchHttpClientConfigurationContext( beanResolver, propertySource, builder ); + + for ( ElasticsearchHttpClientConfigurer configurer : configurers ) { + configurer.configure( clientConfigurationContext ); + } + for ( ElasticsearchRequestInterceptorProvider interceptorProvider : requestInterceptorProviders ) { + Optional requestInterceptor = + interceptorProvider.provide( clientConfigurationContext ); + if ( requestInterceptor.isPresent() ) { + builder.addRequestInterceptorLast( new ClientJavaHttpRequestInterceptor( requestInterceptor.get() ) ); + } + } + if ( customConfig.isPresent() ) { + BeanHolder customConfigBeanHolder = customConfig.get(); + customConfigBeanHolder.get().configure( clientConfigurationContext ); + } + + return builder; + } + + private RequestConfig.Builder customizeRequestConfig(RequestConfig.Builder builder, + ConfigurationPropertySource propertySource) { + return builder + .setConnectionRequestTimeout( Timeout.DISABLED ) //Disable lease handling for the connection pool! See also HSEARCH-2681 + .setResponseTimeout( READ_TIMEOUT.get( propertySource ), TimeUnit.MILLISECONDS ); + } + + private static class NoopTlsStrategy implements TlsStrategy { + private static final NoopTlsStrategy INSTANCE = new NoopTlsStrategy(); + + private NoopTlsStrategy() { + } + + @SuppressWarnings("deprecation") + @Override + public boolean upgrade(TransportSecurityLayer sessionLayer, HttpHost host, SocketAddress localAddress, + SocketAddress remoteAddress, Object attachment, Timeout handshakeTimeout) { + throw new UnsupportedOperationException( "upgrade is not supported." ); + } + + @Override + public void upgrade(TransportSecurityLayer sessionLayer, NamedEndpoint endpoint, Object attachment, + Timeout handshakeTimeout, FutureCallback callback) { + if ( callback != null ) { + callback.completed( sessionLayer ); + } + } + } +} diff --git a/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/impl/ClientJavaElasticsearchHttpClientConfigurationContext.java b/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/impl/ClientJavaElasticsearchHttpClientConfigurationContext.java new file mode 100644 index 00000000000..c934a80ad92 --- /dev/null +++ b/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/impl/ClientJavaElasticsearchHttpClientConfigurationContext.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.java.impl; + +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchRequestInterceptorProvider; +import org.hibernate.search.backend.elasticsearch.client.java.ElasticsearchHttpClientConfigurationContext; +import org.hibernate.search.engine.cfg.ConfigurationPropertySource; +import org.hibernate.search.engine.environment.bean.BeanResolver; + +import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder; + +final class ClientJavaElasticsearchHttpClientConfigurationContext + implements ElasticsearchHttpClientConfigurationContext, ElasticsearchRequestInterceptorProvider.Context { + private final BeanResolver beanResolver; + private final ConfigurationPropertySource configurationPropertySource; + private final HttpAsyncClientBuilder clientBuilder; + + ClientJavaElasticsearchHttpClientConfigurationContext( + BeanResolver beanResolver, + ConfigurationPropertySource configurationPropertySource, + HttpAsyncClientBuilder clientBuilder) { + this.beanResolver = beanResolver; + this.configurationPropertySource = configurationPropertySource; + this.clientBuilder = clientBuilder; + } + + @Override + public BeanResolver beanResolver() { + return beanResolver; + } + + @Override + public ConfigurationPropertySource configurationPropertySource() { + return configurationPropertySource; + } + + @Override + public HttpAsyncClientBuilder clientBuilder() { + return clientBuilder; + } + +} diff --git a/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/impl/ClientJavaHttpRequestInterceptor.java b/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/impl/ClientJavaHttpRequestInterceptor.java new file mode 100644 index 00000000000..3b6ada8fdcb --- /dev/null +++ b/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/impl/ClientJavaHttpRequestInterceptor.java @@ -0,0 +1,142 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.java.impl; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchRequestInterceptor; +import org.hibernate.search.util.common.AssertionFailure; + +import org.apache.hc.client5.http.protocol.HttpClientContext; +import org.apache.hc.core5.http.EntityDetails; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.HttpEntityContainer; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpRequestInterceptor; +import org.apache.hc.core5.http.NameValuePair; +import org.apache.hc.core5.http.nio.AsyncEntityProducer; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.net.URIBuilder; + +record ClientJavaHttpRequestInterceptor(ElasticsearchRequestInterceptor elasticsearchRequestInterceptor) + implements HttpRequestInterceptor { + + @Override + public void process(HttpRequest request, EntityDetails entity, HttpContext context) throws IOException { + elasticsearchRequestInterceptor.intercept( + new ClientJavaRequestContext( request, entity, context ) + ); + } + + private record ClientJavaRequestContext(HttpRequest request, EntityDetails entity, HttpClientContext clientContext) + implements ElasticsearchRequestInterceptor.RequestContext { + private ClientJavaRequestContext(HttpRequest request, EntityDetails entity, HttpContext context) { + this( request, entity, HttpClientContext.cast( context ) ); + } + + @Override + public boolean hasContent() { + return entity != null; + } + + @Override + public InputStream content() throws IOException { + HttpEntity localEntity = null; + if ( entity instanceof HttpEntity httpEntity ) { + localEntity = httpEntity; + } + else if ( request instanceof HttpEntityContainer entityContainer ) { + localEntity = entityContainer.getEntity(); + } + + if ( localEntity != null ) { + if ( !localEntity.isRepeatable() ) { + throw new AssertionFailure( "Cannot sign AWS requests with non-repeatable entities" ); + } + return localEntity.getContent(); + } + + if ( entity instanceof AsyncEntityProducer producer ) { + if ( !producer.isRepeatable() ) { + throw new AssertionFailure( "Cannot sign AWS requests with non-repeatable entities" ); + } + return new HttpAsyncEntityProducerInputStream( producer, 1024 ); + } + return null; + } + + @Override + public String scheme() { + return clientContext.getHttpRoute().getTargetHost().getSchemeName(); + } + + @Override + public String host() { + return clientContext.getHttpRoute().getTargetHost().getHostName(); + } + + @Override + public Integer port() { + return clientContext.getHttpRoute().getTargetHost().getPort(); + } + + @Override + public String method() { + return request.getMethod(); + } + + @Override + public String path() { + try { + return request.getUri().getPath(); + } + catch (URISyntaxException e) { + return request.getPath(); + } + } + + @Override + public Map queryParameters() { + try { + List queryParameters = new URIBuilder( request.getUri() ).getQueryParams(); + Map map = new HashMap<>(); + for ( NameValuePair parameter : queryParameters ) { + map.put( parameter.getName(), parameter.getValue() ); + } + return map; + } + catch (URISyntaxException e) { + return Map.of(); + } + } + + @Override + public void overrideHeaders(Map> headers) { + for ( Map.Entry> header : headers.entrySet() ) { + String name = header.getKey(); + boolean first = true; + for ( String value : header.getValue() ) { + if ( first ) { + request.setHeader( name, value ); + first = false; + } + else { + request.addHeader( name, value ); + } + } + } + } + + @Override + public String toString() { + return request.toString(); + } + } +} diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/CountingOutputStream.java b/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/impl/CountingOutputStream.java similarity index 92% rename from backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/CountingOutputStream.java rename to backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/impl/CountingOutputStream.java index 64e8e5fb65e..c152de27c4b 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/CountingOutputStream.java +++ b/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/impl/CountingOutputStream.java @@ -2,7 +2,7 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright Red Hat Inc. and Hibernate Authors */ -package org.hibernate.search.backend.elasticsearch.client.impl; +package org.hibernate.search.backend.elasticsearch.client.java.impl; import java.io.FilterOutputStream; import java.io.IOException; diff --git a/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/impl/CustomConnectionKeepAliveStrategy.java b/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/impl/CustomConnectionKeepAliveStrategy.java new file mode 100644 index 00000000000..7372da4763b --- /dev/null +++ b/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/impl/CustomConnectionKeepAliveStrategy.java @@ -0,0 +1,37 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.java.impl; + + +import java.util.concurrent.TimeUnit; + +import org.apache.hc.client5.http.ConnectionKeepAliveStrategy; +import org.apache.hc.client5.http.impl.DefaultConnectionKeepAliveStrategy; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.util.TimeValue; + +final class CustomConnectionKeepAliveStrategy implements ConnectionKeepAliveStrategy { + + private final TimeValue maxKeepAlive; + private final long maxKeepAliveMilliseconds; + + CustomConnectionKeepAliveStrategy(long maxKeepAlive) { + this.maxKeepAlive = TimeValue.of( maxKeepAlive, TimeUnit.MILLISECONDS ); + this.maxKeepAliveMilliseconds = maxKeepAlive; + } + + @Override + public TimeValue getKeepAliveDuration(HttpResponse response, HttpContext context) { + // get a keep alive from a request header if one is present + TimeValue keepAliveDuration = DefaultConnectionKeepAliveStrategy.INSTANCE.getKeepAliveDuration( response, context ); + long keepAliveDurationMilliseconds = keepAliveDuration.toMilliseconds(); + // if the keep alive timeout from a request is less than configured one - let's honor it: + if ( keepAliveDurationMilliseconds > 0 && keepAliveDurationMilliseconds < maxKeepAliveMilliseconds ) { + return keepAliveDuration; + } + return maxKeepAlive; + } +} diff --git a/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/impl/GsonHttpEntity.java b/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/impl/GsonHttpEntity.java new file mode 100644 index 00000000000..4d70a70b9f2 --- /dev/null +++ b/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/impl/GsonHttpEntity.java @@ -0,0 +1,321 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.java.impl; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Set; + +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchRequest; +import org.hibernate.search.util.common.impl.Contracts; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; + +import org.apache.hc.core5.function.Supplier; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.nio.AsyncEntityProducer; +import org.apache.hc.core5.http.nio.DataStreamChannel; + +/** + * Optimised adapter to encode GSON objects into HttpEntity instances. + * The naive approach was using various StringBuilders; the objects we + * need to serialise into JSON might get large and this was causing the + * internal StringBuilder buffers to need frequent resizing and cause + * problems with excessive allocations. + * + * Rather than trying to guess reasonable default sizes for these buffers, + * we can defer the serialisation to write directly into the ByteBuffer + * of the HTTP client, this has the additional benefit of making the + * intermediary buffers short lived. + * + * The one complexity to watch for is flow control: when writing into + * the output buffer chances are that not all bytes are accepted; in + * this case we have to hold on the remaining portion of data to + * be written when the flow control is re-enabled. + * + * A side effect of this strategy is that the total content length which + * is being produced is not known in advance. Not reporting the length + * in advance to the Apache Http client causes it to use chunked-encoding, + * which is great for large blocks but not optimal for small messages. + * For this reason we attempt to start encoding into a small buffer + * upfront: if all data we need to produce fits into that then we can + * report the content length; if not the encoding completion will be deferred + * but not resetting so to avoid repeating encoding work. + * + * @author Sanne Grinovero (C) 2017 Red Hat Inc. + */ +final class GsonHttpEntity implements HttpEntity, AsyncEntityProducer { + + private static final Charset CHARSET = StandardCharsets.UTF_8; + + private static final String CONTENT_TYPE = ContentType.APPLICATION_JSON.toString(); + + /** + * The size of byte buffer pages in {@link ProgressiveCharBufferWriter} + * It's a rather large size: a tradeoff for very large JSON + * documents as we do heavy bulking, and not too large to + * be a penalty for small requests. + * 1024 has been shown to produce reasonable, TLAB only garbage. + */ + private static final int BYTE_BUFFER_PAGE_SIZE = 1024; + + /** + * We want the char buffer and byte buffer pages of approximately + * the same size, however one is in characters and the other in bytes. + * Considering we hardcoded UTF-8 as encoding, which has an average + * conversion ratio of almost 1.0, this should be close enough. + */ + private static final int CHAR_BUFFER_SIZE = BYTE_BUFFER_PAGE_SIZE; + + public static HttpEntity toEntity(Gson gson, ElasticsearchRequest request) throws IOException { + final List bodyParts = request.bodyParts(); + if ( bodyParts.isEmpty() ) { + return null; + } + return new GsonHttpEntity( gson, bodyParts ); + } + + private final Gson gson; + private final List bodyParts; + + /** + * We don't want to compute the length in advance as it would defeat the optimisations + * for large bulks. + * Still it's possible that we happen to find out, for example if a Digest from all + * content needs to be computed, or if the content is small enough as we attempt + * to serialise at least one page. + */ + private long contentLength; + + /** + * We can lazily compute the contentLength, but we need to avoid changing the value + * we report over time as this confuses the Apache HTTP client as it initially defines + * the encoding strategy based on this, then assumes it can rely on this being + * a constant. + * After the {@link #getContentLength()} was invoked at least once, freeze + * the value. + */ + private boolean contentLengthWasProvided = false; + + /** + * Since flow control might hint to stop producing data, + * while we can't interrupt the rendering of a single JSON body + * we can avoid starting the rendering of any subsequent JSON body. + * So keep track of the next body which still needs to be rendered; + * to allow the output to be "repeatable" we also need to reset this + * at the end. + */ + private int nextBodyToEncodeIndex = 0; + + /** + * Adaptor from string output rendered into the actual output sink. + * We keep this as a field level attribute as we might have + * partially rendered JSON stored in its buffers while flow control + * refuses to accept more bytes. + */ + private ProgressiveCharBufferWriter writer = + new ProgressiveCharBufferWriter( CHARSET, CHAR_BUFFER_SIZE, BYTE_BUFFER_PAGE_SIZE ); + + public GsonHttpEntity(Gson gson, List bodyParts) throws IOException { + Contracts.assertNotNull( gson, "gson" ); + Contracts.assertNotNull( bodyParts, "bodyParts" ); + this.gson = gson; + this.bodyParts = bodyParts; + this.contentLength = -1; + attemptOnePassEncoding(); + } + + @Override + public boolean isRepeatable() { + return true; + } + + @Override + public void failed(Exception cause) { + + } + + @Override + public boolean isChunked() { + return false; + } + + @Override + public Set getTrailerNames() { + return Set.of(); + } + + @Override + public long getContentLength() { + this.contentLengthWasProvided = true; + return this.contentLength; + } + + @Override + public String getContentType() { + return CONTENT_TYPE; + } + + @Override + public String getContentEncoding() { + //Apparently this is the correct value: + return null; + } + + @Override + public InputStream getContent() { + return new HttpAsyncEntityProducerInputStream( this, BYTE_BUFFER_PAGE_SIZE ); + } + + @Override + public void writeTo(OutputStream out) throws IOException { + /* + * For this method we use no pagination, so ignore the mutable fields. + * + * Note we don't close the counting stream or the writer, + * because we must not close the output stream that was passed as a parameter. + */ + CountingOutputStream countingStream = new CountingOutputStream( out ); + Writer outWriter = new OutputStreamWriter( countingStream, CHARSET ); + for ( JsonObject bodyPart : bodyParts ) { + gson.toJson( bodyPart, outWriter ); + outWriter.append( '\n' ); + } + outWriter.flush(); + //Now we finally know the content size in bytes: + hintContentLength( countingStream.getBytesWritten() ); + } + + @Override + public boolean isStreaming() { + return false; + } + + @Override + public Supplier> getTrailers() { + return null; + } + + @Override + public void close() { + //Nothing to close but let's make sure we re-wind the stream + //so that we can start from the beginning if needed + this.nextBodyToEncodeIndex = 0; + //Discard previous buffers as they might contain in-process content: + this.writer = new ProgressiveCharBufferWriter( CHARSET, CHAR_BUFFER_SIZE, BYTE_BUFFER_PAGE_SIZE ); + } + + /** + * Let's see if we can fully encode the content with a minimal write, + * i.e. only one body part. + * This will allow us to keep the memory consumption reasonable + * while also being able to hint the client about the {@link #getContentLength()}. + * Incidentally, having this information would avoid chunked output encoding + * which is ideal precisely for small messages which can fit into a single buffer. + * + * @throws IOException This is unlikely to be caused by a real IO operation as there's no output buffer yet, + * but it could also be triggered by the UTF8 encoding operations. + */ + private void attemptOnePassEncoding() throws IOException { + // Essentially attempt to use the writer without going NPE on the output sink + // as it's not set yet. + triggerFullWrite(); + if ( nextBodyToEncodeIndex == bodyParts.size() ) { + writer.flush(); + // The buffer's content length so far is the final content length, + // as we know the entire content has been encoded already. + hintContentLength( writer.contentLength() ); + } + } + + /** + * Higher level write loop. It will start writing the JSON objects + * from either the beginning or the next object which wasn't written yet + * but simply stop and return as soon as the sink can't accept more data. + * Checking state of writer.flowControlPushingBack will reveal if everything + * was written. + * @throws IOException If writing fails. + */ + private void triggerFullWrite() throws IOException { + while ( nextBodyToEncodeIndex < bodyParts.size() ) { + JsonObject bodyPart = bodyParts.get( nextBodyToEncodeIndex++ ); + gson.toJson( bodyPart, writer ); + writer.append( '\n' ); + writer.flush(); + if ( writer.isFlowControlPushingBack() ) { + //Just quit: return control to the caller and trust we'll be called again. + return; + } + } + } + + @Override + public int available() { + return 0; + } + + @Override + public void produce(DataStreamChannel channel) throws IOException { + Contracts.assertNotNull( channel, "channel" ); + // Warning: this method is possibly invoked multiple times, depending on the output buffers + // to have available space ! + // Production of data is expected to complete only after we invoke ContentEncoder#complete. + + //Re-set the encoder as it might be a different one than a previously used instance: + writer.setOutput( channel ); + + //First write unfinished business from previous attempts + writer.resumePendingWrites(); + if ( writer.isFlowControlPushingBack() ) { + //Just quit: return control to the caller and trust we'll be called again. + return; + } + + triggerFullWrite(); + + if ( writer.isFlowControlPushingBack() ) { + //Just quit: return control to the caller and trust we'll be called again. + return; + } + writer.flushToOutput(); + if ( writer.isFlowControlPushingBack() ) { + //Just quit: return control to the caller and trust we'll be called again. + return; + } + + // If we haven't aborted yet, we finished! + + // The buffer's content length so far is the final content length, + // as we know the entire content has been encoded already. + // Hint at the content length. + // Note this is only useful if produceContent was called by some process + // that is not the HTTP client itself (e.g. for request signing), + // because the HTTP Client itself will request the size before it starts writing content. + hintContentLength( writer.contentLength() ); + + channel.endStream(); + this.releaseResources(); + } + + private void hintContentLength(long contentLength) { + if ( !contentLengthWasProvided ) { + this.contentLength = contentLength; + } + } + + @Override + public void releaseResources() { + close(); + } +} diff --git a/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/impl/HttpAsyncEntityProducerInputStream.java b/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/impl/HttpAsyncEntityProducerInputStream.java new file mode 100644 index 00000000000..be1ea946d2b --- /dev/null +++ b/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/impl/HttpAsyncEntityProducerInputStream.java @@ -0,0 +1,83 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.java.impl; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; + +import org.apache.hc.core5.http.nio.AsyncEntityProducer; + + +final class HttpAsyncEntityProducerInputStream extends InputStream { + private final AsyncEntityProducer entityProducer; + private final ByteBuffer buffer; + private final ByteBufferDataStreamChannel contentEncoder; + + public HttpAsyncEntityProducerInputStream(AsyncEntityProducer entityProducer, int bufferSize) { + this.entityProducer = entityProducer; + this.buffer = ByteBuffer.allocate( bufferSize ); + this.buffer.limit( 0 ); + this.contentEncoder = new ByteBufferDataStreamChannel( buffer ); + } + + @Override + public int read() throws IOException { + int read = readFromBuffer(); + if ( read < 0 && !contentEncoder.isCompleted() ) { + writeToBuffer(); + read = readFromBuffer(); + } + return read; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + int offset = off; + int length = len; + while ( length > 0 && ( buffer.remaining() > 0 || !contentEncoder.isCompleted() ) ) { + if ( buffer.remaining() == 0 ) { + writeToBuffer(); + } + int bytesRead = readFromBuffer( b, offset, length ); + offset += bytesRead; + length -= bytesRead; + } + int totalBytesRead = offset - off; + if ( totalBytesRead == 0 && contentEncoder.isCompleted() ) { + return -1; + } + return totalBytesRead; + } + + @Override + public void close() { + entityProducer.releaseResources(); + } + + private void writeToBuffer() throws IOException { + buffer.clear(); + entityProducer.produce( contentEncoder ); + buffer.flip(); + } + + private int readFromBuffer() { + if ( buffer.hasRemaining() ) { + return buffer.get(); + } + else { + return -1; + } + } + + private int readFromBuffer(byte[] bytes, int offset, int length) { + int toRead = Math.min( buffer.remaining(), length ); + if ( toRead > 0 ) { + buffer.get( bytes, offset, toRead ); + } + return toRead; + } + +} diff --git a/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/impl/ProgressiveCharBufferWriter.java b/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/impl/ProgressiveCharBufferWriter.java new file mode 100644 index 00000000000..d7ad2072ea0 --- /dev/null +++ b/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/impl/ProgressiveCharBufferWriter.java @@ -0,0 +1,288 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.java.impl; + +import java.io.IOException; +import java.io.Writer; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CoderResult; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Iterator; + +import org.apache.hc.core5.http.nio.ContentEncoder; +import org.apache.hc.core5.http.nio.DataStreamChannel; + +/** + * A writer to a ContentEncoder, using an automatically growing, paged buffer + * to store input when flow control pushes back. + *

+ * To be used when your input source is not reactive (uses {@link Writer}), + * but you have multiple elements to write and thus could take advantage of + * reactive output to some extent. + * + * @author Sanne Grinovero + */ +class ProgressiveCharBufferWriter extends Writer { + + private final CharsetEncoder charsetEncoder; + + /** + * Size of buffer pages. + */ + private final int pageSize; + + /** + * A higher-level buffer for chars, so that we don't have + * to wrap every single incoming char[] into a CharBuffer. + */ + private final CharBuffer charBuffer; + + /** + * Filled buffer pages to be written, in write order. + */ + private final Deque needWritingPages = new ArrayDeque<>( 5 ); + + /** + * Current buffer page, potentially null, + * which may have some content but isn't full yet. + */ + private ByteBuffer currentPage; + + /** + * Initially null: must be set before writing is started and each + * time it's resumed as it might change between writes during + * chunked encoding. + */ + private DataStreamChannel channel; + + /** + * Set this to true when we detect clogging, so we can stop trying. + * Make sure to reset this when the HTTP Client hints so. + * It's never dangerous to re-enable, just not efficient to try writing + * unnecessarily. + */ + private boolean flowControlPushingBack = false; + + private int contentLength = 0; + + public ProgressiveCharBufferWriter(Charset charset, int charBufferSize, int pageSize) { + this.charsetEncoder = charset.newEncoder(); + this.pageSize = pageSize; + this.charBuffer = CharBuffer.allocate( charBufferSize ); + } + + /** + * Set the encoder to write to when buffers are full. + */ + public void setOutput(DataStreamChannel channel) { + this.channel = channel; + } + + // Overrides super.write(int) to remove the synchronized() wrapper. + // WARNING: when you update this method, make sure to update ALL write(...) methods. + @Override + public void write(int c) throws IOException { + if ( 1 > charBuffer.remaining() ) { + flush(); + } + charBuffer.put( (char) c ); + } + + // Overrides super.write(String, int, int) to remove the synchronized() wrapper. + // WARNING: when you update this method, make sure to update ALL write(...) methods. + @Override + public void write(String str, int off, int len) throws IOException { + // See write(char[], int, int) for comments. + if ( len > charBuffer.capacity() ) { + flush(); + writeToByteBuffer( CharBuffer.wrap( str, off, off + len ) ); + } + else if ( len > charBuffer.remaining() ) { + flush(); + charBuffer.put( str, off, off + len ); + } + else { + charBuffer.put( str, off, off + len ); + } + } + + // WARNING: when you update this method, make sure to update ALL write(...) methods. + @Override + public void write(char[] cbuf, int off, int len) throws IOException { + if ( len > charBuffer.capacity() ) { + /* + * "cbuf" won't fit in our char buffer, so we'll just write + * everything to the byte buffer (first the pending chars in the + * char buffer, then "cbuf"). + */ + flush(); + writeToByteBuffer( CharBuffer.wrap( cbuf, off, len ) ); + } + else if ( len > charBuffer.remaining() ) { + /* + * We flush the buffer before writing anything in this case. + * + * If we did not, we'd run the risk of splitting a 3 or 4-byte + * character in two parts (one at the end of the buffer before + * flushing it, and the other at the beginning after flushing it), + * and the encoder would fail when encoding the second part. + * + * See HSEARCH-2886. + */ + flush(); + charBuffer.put( cbuf, off, len ); + } + else { + charBuffer.put( cbuf, off, len ); + } + } + + @Override + public void flush() throws IOException { + if ( charBuffer.position() == 0 ) { + return; + } + charBuffer.flip(); + writeToByteBuffer( charBuffer ); + charBuffer.clear(); + + // don't flush byte buffers to output as we want to control that flushing independently. + } + + @Override + public void close() { + // Nothing to do + } + + /** + * Send all full buffer pages to the {@link #setOutput(DataStreamChannel) output}. + *

+ * Flow control may push back, in which case this method or {@link #flushToOutput()} + * should be called again later. + * + * @throws IOException when {@link ContentEncoder#write(ByteBuffer)} fails. + */ + public void resumePendingWrites() throws IOException { + flush(); + flowControlPushingBack = false; + attemptFlushPendingBuffers( false ); + } + + /** + * @return {@code true} if the {@link #setOutput(DataStreamChannel) output} pushed + * back the last time a write was attempted, {@code false} otherwise. + */ + public boolean isFlowControlPushingBack() { + return flowControlPushingBack; + } + + /** + * Send all buffer pages to the {@link #setOutput(DataStreamChannel) output}, + * Even those that are not full yet + *

+ * Flow control may push back, in which case this method should be called again later. + * + * @throws IOException when {@link ContentEncoder#write(ByteBuffer)} fails. + */ + public void flushToOutput() throws IOException { + flush(); + flowControlPushingBack = false; + attemptFlushPendingBuffers( true ); + } + + /** + * @return The length of the content stored in the byte buffers so far, in bytes. + * This does include the content that has already been written to the {@link #setOutput(DataStreamChannel) output}, + * but not the content of the char buffer (which can be flushed to byte buffers using {@link #flush()}). + */ + public int contentLength() { + return contentLength; + } + + private void writeToByteBuffer(CharBuffer input) throws IOException { + while ( true ) { + if ( currentPage == null ) { + currentPage = ByteBuffer.allocate( pageSize ); + } + int initialPagePosition = currentPage.position(); + CoderResult coderResult = charsetEncoder.encode( input, currentPage, false ); + contentLength += ( currentPage.position() - initialPagePosition ); + if ( coderResult.equals( CoderResult.UNDERFLOW ) ) { + return; + } + else if ( coderResult.equals( CoderResult.OVERFLOW ) ) { + // Avoid storing buffers if we can simply flush them + attemptFlushPendingBuffers( true ); + if ( currentPage != null ) { + /* + * We couldn't flush the current page, but it's full, + * so let's move it out of the way. + */ + currentPage.flip(); + needWritingPages.add( currentPage ); + currentPage = null; + } + } + else { + //Encoding exception + coderResult.throwException(); + return; //Unreachable + } + } + } + + /** + * @return {@code true} if this buffer contains content to be written, {@code false} otherwise. + */ + private boolean hasRemaining() { + return !needWritingPages.isEmpty() || currentPage != null && currentPage.position() > 0; + } + + private void attemptFlushPendingBuffers(boolean flushCurrentPage) throws IOException { + if ( channel == null ) { + flowControlPushingBack = true; + } + if ( flowControlPushingBack || !hasRemaining() ) { + // Nothing to do + return; + } + Iterator iterator = needWritingPages.iterator(); + while ( iterator.hasNext() && !flowControlPushingBack ) { + ByteBuffer buffer = iterator.next(); + boolean written = write( buffer ); + if ( written ) { + iterator.remove(); + } + else { + flowControlPushingBack = true; + } + } + if ( flushCurrentPage && !flowControlPushingBack && currentPage != null && currentPage.position() > 0 ) { + // The encoder still accepts some input, and we are allowed to flush the current page. Let's do. + currentPage.flip(); + boolean written = write( currentPage ); + if ( !written ) { + flowControlPushingBack = true; + needWritingPages.add( currentPage ); + } + currentPage = null; + } + } + + private boolean write(ByteBuffer buffer) throws IOException { + final int toWrite = buffer.remaining(); + // We should never do 0-length writes, see HSEARCH-2854 + if ( toWrite == 0 ) { + return true; + } + final int actuallyWritten = channel.write( buffer ); + return toWrite == actuallyWritten; + } + +} diff --git a/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/impl/ServerUris.java b/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/impl/ServerUris.java new file mode 100644 index 00000000000..cf5611ae259 --- /dev/null +++ b/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/impl/ServerUris.java @@ -0,0 +1,126 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.java.impl; + +import java.net.URISyntaxException; +import java.util.List; +import java.util.Locale; +import java.util.Optional; + +import org.hibernate.search.backend.elasticsearch.client.common.cfg.ElasticsearchBackendClientCommonSettings; +import org.hibernate.search.backend.elasticsearch.client.common.logging.spi.ElasticsearchClientConfigurationLog; + +import org.apache.hc.core5.http.HttpHost; + + +final class ServerUris { + + private final HttpHost[] hosts; + private final boolean sslEnabled; + + private ServerUris(HttpHost[] hosts, boolean sslEnabled) { + this.hosts = hosts; + this.sslEnabled = sslEnabled; + } + + static ServerUris fromOptionalStrings(Optional protocol, Optional> hostAndPortStrings, + Optional> uris) { + if ( !uris.isPresent() ) { + String protocolValue = + ( protocol.isPresent() ) ? protocol.get() : ElasticsearchBackendClientCommonSettings.Defaults.PROTOCOL; + List hostAndPortValues = + ( hostAndPortStrings.isPresent() ) + ? hostAndPortStrings.get() + : ElasticsearchBackendClientCommonSettings.Defaults.HOSTS; + return fromStrings( protocolValue, hostAndPortValues ); + } + + if ( protocol.isPresent() ) { + throw ElasticsearchClientConfigurationLog.INSTANCE.uriAndProtocol( uris.get(), protocol.get() ); + } + + if ( hostAndPortStrings.isPresent() ) { + throw ElasticsearchClientConfigurationLog.INSTANCE.uriAndHosts( uris.get(), hostAndPortStrings.get() ); + } + + return fromStrings( uris.get() ); + } + + private static ServerUris fromStrings(List serverUrisStrings) { + if ( serverUrisStrings.isEmpty() ) { + throw ElasticsearchClientConfigurationLog.INSTANCE.emptyListOfUris(); + } + + HttpHost[] hosts = new HttpHost[serverUrisStrings.size()]; + Boolean https = null; + for ( int i = 0; i < serverUrisStrings.size(); ++i ) { + String uri = serverUrisStrings.get( i ); + try { + HttpHost host = HttpHost.create( uri ); + hosts[i] = host; + String scheme = host.getSchemeName(); + boolean currentHttps = "https".equals( scheme ); + if ( https == null ) { + https = currentHttps; + } + else if ( currentHttps != https ) { + throw ElasticsearchClientConfigurationLog.INSTANCE.differentProtocolsOnUris( serverUrisStrings ); + } + } + catch (URISyntaxException e) { + throw ElasticsearchClientConfigurationLog.INSTANCE.invalidUri( uri, e.getMessage(), e ); + } + } + + return new ServerUris( hosts, https ); + } + + private static ServerUris fromStrings(String protocol, List hostAndPortStrings) { + if ( hostAndPortStrings.isEmpty() ) { + throw ElasticsearchClientConfigurationLog.INSTANCE.emptyListOfHosts(); + } + + HttpHost[] hosts = new HttpHost[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 ) { + HttpHost host = createHttpHost( scheme, hostAndPortStrings.get( i ) ); + hosts[i] = host; + } + return new ServerUris( hosts, "https".equals( scheme ) ); + } + + private static HttpHost createHttpHost(String scheme, String hostAndPort) { + if ( hostAndPort.indexOf( "://" ) >= 0 ) { + throw ElasticsearchClientConfigurationLog.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 ElasticsearchClientConfigurationLog.INSTANCE.invalidHostAndPort( hostAndPort, e ); + } + host = hostAndPort.substring( 0, portIdx ); + } + return new HttpHost( scheme, host, port ); + } + + HttpHost[] asHostsArray() { + return hosts; + } + + boolean isSslEnabled() { + return sslEnabled; + } + +} diff --git a/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/logging/impl/ElasticsearchClientLog.java b/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/logging/impl/ElasticsearchClientLog.java new file mode 100644 index 00000000000..0b7d57765f3 --- /dev/null +++ b/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/logging/impl/ElasticsearchClientLog.java @@ -0,0 +1,29 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.java.logging.impl; + +import java.lang.invoke.MethodHandles; + +import org.hibernate.search.util.common.logging.CategorizedLogger; +import org.hibernate.search.util.common.logging.impl.LoggerFactory; +import org.hibernate.search.util.common.logging.impl.MessageConstants; + +import org.jboss.logging.annotations.MessageLogger; + +@CategorizedLogger( + category = ElasticsearchClientLog.CATEGORY_NAME, + description = """ + Logs information on low-level Elasticsearch backend operations. + + + This may include warnings about misconfigured Elasticsearch REST clients or index operations. + """ +) +@MessageLogger(projectCode = MessageConstants.PROJECT_CODE) +public interface ElasticsearchClientLog { + String CATEGORY_NAME = "org.hibernate.search.elasticsearch.client"; + + ElasticsearchClientLog INSTANCE = LoggerFactory.make( ElasticsearchClientLog.class, CATEGORY_NAME, MethodHandles.lookup() ); + +} diff --git a/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/package-info.java b/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/package-info.java new file mode 100644 index 00000000000..ce509db87fc --- /dev/null +++ b/backend/elasticsearch-client/elasticsearch-java-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/java/package-info.java @@ -0,0 +1 @@ +package org.hibernate.search.backend.elasticsearch.client.java; diff --git a/backend/elasticsearch-client/elasticsearch-java-client/src/main/resources/META-INF/services/org.hibernate.search.engine.environment.bean.spi.BeanConfigurer b/backend/elasticsearch-client/elasticsearch-java-client/src/main/resources/META-INF/services/org.hibernate.search.engine.environment.bean.spi.BeanConfigurer new file mode 100644 index 00000000000..fbf115c82b4 --- /dev/null +++ b/backend/elasticsearch-client/elasticsearch-java-client/src/main/resources/META-INF/services/org.hibernate.search.engine.environment.bean.spi.BeanConfigurer @@ -0,0 +1 @@ +org.hibernate.search.backend.elasticsearch.client.java.impl.ClientJavaElasticsearchClientBeanConfigurer diff --git a/backend/elasticsearch-client/elasticsearch-java-client/src/test/java/org/hibernate/search/backend/elasticsearch/client/java/impl/GsonHttpEntityTest.java b/backend/elasticsearch-client/elasticsearch-java-client/src/test/java/org/hibernate/search/backend/elasticsearch/client/java/impl/GsonHttpEntityTest.java new file mode 100644 index 00000000000..f091ac131fc --- /dev/null +++ b/backend/elasticsearch-client/elasticsearch-java-client/src/test/java/org/hibernate/search/backend/elasticsearch/client/java/impl/GsonHttpEntityTest.java @@ -0,0 +1,343 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.java.impl; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.Reader; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.hibernate.search.backend.elasticsearch.client.common.gson.spi.GsonProvider; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.nio.ContentEncoder; +import org.apache.hc.core5.http.nio.DataStreamChannel; + +class GsonHttpEntityTest { + + public static List params() { + List params = new ArrayList<>(); + Gson gson = GsonProvider.create( GsonBuilder::new, true ).getGson(); + + JsonObject bodyPart1 = JsonParser.parseString( "{ \"foo\": \"bar\" }" ).getAsJsonObject(); + JsonObject bodyPart2 = JsonParser.parseString( "{ \"foobar\": 235 }" ).getAsJsonObject(); + JsonObject bodyPart3 = JsonParser.parseString( "{ \"obj1\": " + bodyPart1.toString() + + ", \"obj2\": " + bodyPart2.toString() + "}" ).getAsJsonObject(); + + for ( List jsonObjects : Arrays.>asList( + Collections.emptyList(), + Collections.singletonList( bodyPart1 ), + Collections.singletonList( bodyPart2 ), + Collections.singletonList( bodyPart3 ), + Arrays.asList( bodyPart1, bodyPart2, bodyPart3 ), + Arrays.asList( bodyPart3, bodyPart2, bodyPart1 ) + ) ) { + params.add( Arguments.of( jsonObjects.toString(), jsonObjects ) ); + } + params.add( Arguments.of( + "50 small objects", + Stream.generate( () -> Arrays.asList( bodyPart1, bodyPart2, bodyPart3 ) ) + .flatMap( List::stream ).limit( 50 ).collect( Collectors.toList() ) + ) ); + params.add( Arguments.of( + "200 small objects", + Stream.generate( () -> Arrays.asList( bodyPart1, bodyPart2, bodyPart3 ) ) + .flatMap( List::stream ).limit( 200 ).collect( Collectors.toList() ) + ) ); + params.add( Arguments.of( + "10,000 small objects", + Stream.generate( () -> Arrays.asList( bodyPart1, bodyPart2, bodyPart3 ) ) + .flatMap( List::stream ).limit( 10_000 ).collect( Collectors.toList() ) + ) ); + params.add( Arguments.of( + "200 large objects", + Stream.generate( () -> { + // Generate one large object + JsonObject object = new JsonObject(); + JsonArray array = new JsonArray(); + object.add( "array", array ); + Stream.generate( () -> Arrays.asList( bodyPart1, bodyPart2, bodyPart3 ) ) + .flatMap( List::stream ).limit( 1_000 ).forEach( array::add ); + return object; + } ) + // Reproduce the large object multiple times + .limit( 200 ).collect( Collectors.toList() ) + ) ); + params.add( Arguments.of( + "1 very large object", + Stream.generate( () -> { + JsonObject object = new JsonObject(); + JsonArray array = new JsonArray(); + object.add( "array", array ); + Stream.generate( () -> Arrays.asList( bodyPart1, bodyPart2, bodyPart3 ) ) + .flatMap( List::stream ).limit( 100_000 ).forEach( array::add ); + return object; + } ) + // Reproduce the large object multiple times + .limit( 1 ).collect( Collectors.toList() ) + ) ); + + params.add( Arguments.of( + "Reproducer for HSEARCH-4239", + // Yes these objects are weird, but then this is a weird edge-case. + Arrays.asList( + gson.fromJson( "{\"index\":{\"_index\":\"indexname-write\",\"_id\":\"0\"}}", JsonObject.class ), + gson.fromJson( "{\"content\":\"..........................................................." + + "...........—.....................—.......—....—....................................." + + "...—.....................................................................——........." + + "...................................................................................." + + "........................—..........................................................." + + "...................................................................................." + + "...........................................—........................................" + + "............—............—.........................................................." + + "...................................................................................." + + "................................................................——.....—............" + + ".................—.................................................................." + + ".............................——....................................................." + + "..........................................\",\"_entity_type\":\"indexNameType\"}", + JsonObject.class ) + ) + ) ); + + params.add( Arguments.of( + "Reproducer for HSEARCH-4254", + Arrays.asList( + gson.fromJson( + // This is randomly generated content that happens to reproduce the problem. + // If it means anything, that's purely coincidental. + // I'm sorry if it's offensive, + // but if I change even one character the problem no longer appears, + // so I'm stuck with this content. + "{\"content\":\"\u010D\uFC7A\uCBED\uD857\uDFC9\uB5FA\u17B2\uD83E\uDD21\u15C5\uFF3D\uD397\u3F7B\u7822\uD84F\uDDB2\uD859\uDD21\uD871\uDFCC\uD852\uDDC3\uD848\uDCFD\uD85E\uDEAF\uD81E\uDD10\uD855\uDDF1\u2D67\uD859\uDC62\uD85C\uDC89\u7D75\uD82F\uDC09\uD800\uDFAA\uD803\uDC35\u1BE9\u4183\u068A\uC61E\uD85D\uDDC7\uD3CC\uD873\uDFD6\u3DA6\uD842\uDE91\uD85A\uDC3D\uD865\uDCF0\uD81C\uDDEE\u718A\uD84D\uDEB3\uC2C2\uD86C\uDF47\u8F9D\u4801\uD84C\uDED9\uD86B\uDE9C\uC759\uD862\uDF42\uCE83\u7B02\u526D\u978E\u3936\u7C94\u6746\uD866\uDF98\uD844\uDFC8\uD851\uDF8E\u4A98\u789E\uD85B\uDF01\uD855\uDE5B\uD87A\uDF2F\uD869\uDF29\u2AFF\uD821\uDD33\uD86C\uDE14\uD87E\uDCD7\uFF1F\uD853\uDEA4\uD847\uDD4B\uD85A\uDD5D\uD81A\uDCF1\u7F23\u7A6C\uD848\uDC48\uD84D\uDD06\uD821\uDED2\u2ED6\uD83E\uDD56\u591B\uD84C\uDD84\uD85D\uDE1B\uD877\uDEFE\uC624\u6DEA\u73BC\uD821\uDE30\u23A7\uD7DB\uD862\uDD93\u3B3C\u0ED4\uD846\uDC7B\u0959\uD86D\uDE2A\uD86B\uDC2B\uD86D\uDE87\u76A3\uA10F\uD872\uDEB1\uA604\uA852\uD1D5\uD856\uDEBD\uD844\uDD82\uD820\uDEB4\\u0006\uD81E\uDF6F\uD870\uDD2D\uD856\uDF4E\uD875\uDE78\uB7DB\uCF98\uD856\uDEAF\uD87A\uDEC9\u04A8\uD848\uDFEF\uD852\uDE49\uC933\u12AE\uD84F\uDC63\uD850\uDECE\uD874\uDDAC\uD872\uDC7D\u7F76\uD869\uDE37\uD2A0\uD4C7\uD868\uDDBC\uD848\uDC31\u86EA\u7276\u48B9\u8412\uB216\uD848\uDF0D\u7102\uD869\uDC63\u9018\u2FFA\uD844\uDD1E\u91DD\uFAA3\uD877\uDF17\u8617\uD821\uDEC9\uD860\uDCA7\u66B1\uD808\uDCF5\uD807\uDC85\u483B\u1CE0\u7DF3\uD878\uDDA4\u14BA\u3558\uB82D\uD845\uDE49\uD871\uDEFC\uD87E\uDDCC\uA1C2\uB195\uD874\uDE28\u3AD4\uD87A\uDF69\u12E4\u6787\uD850\uDC86\u414D\uD84B\uDCFC\u14DB\u3259\uD85A\uDD6B\u15A8\uD87E\uDCB3\uD868\uDE40\uD81D\uDF4B\uD821\uDDAF\uD81F\uDC9E\u1BC8\uD805\uDE1A\uD84B\uDE33\uC4E3\u9D76\uD849\uDEA5\u1230\u9DE0\u2F65\u2BBD\u604E\uD848\uDC6B\uD835\uDF6B\u27E6\uD846\uDF6C\u77AB\uD865\uDD55\u15EC\u380D\uD857\uDEA7\uD859\uDEA3\uD841\uDC58\uD86D\uDC68\uD85B\uDCCC\uD864\uDE04\uD874\uDEEB\uD879\uDC9A\uD874\uDFF5\uD83C\uDF9A\uD879\uDD5E\u4C5C\uD80C\uDE45\uCAB3\uD81F\uDFFB\uD81F\uDDC1\uD844\uDF64\u3DB6\uD862\uDF7D\uD870\uDC1B\uD853\uDC91\uD864\uDE99\uD845\uDD16\uD84D\uDCC5\uD870\uDF76\uD86C\uDCA2\uD821\uDC02\uD820\uDFF9\uD865\uDF2C\u75B5\u9AA4\uD87A\uDDCE\u3004\u8355\uA2D8\uD84B\uDCED\uD809\uDD01\u763F\uD845\uDE39\u610B\uD84A\uDDD8\u89DC\uD842\uDE17\u9797\uD842\uDFC2\uD83E\uDC70\uD804\uDD72\u732E\uD86C\uDEA0\uAE24\uD822\uDD4F\uD86A\uDDF1\uD847\uDDA7\uD876\uDDB9\uD85F\uDC41\u6428\uD85A\uDCE4\uD84F\uDE6C\uD868\uDE62\uD860\uDEDC\uD83E\uDC05\uD83A\uDC3A\uC710\u332D\uD17F\u3CBF\uC2C0\uD86A\uDE20\uD858\uDCB3\uD858\uDDDE\uD855\uDFF6\u5E77\uD860\uDD85\u16D8\uD867\uDCD2\uD84E\uDFB1\u3978\uD853\uDD69\u07D4\uD874\uDC5B\uD84F\uDFDC\uD86A\uDF87\uCA0C\u3FE1\uD84A\uDF3F\u2EEA\uD821\uDDA7\uAADD\uD80C\uDF61\uA8D3\uD860\uDFFB\u1386\uD86B\uDE4B\uD874\uDF01\uD855\uDE10\u104A\u8C65\uD85E\uDEC4\uD85E\uDD20\u810A\uD81A\uDCD0\u2B3D\uD842\uDEFF\uD821\uDC28\uD877\uDE3E\uD856\uDE83\uD854\uDFC9\uD87A\uDE84\u01D4\uD85F\uDED8\uC53E\uD841\uDF04\uD857\uDE31\uCDB9\uD877\uDF5A\u99E2\uC3A4\uD83E\uDD2E\u1BA9\uD852\uDD5B\uD848\uDE9D\uD870\uDED3\uD849\uDD9F\u3D3F\uD857\uDEB0\u4193\u053A\u5B90\uD852\uDFBA\uD878\uDE00\u738C\uD878\uDF7E\uD868\uDF86\u7A08\uD868\uDFF1\uD81A\uDF1A\uD80C\uDD52\uD85C\uDE22\uD870\uDD12\uD81C\uDC35\uD871\uDD2C\uD877\uDE45\uD84F\uDD4B\u37DB\uD86C\uDC11\u9E19\uD85B\uDDA7\uD84D\uDEE8\uD86F\uDF6D\uD86A\uDDFC\u4CD5\u4459\uD85A\uDD6E\uD844\uDF3C\uCB39\uD809\uDD36\uA240\uD84B\uDC56\uD847\uDD46\uD852\uDD78\uD84A\uDC25\u316E\uD85C\uDF62\uD86E\uDD3E\u720D\u3C94\u99ED\uD489\u66A3\u4325\u663D\uB04B\u60D3\uCFB2\uD840\uDEF8\uD83A\uDCAE\uD843\uDCC0\u97C6\uD84D\uDC8E\uD858\uDCEF\uD87A\uDE11\uD84D\uDC23\uD86F\uDCE1\uD875\uDF57\uD84F\uDD00\u9AAA\uD835\uDF15\uD84E\uDC0E\uD822\uDC79\uFC38\u6D95\uD876\uDE83\uD84B\uDEAB\uC7A6\uD849\uDEA8\uD866\uDCE5\uD836\uDD60\uCD9E\uD802\uDC78\uD867\uDEFA\uD800\uDCE1\uD112\uD804\uDE23\u74A7\uD86A\uDC5F\u8E99\uD811\uDDFA\uD848\uDCF0\uD875\uDC60\u38D9\uD808\uDD5A\uD820\uDF59\uD86E\uDDD2\u95A5\uA72E\u2756\uD845\uDF91\u610C\uD820\uDEF5\u6F33\uD868\uDFE3\uD859\uDE88\uD874\uDC80\uD85D\uDE43\uD85E\uDD15\uD875\uDEAE\u8DD8\u8BAD\u5D7E\u3491\uD858\uDE1E\u2A4F\u95EE\uD806\uDE5F\u2995\uD869\uDDDE\uD81A\uDF74\uFDB4\u8EAD\uD802\uDDE3\uD841\uDFA9\u9B32\u0F03\uD863\uDFC3\u20E6\uD85E\uDDB0\u928E\uB839\uD86C\uDDC7\uD840\uDDFF\uD870\uDED5\uD873\uDC91\uD850\uDECB\uD84A\uDD73\uD854\uDCFB\uBB90\uBA0D\uD834\uDD2F\u38CB\uD805\uDC83\u5AB3\uD847\uDCC4\uD85B\uDC14\u73A1\u13CE\uD846\uDF08\u136E\uA032\uF956\uA1B9\uD820\uDD41\uD85E\uDDC3\uD870\uDEB0\uD86D\uDC05\uD86F\uDECB\uD85E\uDF16\uD802\uDC1E\uC74E\u581C\uD808\uDF01\u5991\uD805\uDE0D\uD86F\uDE88\uD851\uDC40\uD86A\uDD24\uD865\uDFA8\u6DE1\uD804\uDCF4\uD84E\uDE5A\uD86E\uDFB3\u4F8E\u7F02\uD84F\uDCD1\uD863\uDC15\uD859\uDC82\uD834\uDE40\uD863\uDF4D\uD86C\uDC3A\uC599\uD01E\u82DB\uD871\uDF39\uD840\uDF44\uD81F\uDCA1\uD85E\uDDEE\u9745\uD845\uDC31\uD856\uDD79\uAB72\uD66D\uB29F\u37B0\u96DD\uB781\uD85A\uDF54\uCD02\uD86C\uDE6E\u2CFF\u367D\u7DA2\uD854\uDE05\u7D83\uD808\uDE70\uD85A\uDE12\uD853\uDD97\u2ACC\uD85C\uDEE5\uD872\uDE5C\uD85A\uDEBA\uD875\uDC4F\u7008\uD867\uDFC3\u1C13\u9888\u9DCA\u9FE9\uD84D\uDCA8\u5530\uD855\uDEE2\uD841\uDD5D\uD877\uDE59\uD867\uDD7A\uD85C\uDF97\uD80C\uDDD3\uD847\uDF4A\uD876\uDF1A\uC279\uD874\uDF6F\u1339\uD86A\uDC20\uD841\uDFAB\u09CC\uD811\uDC98\u4228\u5DCF\u2A82\uD86B\uDCF4\u069F\uD860\uDF40\u93F0\uD808\uDF0B\uD841\uDC86\uD870\uDD41\u610B\u63EE\uD81A\uDCBE\u9A85\uD809\uDC91\uD852\uDDAA\u6974\uD847\uDDA3\uD85B\uDDBA\uD840\uDE4C\u56BA\uD876\uDED3\uD867\uDDF8\uD808\uDC2F\uD867\uDC05\uD84B\uDC14\uD867\uDDB4\u4DF0\u82E6\uD845\uDEE2\uD81C\uDE14\uD802\uDD83\uBC7E\uD85D\uDC2C\uD846\uDD1E\uD873\uDCCC\uD860\uDC8A\uD878\uDC92\uD874\uDE65\uD861\uDC28\uD854\uDC3E\uD84C\uDC11\uD856\uDD11\uD81D\uDEB0\uB07A\u89A4\uD85A\uDED9\uD81C\uDEE2\uD805\uDC0D\uD834\uDC22\uD864\uDC4F\u240B\uD86A\uDE80\uD876\uDFC4\uD85E\uDCFF\uD871\uDE7C\u1987\uD835\uDC8F\u1990\uD863\uDFE1\uD81A\uDF26\uD802\uDC0E\uD853\uDE0B\uD841\uDE95\u59E7\u1E02\uD864\uDF58\uD877\uDEE1\uD82C\uDEC0\uD86B\uDD8A\u391D\u03BB\uD863\uDE29\uD84B\uDEDB\u8F02\uD2A0\u22ED\uD863\uDF74\u70A9\uD856\uDE3A\uBC76\uD861\uDD78\uD868\uDD9C\uD851\uDFF6\u8860\uD841\uDD85\uFB84\u110B\uD850\uDCD4\uD875\uDE6D\uD845\uDD20\uDB40\uDD9D\uD853\uDD7A\uD841\uDEA3\u0D05\uD869\uDDC0\uD856\uDDA8\uD870\uDD1F\uD86C\uDCF9\uD80C\uDF2A\uD804\uDCD4\uD85B\uDDF5\u9EF2\uD844\uDF27\u8536\uD85B\uDFE7\u581F\u6C39\uD856\uDE79\uA50D\uD864\uDCD9\uD879\uDEF7\uD841\uDE38\u3D49\u1B50\uD807\uDC8D\uD811\uDDAF\u82CC\u3F64\u851E\uD847\uDF0B\u0923\u3000\uD85A\uDE7E\uAC0D\u9344\uD801\uDEE5\u02EF\uD0D1\uD877\uDEDD\uD861\uDE7E\u9910\uD86B\uDEC1\uD85D\uDF3F\u6CBF\u27B2\u7869\uD822\uDC60\uD57E\uD860\uDD92\uD85F\uDCC0\uD86E\uDD44\uD872\uDE59\uD81D\uDE21\u956A\uD85F\uDD7A\u3A62\u95F5\uD878\uDF26\u5F0E\u2934\uD82C\uDC91\uD862\uDDA7\uD848\uDFD4\uD81D\uDE6F\uD86A\uDD5B\uD845\uDF26\u9C4F\uD82C\uDEBF\uD87A\uDE9B\uD849\uDC8A\u13D8\uD248\uBF92\uD853\uDDD6\uD862\uDEEE\uD7DA\uD863\uDCB6\uD860\uDDE9\uD859\uDCD6\uD856\uDCE8\u7612\uD84B\uDE38\uD82C\uDD70\uD85B\uDF2B\uD869\uDC4A\u396C\u7728\u41F4\uD859\uDC5D\uD83E\uDC3E\u648C\uD877\uDD93\u7358\uD845\uDE51\uD857\uDF95\uD870\uDE49\u93D4\uBDF1\u5E2E\u8197\u24E3\uD856\uDE54\u55CF\uD81F\uDF7A\uB6AC\uD865\uDF8B\uD81F\uDDD8\u6F19\uD84B\uDC82\uD867\uDD94\uB377\uD847\uDD74\uD875\uDFC4\uD86E\uDEF9\uD87A\uDD0C\u25C6\uD877\uDCF0\uD864\uDEE8\uD804\uDDC0\uD359\uD835\uDFC0\u813D\u2AC1\uD86C\uDD80\uD807\uDC58\u3750\uD805\uDCC7\u51D0\uD845\uDC5D\uD847\uDEB2\uD821\uDD63\uD869\uDE68\uD811\uDD5F\u76C0\uB127\u9D0D\u88DE\u163E\uD865\uDC75\uD85F\uDE50\uD857\uDF48\uD865\uDF68\uD87A\uDFCE\uD802\uDC89\uD86E\uDDE2\uD808\uDF58\uD850\uDC8F\u0714\uC90D\uD845\uDF49\u8EC9\uD856\uDE91\uD804\uDC61\uD847\uDE44\uD5FA\uD811\uDDB6\uD840\uDE71\uD38F\uFF3B\uD866\uDF1B\uD845\uDF61\uD871\uDFC5\u858A\uA12D\u53A8\uD820\uDF73\uD843\uDCCE\uD857\uDC68\uD84E\uDD33\uD867\uDC17\u26D1\u3573\u5701\u7BE5\uD870\uDF52\uD853\uDECD\u427E\uD873\uDF75\u51ED\uD857\uDF19\uD852\uDF3D\uD85F\uDC4E\uD84C\uDC13\uD842\uDDDE\uD868\uDDB4\uD871\uDEC5\uD871\uDC24\u8C19\uD85A\uDEF1\uD863\uDDDE\u2967\uD86A\uDEA6\uD857\uDC74\uD85B\uDF23\u3294\u7DC1\u7A9C\u7B1F\uD83B\uDE68\uD86D\uDCFC\uD80C\uDE3F\u2880\u488F\uD84C\uDE42\u70D2\uD861\uDC7D\uD821\uDFE3\uD848\uDEBD\uD841\uDD80\u69B6\uD848\uDEB5\u916E\uD803\uDC36\uD84E\uDE70\uD86E\uDC95\u0438\u95FC\uD870\uDE9A\uD7C5\uD875\uDF29\uCB39\uD860\uDDAF\uD86A\uDCBA\u1321\uD811\uDDD7\uD871\uDF2E\u35DF\uD873\uDCBB\u5373\u9A9A\u3055\uD84E\uDE62\uD85E\uDE7E\u7467\uB87D\uC42C\uD82C\uDD91\uD857\uDD05\uD84A\uDF2D\u9A18\uD81F\uDD42\u50FA\u8C62\uD84A\uDC70\u49AD\uD871\uDFD9\uD850\uDEA3\u3AF4\u198C\uB6FF\uA697\uD855\uDFEC\uD81D\uDD1A\uD878\uDC50\u9F9C\u1D34\u4FAF\uDB40\uDC64\u75C1\uD835\uDDF1\uD854\uDCEA\uD842\uDFC7\uD853\uDFD1\uD81C\uDD99\uD874\uDD2F\uBC5A\uD873\uDE4F\uD872\uDF8E\uD852\uDDF1\uD85F\uDC4C\uD842\uDF01\uD86D\uDD9A\uD84F\uDC70\uD867\uDD15\u0788\uD820\uDE7D\uDB40\uDDD3\uD82C\uDCCF\uD871\uDC2D\uD834\uDCD0\u9669\uD877\uDE12\uD872\uDDAB\uD843\uDC43\uD84E\uDC8D\u5840\uD840\uDD5D\uD858\uDFF2\uD855\uDF37\uB5A5\uD821\uDE6E\uD805\uDDCF\uD7DB\uD86A\uDC9F\uD861\uDC05\uD81E\uDE8B\u0718\uD852\uDEFE\uD80C\uDE1A\uD83D\uDC0D\uD86E\uDFBC\uD834\uDDCA\uFA86\u2E04\u0568\uC32B\uD871\uDE34\uD854\uDEC4\uD867\uDC93\uD85E\uDD31\u46A7\u8205\uD843\uDE2A\u0771\uD83C\uDE38\uD808\uDDD3\uD867\uDC11\uFD9F\uD84F\uDF7B\u552F\uA586\uD82F\uDC28\uD87A\uDCA9\u32B7\u39A4\uD802\uDC0F\uD806\uDE8B\u5E70\uBCA8\uD81D\uDCA2\uD840\uDC46\u8F5D\uD83D\uDF70\u47EF\uD85F\uDD3D\uA34E\uFE33\uD872\uDF71\u49D7\uD862\uDD8F\uD81D\uDE4A\uAF06\uCF0E\uD862\uDF2A\uD86E\uDE3B\uD867\uDE05\uD772\uD856\uDEC3\uD86F\uDC2A\uD878\uDCED\uD84C\uDF2E\u83F7\uD866\uDDD0\uD800\uDFB3\u33D4\uD81C\uDEAA\u0653\u7E17\uD858\uDEAB\u70BB\uD821\uDE30\uD85C\uDC09\uD856\uDC6E\u8D9E\uD854\uDF1D\uD86D\uDFF1\uAB78\u8511\uD858\uDE6C\uD820\uDE40\uBC40\u6F4C\u7F3E\uD858\uDE13\uC515\uB0D4\uD842\uDD61\uD852\uDFD8\uD821\uDCD1\uD025\uD858\uDF86\uD875\uDD75\uD86B\uDF7E\uCC14\u8184\uD855\uDF8C\uD844\uDF84\u486F\u7C5F\u607F\uD871\uDF9F\uD85D\uDCBE\u35A4\uD867\uDD8B\uD821\uDE68\uD84E\uDEC4\uC454\uD836\uDCFE\u41CE\uD861\uDFFE\uD852\uDD45\uD821\uDE08\u6B23\uD834\uDD8F\u1A62\uD843\uDF2D\uD864\uDD6A\uD871\uDD5E\uBA1E\uD802\uDF86\uD879\uDD5A\uD840\uDF1E\uD806\uDE00\u2954\uD86D\uDC96\u69A7\uD873\uDC6B\u97C4\uD862\uDE7C\uD872\uDD8E\u4655\uD871\uDC87\uD852\uDECD\uD80C\uDE09\uD84F\uDE14\uD80C\uDC55\uA462\uD872\uDCA9\u8FF6\u112F\uD867\uDDC1\uD874\uDDE7\uB42B\uD84E\uDCA6\uD87A\uDD28\uD876\uDE08\uD844\uDC59\u3339\uD2B2\u1846\uD84F\uDF01\uD843\uDC35\uD844\uDFCF\u3454\uD834\uDF19\uD864\uDFE1\uD81D\uDDDF\uD811\uDD67\uD875\uDC96\u249A\uD802\uDC55\uB8CA\u36E9\uD85D\uDEC2\u554B\uD867\uDEE7\u51AA\uD864\uDC4E\uFDAF\uD869\uDC06\uD854\uDC33\uADF4\u961C\uD861\uDF19\uD869\uDCDB\uD845\uDE48\uD800\uDF5D\uD853\uDFB7\uD86A\uDF06\uD875\uDD36\u7FF0\u5E62\uD865\uDD3A\uD860\uDFAC\uD821\uDEB3\uD862\uDCDC\uBEAA\uD87A\uDEBC\uD811\uDCDC\u85E4\u0361\u2034\u7908\uD845\uDD86\uD874\uDF9A\uD863\uDE70\u7BEE\uD85A\uDFE5\uD836\uDD4F\uC8AB\uD859\uDD0B\u4E29\uD86A\uDC59\uD83D\uDFC6\uD853\uDE9A\uD859\uDC89\u8F4E\u7B05\uD856\uDE6F\uD801\uDEA0\uD861\uDCA2\uA9CC\uD866\uDCAB\uD804\uDE29\uD845\uDF88\u4E60\u2C88\u6647\u7778\uA4AE\uD848\uDFB4\uD872\uDFCC\uD840\uDC2F\uD836\uDC88\uD873\uDDAE\uD849\uDC79\u560F\uD850\uDF73\uD85C\uDF65\uD135\u3BEE\uD84F\uDE98\uD848\uDEAC\uD854\uDD9A\uD877\uDCA7\uD84D\uDE3C\uCBBF\u4540\uD86E\uDD73\u64A0\uD836\uDC2D\uD861\uDF2D\uD81D\uDF78\uD867\uDC15\u250C\uD85F\uDDF0\u2FA3\uBF93\uD870\uDDAA\uD83C\uDCF3\uD86C\uDDE2\uD857\uDF46\uD852\uDE3D\uD868\uDFAC\uD846\uDDCC\uD81E\uDC20\uD86D\uDCFC\u1BB4\uB299\uD820\uDD71\uD867\uDFF3\uD840\uDF70\uD868\uDE0C\uD850\uDCE4\uD868\uDE55\uD85B\uDD96\u7655\uD80C\uDDBA\uD84C\uDF25\uD85C\uDD4B\uD861\uDF4C\uD84E\uDC4F\uD861\uDD1D\uD86E\uDD52\uD802\uDD9F\uD844\uDEB3\u6A9E\uD86F\uDFA0\uB34B\uD80C\uDF65\uD866\uDDF2\uD85F\uDCCA\uB9F3\uD85E\uDE08\uD84E\uDD9E\u183B\uD849\uDF5E\uD85A\uDC39\uD84A\uDD4D\uD852\uDC47\u6858\u011F\uD875\uDF8A\u84DA\u368B\uD876\uDE95\uD81F\uDEFE\uD87A\uDDE4\uA756\u037D\uD855\uDC7A\uD867\uDF03\uAFFC\u5FE2\uD86D\uDC47\uD872\uDE7C\uD856\uDD97\u3AE1\uD874\uDD8C\uD861\uDE70\uD860\uDC9D\uD698\uD81E\uDDF3\u35F0\u93C3\u2793\uD836\uDDBC\uD861\uDD09\uD85E\uDEEE\u9AEF\u1A16\uD849\uDE6D\u813F\uBA8B\uD821\uDC2E\uC8BE\uD83D\uDE4B\u833C\uD855\uDC8F\uD820\uDC58\u1ECD\u5ED5\uD85D\uDC49\u41F2\uD856\uDCBA\u4B3C\u246D\uD81D\uDD4C\u8A06\uD86F\uDD04\u19F6\uD85D\uDDB3\uD82C\uDE45\uD845\uDE39\uD846\uDD6C\u9D84\u1FBF\uD81C\uDF8B\uD872\uDE66\uD848\uDE60\uB7D0\uD859\uDC7A\uD822\uDC9B\u6337\uD855\uDD30\u4E11\u11DA\uD87A\uDCAD\uD811\uDCDE\uD83E\uDC94\uD6EC\u291D\uD81F\uDDE2\uD853\uDFE5\uD856\uDF8A\u4380\u26C1\uD848\uDED7\uD840\uDED8\u9B25\uD873\uDD9F\u302E\uD856\uDEB6\u8A9C\u8AE4\u9243\uD822\uDC9B\uD857\uDD59\uD86A\uDF91\u93BA\uD858\uDCC3\uD85E\uDD7D\uD873\uDCC8\uD85B\uDE59\uD855\uDD6E\\u2029\uD849\uDCB4\uD859\uDFF4\uD81C\uDC89\uD840\uDE15\u16E1\uD834\uDDA4\u689C\u1C70\uD85C\uDFAF\uD84C\uDE96\u6A60\uD851\uDD68\u3783\uD873\uDC9E\u35E1\u8C93\u1B6C\uD821\uDDC1\uC5E0\uDB40\uDD46\uD844\uDC60\u72FC\uD811\uDCCB\uD2F6\uD850\uDE8E\uF992\uD7E2\uB5B6\uD843\uDD4A\uD874\uDC3B\u40E4\u4F3A\u2B63\uD841\uDC98\u90A5\uD878\uDC20\uD821\uDE10\uD81A\uDF0C\uD853\uDF8C\uD836\uDE82\uD877\uDF38\uD867\uDFAF\uD846\uDC72\u2EEB\uD858\uDC64\uD851\uDC29\uD85B\uDCEB\uD851\uDD90\u50B0\uCF83\uD858\uDC19\uD855\uDE6C\uD84F\uDC96\uD878\uDF50\uFEA1\uD873\uDDA8\u0B71\uD820\uDE87\uD866\uDC3C\uD84E\uDF1B\uD855\uDEC8\uD875\uDDCB\u112E\u9220\uD863\uDD49\uB12C\uD84E\uDC02\uD865\uDC4B\uD869\uDF09\u936C\uD800\uDEBF\uD1FD\uD843\uDE31\u4D72\uA279\uD86A\uDC28\u8D16\uD858\uDEAF\u64A7\u100F\uA926\uD85A\uDC90\u86CF\u0664\u9DF3\u3D52\u22AF\uD86B\uDF1E\uD804\uDE80\uCEB3\u6265\u7519\uD81F\uDE52\uC4DB\uD865\uDD91\uD875\uDD25\uD867\uDE8E\uD85A\uDCEC\u67ED\uD857\uDC30\uD849\uDC78\uD841\uDC19\uD874\uDE96\uD841\uDEB1\uD874\uDE7D\uB05F\uD86A\uDC8D\uC01B\uA1E5\uD879\uDC39\uD861\uDC27\uD84D\uDD46\uBD5B\u6619\u7A05\uD851\uDEEB\uD835\uDF3C\uD81E\uDEB9\uD879\uDD0B\u092D\uD874\uDC91\uD864\uDFD6\uD848\uDEBC\uD847\uDDE8\u47BC\uD84B\uDC3F\u503D\uD862\uDF7A\uD851\uDC3E\u36B0\u7459\uA742\uD859\uDD91\uD85A\uDE3E\uC59C\uAAE6\u877C\uD840\uDCB4\uD859\uDC99\uD805\uDF1D\u98F8\uC819\uD85E\uDE2E\uD850\uDFB3\uD874\uDF7C\uD850\uDE51\u7484\uD873\uDCAA\uD857\uDC15\uD84C\uDCDF\u4B44\uD855\uDD04\uD863\uDED1\u47F0\uD858\uDF64\u01C4\u1504\uCF5C\u2E05\uD859\uDFE9\uD843\uDE85\uD846\uDE59\uD848\uDEB1\uD86F\uDEB0\uD873\uDF43\uA8A6\uD842\uDF95\uD878\uDC79\uD877\uDF0F\u4BB8\u663B\uA245\u5299\uD86A\uDF07\uD843\uDF2E\uD84D\uDE1F\u35DD\u87DF\uD83C\uDDF1\u79E4\uD84B\uDD87\uD84D\uDFF4\uD84B\uDE31\uD81C\uDC5B\uD855\uDE1C\u9AA0\u88BB\uD862\uDE94\u6BB7\uD86D\uDDC5\uD869\uDE77\u2D0C\uD801\uDD52\u300A\uD81C\uDE01\uD841\uDE12\uD81F\uDCB2\uD86E\uDDF0\uD820\uDC27\uD860\uDF12\u3C58\uD855\uDDE0\uD870\uDD44\u66BA\uD811\uDE32\uD878\uDC7C\uFF6B\uD846\uDE61\u8F4A\uD84B\uDEC8\uD865\uDF83\u8623\u8499\uD873\uDF41\u62B7\u61AF\uCFD3\uD843\uDC92\u33E5\uD85E\uDFD3\uD86B\uDF32\uD85E\uDF40\u4F9E\uD81E\uDF0F\uD847\uDE10\u6D8F\uD85F\uDFA0\uD87E\uDD2C\u13B3\uB70F\uD872\uDE3C\uD802\uDEC8\uD806\uDCE4\uD848\uDE9D\uD800\uDF30\u83FC\u776A\u2402\uD843\uDF67\uD867\uDCA8\uB6F7\u833E\uD820\uDFCE\uD87A\uDCF6\uD835\uDEED\uB3A3\uBDDE\uD851\uDF4E\uD867\uDF7A\uD84C\uDCDA\uC01B\uD879\uDD17\uD84F\uDDF9\uD878\uDE74\uD855\uDFA0\u2608\u30AF\uD859\uDDAE\uD874\uDDC1\u659E\uD873\uDF8B\uBA09\uD873\uDE87\uD879\uDD29\uD874\uDDC4\uD875\uDC3E\uFBFC\u047C\u5C00\u3EED\u5613\u2D07\uFF8D\u8AC4\uD855\uDDFD\uA153\uB518\u2B91\uD809\uDC6C\uD86A\uDF17\uD1F9\uD802\uDC2C\uD861\uDDD3\uFF38\uD80C\uDD65\uB375\uBE10\uD869\uDF49\uD862\uDDB0\uD86B\uDD7F\uD85E\uDEEE\uD874\uDF44\uD85E\uDD38\u1929\uD872\uDF71\uD855\uDC49\uAD04\uCCED\u3FB7\u1B03\u55E8\uD84E\uDE46\uD853\uDEFB\uD870\uDE40\uD849\uDFAF\uD875\uDF65\u07AF\uD863\uDFE6\uD802\uDEEE\uB8D1\u49E8\uD82C\uDD17\u5104\uD875\uDDAC\uD86B\uDDF6\uD846\uDF30\uD86E\uDFAA\u7BDE\u581A\uD81A\uDDBA\u7017\u2A45\uAC50\uD86D\uDDED\u6562\uD81D\uDF9D\uD84D\uDEFE\u9F00\uA566\uFCEF\u4B7C\u511B\u1C71\u8B8C\uD870\uDD18\uD81F\uDCEB\uD836\uDD4A\u6483\uD869\uDC59\uD86E\uDFC7\uD80C\uDD92\uD510\uD85C\uDFED\uA048\uD862\uDF75\uD877\uDDBB\uD865\uDEA8\uD867\uDF02\uD841\uDDAB\uD86B\uDEB6\uB009\u69E6\uD81C\uDDBA\uD844\uDD69\uD840\uDF41\uD811\uDCB5\uD85E\uDF5B\u78EA\uD86E\uDC52\uD84E\uDE70\uD879\uDDBB\uD10D\uC930\uD769\uA0DE\uD860\uDD27\uD84A\uDC4A\uD834\uDDCF\u8BE6\uBEAD\uD81D\uDE16\uD820\uDF52\uD80C\uDFB1\uD84C\uDDD6\uD870\uDCAF\u8886\uA105\uD808\uDCA9\u6758\uD879\uDF66\u4595\uD86C\uDC68\uD841\uDEC7\uD862\uDD65\u8A16\u712F\uC730\u5A0F\uD864\uDD8E\uD857\uDD5C\uD864\uDEA1\uD878\uDE43\uD87A\uDE0F\uD82F\uDC16\uD870\uDC99\uD800\uDC2E\u738B\u137C\u4EEA\uD852\uDD2E\uD860\uDEF3\uD876\uDC6D\uD802\uDEE2\uD804\uDDC6\u37DB\u7AF4\uD86F\uDDBD\u64FF\u056A\uD84F\uDEE2\uC93E\uD869\uDED5\uD800\uDF3F\u7F4A\u6FB4\uD861\uDF0C\uD846\uDE2A\u6641\u47AA\uD81D\uDCFB\u7419\uD84A\uDE98\uD809\uDC53\u6310\uD845\uDDBF\uD873\uDD5F\uD844\uDDD7\u3595\uD841\uDD4B\uD840\uDCFC\uD846\uDC1E\uB128\uD85A\uDD04\u4B93\uD87A\uDEF0\uD86F\uDF70\uD854\uDE47\uD808\uDF2A\uD836\uDDD2\uD878\uDF70\uD2CC\u9BFB\uD873\uDF95\uD81C\uDD3E\u8AC6\uD85E\uDCE1\u5369\uD853\uDDEC\uD803\uDE6A\u3E63\u85BD\uD82F\uDC9E\u0519\uD879\uDDB1\uD855\uDE15\u6401\uD871\uDF39\uD841\uDC8A\uD857\uDC4D\uD872\uDCB6\u8911\uD822\uDE0F\uD811\uDDD0\uD866\uDF4C\uD870\uDF8C\uD83E\uDC09\uD834\uDE16\uD820\uDE23\u8990\u3C7B\uD872\uDD61\uD849\uDDA4\u1243\uD845\uDF31\uD856\uDDD5\u2AD4\uD81F\uDF0C\uFAB5\uD809\uDD3B\u03E1\uD821\uDC12\uFE85\u4FFF\u2A22\uD876\uDDB2\uD876\uDE9F\uD870\uDCEE\uD855\uDCEF\uD865\uDC96\uD84D\uDDD9\uD86A\uDD62\uD844\uDF86\u5516\uD82F\uDC55\uD847\uDF31\uD868\uDFA2\u4C1F\uCDDA\uD875\uDDCB\u67CD\u852B\uB616\uD808\uDD8B\uD85F\uDF91\uAEC4\uD82F\uDC02\u7BC1\uD879\uDEE9\uD843\uDFB5\uD804\uDE0E\uD85E\uDFFA\u4AE9\uD848\uDD51\uD808\uDF49\uD862\uDCAF\uD81A\uDDDB\uD856\uDFC6\u6603\uD87E\uDDF1\u3F71\uD841\uDD8D\uD868\uDE1B\uD853\uDF6A\uD83B\uDE2D\uD87A\uDE90\u66E1\uD84A\uDC05\uD843\uDDD7\uD876\uDD21\uD862\uDFB6\u7640\u0092\uD845\uDC11\uD840\uDDE6\uD85A\uDE06\uD845\uDEF1\u2DD8\uD850\uDDD5\u526C\u0943\uD871\uDFEA\u08E7\uC9A9\uA0C8\uD873\uDDFF\u7D06\uD873\uDD5D\u146E\uCE31\u52E9\uD858\uDE16\uD849\uDD24\uD875\uDED2\uD868\uDDF9\u3E00\uA98F\uD81D\uDC5A\uD847\uDD1D\u85EF\uD806\uDE1C\u35A4\uD836\uDEAF\uA899\u7138\uD836\uDD4C\uD874\uDC5E\u7192\u69C9\uB01C\u8B54\uD842\uDF3F\uD84A\uDED3\uD85A\uDD34\uD86E\uDF76\u0B5D\uD875\uDD2B\uD873\uDDEA\u41ED\uD863\uDFD8\u3EEE\uD83C\uDF45\uD87A\uDC68\uD850\uDCFC\u2C83\uD801\uDC3D\uD855\uDC8D\uD86A\uDD77\u9986\uD822\uDE91\u3D03\u8CD9\uCCDC\uD821\uDC34\uD87E\uDCD5\uD81E\uDCC2\uD802\uDF28\u6310\uB32E\uBCA1\uD865\uDF5A\u5143\uD843\uDD31\u9C49\uD873\uDE8C\u2797\uD85A\uDE34\uD872\uDF6E\uD879\uDD70\u3A54\uD854\uDE26\uD86B\uDF20\uD81F\uDE8B\uD865\uDDA2\u75E0\uD80C\uDC3B\uFE7E\u1D08\uD84D\uDCE1\u628F\u2C31\uCD70\uD847\uDF86\uD84D\uDDC5\uD85E\uDD0F\uD879\uDEF3\uD845\uDE3C\uD866\uDD3B\uD864\uDE41\u3E2E\uD81F\uDD9A\uD848\uDE7C\uD861\uDC48\u7D00\u3C08\u7A91\uD868\uDCE0\uD86B\uDD16\uD84F\uDF51\uD80C\uDECA\uD853\uDFB3\uD85A\uDEA0\uD862\uDC59\uD874\uDCAA\uD879\uDD71\uBE48\u3D8E\u79F4\uCA2A\u8043\u43E6\u557E\uD86C\uDD29\uD873\uDE86\uD875\uDE80\uD835\uDE2D\uD83E\uDC34\u2E91\uD870\uDEA1\uAF93\u3A90\u8941\u1A5B\uD845\uDC02\u81B8\uD852\uDFEF\uD84D\uDF43\uD859\uDF73\u0D37\uD81F\uDD2D\uA241\uAC55\uD85C\uDDBF\u6477\uD863\uDDB7\uD86D\uDEE2\uD85F\uDD26\uD84C\uDE0B\uD76F\u205B\u8B92\uD84E\uDFA6\u9228\u7966\uD86A\uDF5D\u2606\uD84F\uDD7F\uD858\uDD7A\u581B\uD874\uDCCD\uD804\uDECF\u64BD\uD863\uDD22\uD822\uDC21\u9C88\u689E\uD86E\uDC4E\u857D\u0E39\uD83D\uDFAD\uD81E\uDF9A\u6338\uD869\uDC89\uD85B\uDC4D\u4774\uD81A\uDF8D\uC736\uD85A\uDF4C\uD836\uDDEB\uD850\uDFFB\u4424\uD84F\uDEFC\uBDAC\uD821\uDE90\uD85E\uDF4D\uD81F\uDCEC\uD033\uD840\uDEAE\uD85A\uDD6C\uD86F\uDF76\uD864\uDF1A\uD857\uDCDF\uFA73\uD852\uDF73\u052F\u9525\uD809\uDD2F\uD83D\uDD10\uD845\uDC8D\uD84D\uDE9D\uD84F\uDC2C\uD858\uDC4B\uD84F\uDD0B\uD87A\uDF09\uD877\uDFE9\uD859\uDD15\uD801\uDC69\uD81D\uDEB7\uD86C\uDCDC\uD846\uDC75\uD846\uDD5E\u9EDA\u7E8F\uDB40\uDDBA\uD85E\uDD0D\uD84F\uDF82\u20ED\u4C5C\u8DDA\uD81C\uDEBC\uD847\uDE7B\uD842\uDD69\uD858\uDD6F\u8FFB\u3741\uD863\uDF98\uD850\uDE38\uD869\uDC6A\uD87A\uDFC5\uD849\uDEC1\u3992\u2287\uD870\uDFDA\uBE71\uD840\uDDF7\uD843\uDD27\u37AB\uD808\uDCD0\uD865\uDD04\uD853\uDE2A\uD849\uDC92\uD86A\uDCDA\uD866\uDF24\uD862\uDF41\uD853\uDF8A\u63E4\uD852\uDDC3\uD858\uDF5E\uD86C\uDEA5\uD84C\uDD12\uD86D\uDDB0\u6E21\uD804\uDD40\uD574\uD869\uDD4D\uD840\uDEE0\u83F1\uD85D\uDFFB\uD13B\u600D\uBC1B\u2C85\uD851\uDD41\u6AAD\u407C\u8F1F\uAC4B\uDB40\uDDC1\uD83D\uDCB0\uD848\uDEEF\uD861\uDC62\uD85A\uDCE8\uD86B\uDC85\uD873\uDFA4\u484B\u1524\uD878\uDD8C\uA9E7\u8171\u86A2\u97A5\u239B\uD835\uDFAF\u73BF\uD85A\uDE79\uC17B\uD85A\uDD66\uD85C\uDCA4\uD834\uDE0D\uD83D\uDF3E\u8755\u6193\uD862\uDC35\uD86B\uDE7B\u9D92\uD822\uDC7E\uD879\uDF43\uFE23\uD86E\uDE5C\uB917\uD86F\uDECC\uD846\uDDFB\uD81A\uDCA5\uA8D2\uD81B\uDF3F\uD81C\uDFAC\uCCF2\uD84B\uDF81\u8391\uD866\uDC69\uD875\uDCCC\uD1B9\uD860\uDDD9\uAB01\uFCBF\u4807\uD859\uDECB\uD86E\uDE24\u71E8\uD857\uDFBB\uD802\uDC68\uD843\uDD71\uD865\uDEC9\u1695\u9246\uD845\uDFED\uD83D\uDF6C\u053B\uD84C\uDFBC\uD86F\uDE33\u2526\uD83C\uDD93\uD874\uDD20\uD85E\uDF48\uD83C\uDF29\uD846\uDE9F\uD86D\uDF57\u91ED\uD87A\uDDA8\u86EC\uD851\uDD42\uD876\uDC27\uD873\uDE0F\u58A6\uD862\uDEC7\u7535\uD860\uDFFC\uB5E9\u3D34\u32F3\uD841\uDC92\uD66E\uD83C\uDF4C\uD855\uDD20\u7FE3\uD807\uDC58\uD822\uDE45\uD84D\uDDF1\uBD8F\u26F2\uD848\uDC35\uD81C\uDE37\uA8B7\uD84E\uDDA2\uD866\uDD60\u6433\uD86B\uDFBE\u431B\u43E5\uD861\uDC8A\uD849\uDCC0\uD862\uDCD5\uD844\uDFBA\uD864\uDEBD\uD804\uDEA5\uD878\uDD37\uD87A\uDFA1\uD856\uDCFF\u6EBC\uD87E\uDD2E\uD848\uDE9B\u55B8\uD846\uDCED\uD81C\uDC50\uD845\uDCEF\uD86F\uDD66\u0C08\uD801\uDEF6\uD85C\uDE64\uC6BE\uD84B\uDE9B\u3320\uD861\uDD3F\uD81F\uDFDC\uD859\uDD76\uD851\uDDE0\u261F\uD835\uDCF6\u3400\uD853\uDEBE\u9F4D\u311A\uFE0E\uD85E\uDFCD\uD865\uDD09\uD852\uDEB8\u070D\uD847\uDEF8\u04E1\u53FF\uD842\uDE64\u55C7\uD85A\uDE5F\uD87A\uDE0F\uD835\uDC1F\uD842\uDE63\u6F3D\u1744\uD847\uDE23\uD873\uDD5A\uA990\u04DE\uD835\uDCB3\uD840\uDE4E\u4E63\uD845\uDDA1\uD86F\uDCCF\uD842\uDF08\uD85B\uDE72\uD84B\uDFB6\uD846\uDE0F\uD809\uDD1B\uD855\uDF29\uD84A\uDCE9\uD864\uDC38\uD81B\uDF2F\uA2EF\uD853\uDCE5\u14BA\u9435\u7FD2\uD875\uDE7E\uD81A\uDC8E\uD80C\uDFF4\uD820\uDD92\u7967\uD841\uDEEE\u8393\u3C83\uD87E\uDC8A\u844A\uD84A\uDE53\u4895\uA34E\uD861\uDD73\uD854\uDE98\u88DC\u1BA4\u7B6B\u0F1C\uD873\uDDD7\uD86C\uDF01\uD168\uAD5A\uD876\uDF29\uD860\uDC40\uD80C\uDF2C\uD879\uDEB5\uD801\uDEF0\u0544\u8421\uD83D\uDF08\uD857\uDF51\uD867\uDD9D\uD867\uDFB1\uB404\u4724\uB132\u081E\uD86A\uDE8D\u3597\u9DDA\u751E\uC58C\uB2E7\uD875\uDD65\uD82C\uDCF0\uD842\uDEE6\uD81A\uDE64\uD848\uDF86\u9AA0\u87A1\uD866\uDFD4\uD855\uDD29\uD871\uDDD0\u45C1\u8AE2\uD851\uDF00\u62DE\uD855\uDDEE\uD84A\uDC9C\uD84D\uDFCB\uD805\uDE58\uD820\uDEED\uD846\uDE6E\u7B32\uD878\uDEE2\uD84A\uDC92\uD808\uDE7C\uD84E\uDE47\uD805\uDCB8\uCD1B\uD804\uDD1E\uD803\uDCA3\uD808\uDF1F\uD846\uDD23\uD860\uDF17\uD84C\uDE52\u9C27\u2C53\uD85D\uDF8E\u8A55\uD800\uDE96\uD85A\uDF07\u7E8F\uD858\uDD45\uD85F\uDF5A\uD854\uDD14\u5C1D\uD861\uDFE6\uD842\uDCC2\uD81A\uDD70\u8128\uD86D\uDEFE\uD86D\uDDC4\uD844\uDFB7\uD862\uDF98\uD85C\uDF21\uD86D\uDF9B\u0DA5\uAF3F\u966B\uD81C\uDD62\uD83E\uDC83\uD86B\uDFAF\u9CF6\uD85F\uDE74\u47E9\u25FE\uD864\uDC07\u3108\u7467\uD85C\uDE1B\uD83D\uDC9B\u83F6\u37DE\u12F9\uD851\uDC05\uD83C\uDCA6\uA223\u344A\u6039\u2361\uD855\uDC44\u1439\u4758\uC9FD\u7820\uD855\uDE93\u3D7D\u5F8A\uD83C\uDE13\uA643\uD85F\uDC9D\u8550\uD85A\uDC95\uD835\uDEC8\u541E\uD802\uDC0F\uAF1B\uCFEC\uD855\uDEE5\uA0EB\uD821\uDC2C\u406F\u5739\uD87E\uDC91\u23DE\uD870\uDF41\uD110\u516F\uD843\uDC58\uD846\uDD53\uD81C\uDD62\uD834\uDF19\uD835\uDD7E\uD808\uDEEC\uD86D\uDD72\uD878\uDC92\u8273\uD85B\uDE1C\uD867\uDC1E\u2B1D\uD870\uDC31\uD851\uDF1F\uD85E\uDF7C\u324B\uCC6C\uD85E\uDE44\u7F85\uD841\uDFD8\uD851\uDC71\uD879\uDEDC\uD87A\uDF05\uD801\uDE5F\uD864\uDF5B\u119E\u29B3\uD844\uDED6\uD85B\uDE57\u853D\uD856\uDFD0\u486F\uD869\uDC7C\uD875\uDD73\uD87A\uDDED\uCE3C\uD81A\uDC28\uC85E\uD85C\uDDFC\u373C\uC174\uD868\uDE6D\uD83D\uDE28\uD808\uDC39\u74B1\u6BE3\uDB40\uDDC3\u8907\u4DF5\uD860\uDEB6\uD864\uDFAB\u0F55\uD84B\uDE0A\u1851\uD81D\uDDDB\uD850\uDD7E\uFF71\uD82C\uDE36\uD859\uDE0F\uD841\uDE0D\uD83C\uDFB4\uD848\uDF8B\u3476\uD80C\uDF25\uD7E9\uD85B\uDC9B\uD80C\uDC78\u38A0\uD84D\uDE0F\uD871\uDC10\uD86F\uDEAA\uD802\uDD98\uD84A\uDFF2\uD822\uDDC2\uB39B\u301D\uD84D\uDDF5\uBDC5\uD865\uDD56\uD879\uDE8E\uD855\uDE0A\uD821\uDC85\uC25D\uD84F\uDDA6\uD841\uDC52\uD878\uDCD5\u4C4F\u2E9C\uD821\uDC6C\u6083\u719F\uD84C\uDC47\u8D6B\uD86B\uDFE8\u056E\u4703\u60EF\uFF21\uFE9C\u6B09\uD85A\uDD10\uD213\uDB40\uDD23\uFBF8\uD86D\uDC88\uD854\uDD1E\u8FB2\uD841\uDFA0\uD857\uDFFE\u9618\uD870\uDF1C\uB3B7\uD861\uDEF6\uD854\uDCC6\uA2B8\uB8D1\u95E1\uD84E\uDEE8\u3253\uA58D\uD851\uDDF4\u6C4C\u9963\uD86D\uDF98\uD868\uDF4F\uD820\uDC90\uD86D\uDCC1\u6ACC\uD804\uDC28\uD877\uDC66\uD86A\uDD7B\uD876\uDEA4\u3E90\uD80D\uDC01\uDB40\uDD2A\u8813\uD850\uDCC0\u02D5\uD1F9\uD86D\uDFFF\uD850\uDFF7\uD81C\uDC5B\u97C6\u6C4E\uD83D\uDF6D\uD848\uDF70\u75DA\uD861\uDE21\uD841\uDEB4\uD84A\uDF01\uD842\uDE83\uB65E\u8367\u22E8\u5885\uD82C\uDC27\uCC2E\uD85C\uDD77\uD858\uDCD0\uD876\uDFF0\u0670\u4ED4\uBF9C\uD83E\uDD48\uD841\uDDA2\uD674\u9A95\uD862\uDFF0\uAEFD\uAE14\uC0C5\uD84C\uDC39\uD850\uDD59\u4FEB\u5F87\u825C\uD845\uDF61\u5B57\uB83F\uD82C\uDCF8\uD84D\uDF12\uD86A\uDCB9\uD854\uDC37\uD849\uDEC3\uD848\uDD2D\u7CD9\uD877\uDEE7\uAF4A\uD81E\uDFC3\uD87A\uDF33\u90AC\uD85E\uDC14\uD836\uDD7A\uD81A\uDF10\uD867\uDD36\uD844\uDCDD\u2467\u39E9\uD85D\uDC7C\u9721\u88E6\uDB40\uDC3B\uD860\uDFAD\uA375\uD83B\uDE8C\uBB67\uD873\uDEF3\u4571\uD864\uDFEF\uC9B5\uAF14\uD86D\uDD8F\u7799\uB5C2\uD81C\uDCD9\uD872\uDE5D\uD82C\uDC33\uD86B\uDD40\u5D1D\uBE98\uD811\uDC4E\u4E84\uD87A\uDFCF\uD809\uDD30\uAD3D\u3A83\u643D\u4521\uD86E\uDF23\u3665\uD873\uDD9C\u2DED\uD820\uDF5F\uD3B2\uD862\uDD37\u0269\uBB59\u0AB2\uCB7A\uD847\uDD0C\u5BD3\uD83D\uDD32\u29A4\uD836\uDC84\u6F08\u4E4A\uD878\uDDE8\uD876\uDC69\u16F6\u21E9\uD804\uDC5C\u4265\u5C3F\uD851\uDCE7\u547B\uD848\uDDE7\uD855\uDF7F\uD865\uDC73\uD85A\uDEAA\uD849\uDFA4\uD84F\uDE0B\uD861\uDF83\uD87A\uDF2A\u0989\uD875\uDE78\u9E1E\uD841\uDC45\u832F\uD836\uDDB7\uD85A\uDF6A\u70EE\uD81E\uDFCA\uD85F\uDFED\u07C3\uD85F\uDDE7\u9312\uD807\uDC10\uD86F\uDE41\uC88A\uD86E\uDD85\uD811\uDD10\u0CC2\u96E9\uD85A\uDC52\u8A9C\uD879\uDE7B\uD80C\uDCFC\uD85F\uDEB9\uD851\uDC50\uD85D\uDFAA\u4780\uD835\uDC8C\uD866\uDC10\uD868\uDD03\u0352\uC2C0\u7CF7\uADF9\uD82C\uDC78\uD86C\uDE47\uD848\uDF7E\uD855\uDC11\uD846\uDD34\u7A5E\uD875\uDD13\uD84A\uDC0B\uD2CA\uD835\uDC5A\uD81F\uDDD1\uD84B\uDE88\u7036\uABCA\uD865\uDF42\uC62B\uC51C\uD84B\uDEF2\u8136\u3C33\uD871\uDC9B\uAEA1\u7784\u5CB1\uD81D\uDF0E\u4971\uD849\uDCDE\uD86D\uDE44\uD85B\uDC86\uD847\uDD0C\u17E2\uD85A\uDC55\uD84C\uDED6\u9649\u62A9\u33E2\uD836\uDE4D\uD44B\u7BFE\uD836\uDE79\u5C62\uD865\uDE86\uD858\uDDE0\uD80C\uDF54\uD84F\uDEA1\uD843\uDDEF\uD803\uDC44\u2123\uD87A\uDF55\uD877\uDDBA\u5CD9\uD834\uDCA8\uD836\uDCBB\uD808\uDC65\u16DC\u1F6F\uD87E\uDC12\u4CF3\uD81D\uDD46\uD821\uDCB6\uD86E\uDF12\uA539\u0837\u76F4\u1108\u95D1\u34CD\uD805\uDEB4\u3766\u03BE\uD81C\uDCBB\u8312\uD865\uDC63\u4A8B\uD804\uDE36\uD820\uDF88\uB3EB\u9584\uD86F\uDD25\uD843\uDCBB\uDB40\uDC4A\u3B3E\uD84D\uDC6B\uFA9F\u15E2\uC5B8\uD856\uDC76\uD867\uDE6E\u9CC0\u7353\uD855\uDC47\uD854\uDFFC\uD873\uDF6B\uD855\uDD49\uD85F\uDC00\u8B73\uD841\uDDA5\u9238\u589C\uD855\uDEB7\uD85E\uDD4A\uD846\uDD96\uD808\uDE62\uD82C\uDCF9\uD81E\uDD7C\u6940\uD844\uDECC\uD861\uDEE4\uD87A\uDE04\u5D8B\uD84F\uDC28\u1BB5\uD846\uDFF7\u8D69\uD860\uDEBB\uD85D\uDF3A\uD85A\uDE53\uD864\uDD95\uD85F\uDFA3\uC23F\u4739\uD878\uDEF7\uD800\uDE88\uD86B\uDDDC\uD840\uDE18\uD854\uDFA1\u654D\uD842\uDDD6\uD834\uDF55\uD821\uDD33\uB3B4\uD85B\uDEC5\u7A0A\uD862\uDDC3\uD876\uDDF9\uD875\uDE5F\uD877\uDC9E\uD804\uDC9C\uD878\uDFA2\u7BB7\uA576\uD848\uDECB\uD81C\uDF99\uD85C\uDFAB\uD835\uDC26\uD84E\uDD0A\uD860\uDE5A\u82BB\uD857\uDEEB\uD81F\uDF05\u0974\uD860\uDD22\uD876\uDD43\uD84A\uDC59\uD834\uDDB0\uD81F\uDE09\uD87A\uDC53\uD875\uDE1F\u28B6\uD870\uDFEB\uD840\uDEAC\uAFA6\uD83D\uDEBB\u082A\u8A68\u3517\u6411\uD856\uDDA4\uD86F\uDE37\uD855\uDD82\uD854\uDEA4\uD835\uDC02\uD871\uDE24\uD821\uDC4B\uD81C\uDEBE\uD862\uDE5E\u56BE\u89CC\uBF6F\uD877\uDF59\uD81E\uDE8C\u9FC2\uD808\uDDB3\uD874\uDE47\u169A\uD875\uDCCB\uC1C9\uD835\uDEBF\u67C0\u676B\uD836\uDDA4\uD841\uDC14\u8569\uD81C\uDEEB\uD856\uDCE6\uD85C\uDEFA\u88ED\uD847\uDD12\uD873\uDED8\uD850\uDC14\u61FD\uD85B\uDF1B\u5C37\uB979\uD840\uDC78\uD860\uDE41\uD84E\uDFE8\uD808\uDC9B\u47CC\u7928\uD86A\uDFDC\uD850\uDE9B\uD821\uDC95\uD811\uDD42\uD863\uDE15\u73B4\uD852\uDCB6\u1662\u96CB\uD866\uDCF7\uD84E\uDE0C\uD83E\uDD8E\u68D6\uD85A\uDCD3\u4F31\u8A54\uD821\uDED2\uD871\uDF20\u5EEA\u4DF6\uD859\uDE43\u2C7D\u2166\uD868\uDD22\uD835\uDE6B\u0854\uD85E\uDE0F\uD84B\uDEB9\uD871\uDD60\uD848\uDFDD\u961B\uCD9C\uD86C\uDD57\u8491\u13A6\uD807\uDD1C\u25A9\u22A6\uD84E\uDCC1\uD81C\uDE8F\uD821\uDF0F\uD85F\uDCDE\u2E1A\u23EB\uD85F\uDD24\uD81D\uDDA5\uD84C\uDFE3\uC41D\u76C7\uD85E\uDF8C\uD81D\uDED0\uD864\uDE62\uD861\uDF24\uAC4F\uD872\uDCC7\uD846\uDEBD\uD84E\uDD6C\uD5CC\uD836\uDC01\uD847\uDEB7\uD841\uDFEF\uD84C\uDD80\uD809\uDCD9\u6DA0\uD869\uDC90\u0331\uD7E1\u9720\u193B\uD879\uDF8C\uD857\uDF72\uD848\uDEF1\uD834\uDD16\u04B6\uBE33\uD878\uDF6C\uD843\uDEE6\uD808\uDF5B\uD855\uDC4A\u118A\uD85C\uDD92\uFAC0\uD851\uDDA1\u1C7D\u80D8\uC33E\u6C68\uD876\uDE42\u40DE\uD84B\uDC9A\uD855\uDCE3\uD844\uDEF6\uD873\uDFF2\u8F62\uD84E\uDD59\u43EC\uD841\uDFCB\uD851\uDE76\u6F27\uD857\uDD2B\u9821\uD836\uDE3C\uD820\uDE02\u45CE\u2B81\u07AB\uD84D\uDC6A\uD81C\uDFF4\u6F91\u11CD\uD808\uDD6C\uD849\uDE1C\uD843\uDCDA\u5180\uD86B\uDF40\uD862\uDD1C\uD808\uDF04\uD852\uDD14\uD872\uDDF2\uD848\uDC43\u42E1\u0607\uD81D\uDC18\uD83A\uDC04\uD855\uDF29\uD820\uDED0\uD85E\uDE13\uA78E\uD860\uDC73\uD806\uDE5A\uD864\uDEA2\uD848\uDECF\uB6A7\uD860\uDD12\uA8A9\u29A6\u638A\uD867\uDCAB\uD874\uDCD0\uD85D\uDF05\uD81D\uDD2B\uD843\uDEA9\uCCEF\u636A\u8445\u5B47\uD869\uDE66\uD84C\uDE79\uD86D\uDDEF\uD870\uDFEE\u30DB\uD86E\uDD53\uD85B\uDCA1\uD868\uDE4C\uAB24\uD852\uDD5B\uD851\uDE0D\u7525\uD84E\uDF26\uD86E\uDD8C\u1D38\uD85F\uDF73\uD86E\uDD63\uD866\uDEFA\u3E6F\u1D19\uD843\uDF8C\uFF88\u205D\u858A\uD85A\uDEAC\uD86F\uDC19\uD874\uDE7B\uD879\uDE47\uB1A3\u878B\uD85D\uDCF2\uD862\uDCCF\uD876\uDDD2\uD847\uDCF0\uD845\uDF22\u909E\uD84D\uDDA9\uD859\uDDDA\uADD5\u4B9E\uBB44\u745F\u67A9\uD803\uDC96\u7A4D\uD85E\uDE46\uD87A\uDC6B\uB69E\uCF38\uD851\uDF62\uD801\uDF50\uD82F\uDC77\u8932\uD875\uDE7B\u1610\uD52A\u6A51\uD5B4\uD852\uDD63\u0C60\uD86D\uDCD8\uD822\uDE11\uC621\uD802\uDCFD\u8D9E\u98B4\u6F03\uD849\uDCFB\u1B30\u1922\uD846\uDFF9\uD855\uDF14\uDB40\uDD16\uD867\uDC1D\uD848\uDD5E\uD850\uDC3D\u3E97\uD840\uDE88\uD86C\uDC85\u485C\u8A3C\u5C45\uBBDE\uAA41\u6AEA\uD855\uDF96\u8381\uD84F\uDFC0\u7381\uD81C\uDF14\uD86B\uDF20\u1985\uD879\uDCAA\uD80C\uDD5A\uD872\uDC36\uA3CD\u7499\uD85D\uDCF2\u18F4\uD857\uDDB7\uD851\uDDE6\uD834\uDDD0\u3372\uD877\uDCFE\u9DF7\u6C12\uD851\uDC70\uA53D\uBC94\u5449\uD849\uDFE4\u8D71\u23A7\uD85E\uDE58\\u000b\uD85E\uDFCE\uD2DA\uD859\uDCF0\uD873\uDC08\u9C20\u2AB6\u7488\uD879\uDE04\u3487\uD86E\uDFD2\u80DA\u445F\uD801\uDCDF\uD84D\uDD95\u6683\u91C8\uD841\uDD28\uD81D\uDE3B\uD821\uDC21\uD853\uDF02\uA9AC\uD81C\uDDE7\uD846\uDD74\uD854\uDD56\uD802\uDC35\uD846\uDF52\u4DA1\uD875\uDCF7\uCCE7\uD854\uDD16\uD869\uDC44\u4E6C\uD85D\uDC13\uD873\uDCBC\uD852\uDF11\uD809\uDC8A\u09A1\uD834\uDC56\uD840\uDE4C\uD878\uDCE6\u600F\uD85D\uDF5A\u59A5\uA8B1\uD86F\uDC4A\u63A8\uD86C\uDEE0\u9DFD\uD86D\uDC26\uD81F\uDCA5\uD84D\uDC97\uD86D\uDC55\u3A08\uD81C\uDC58\uD821\uDCBB\uD800\uDEB4\u28E6\uD860\uDF98\uD801\uDCE5\u7E28\uD864\uDFF4\uD821\uDC39\uD81E\uDCD8\uD81A\uDCE2\uC7B9\uD83C\uDF26\uD847\uDDCB\uD84D\uDE60\uD811\uDE38\u7532\uD81A\uDC16\u5513\u30F6\u3C8E\uC116\uD811\uDDA9\uD846\uDEC2\u52F4\u5C1C\uD80C\uDDC9\u10A6\uCABD\uD844\uDFE3\u094C\uD863\uDF3C\uD84D\uDD9D\uD821\uDDF6\uD855\uDCBC\uD822\uDC12\u70E0\uD859\uDC5F\u623A\uD877\uDF0F\uD85D\uDD75\uD866\uDFE2\uD86A\uDF1C\u4D12\uD822\uDC6F\u81E1\uD877\uDC3A\u4B42\uD872\uDC66\uD87A\uDCD9\uD850\uDF84\uD878\uDE8B\u2941\u2F62\u8A09\uD81C\uDC5C\uD836\uDE88\uD84F\uDE7D\u5152\u188A\uD80C\uDF17\uD82C\uDDEF\uD86A\uDE2D\uD868\uDCDC\uD81F\uDF1A\u9249\u8D10\uFE44\uD858\uDEFF\uD850\uDEBA\uD853\uDE41\uA863\uD803\uDE72\uD879\uDCF5\u050E\uD846\uDD5F\u5124\uA2C5\uD86E\uDED3\u6FED\uD820\uDF3E\uA502\uD846\uDE7C\uD453\uD866\uDE6E\u05DE\uD855\uDD5F\uD848\uDD52\uD84C\uDF2B\uD846\uDE2D\uD863\uDCAC\uD869\uDE0B\uD86E\uDC67\uD821\uDE37\uD842\uDC2A\uD802\uDE65\uD862\uDC66\u919D\uD83C\uDC43\u6A39\uD866\uDCCC\uB78D\u5F30\uD874\uDE53\u29DB\u3432\uC9BA\uD83A\uDC90\uD821\uDDA7\uD851\uDEF4\uF9C3\uD85F\uDE34\uD81E\uDEC3\uD866\uDCCE\uD835\uDD9E\uD87A\uDE2D\uD86C\uDD15\u0449\u8128\uD86A\uDC4F\uD874\uDD3E\uD84C\uDED9\uD85C\uDFEC\uD851\uDE65\uD856\uDEA9\uD802\uDDA6\uD85B\uDFC3\u5E4F\uD835\uDCD7\uD86E\uDE7A\uD84B\uDC33\uD3F9\uD878\uDC56\uD846\uDFE7\uD867\uDF70\u4325\u3CE0\u95F3\uA6A1\u690B\u012A\uD81A\uDC11\u55E8\uD850\uDC04\u8321\uD879\uDE6D\u58D0\uD853\uDD57\uFDF5\u76E3\uD805\uDE8A\uD81C\uDCBF\uD81B\uDF71\uD849\uDDDA\uD866\uDD72\uD869\uDF7A\uD83C\uDC5C\uD867\uDD0B\u2CF1\uD84D\uDD0E\u94E4\u67AE\uD876\uDF2A\uD85F\uDEEE\uD834\uDC31\uCDFD\uD811\uDDFA\uD85B\uDD2C\uD822\uDE71\uD81F\uDE1A\uD83D\uDDCC\u03E2\uD84F\uDEB5\uD86A\uDEE9\uD2D7\uD873\uDD0E\u24F3\uD871\uDC53\uA93B\uD857\uDD08\uD859\uDEF7\uD822\uDD0B\uD869\uDCCF\u870F\uD86E\uDCD7\u2C00\uD85E\uDF01\uD866\uDC8E\u749B\uD85C\uDF13\uD868\uDD84\uD865\uDFF4\uD87E\uDD9B\uD846\uDDF9\u99E7\uC088\uD85D\uDFD8\u875E\uD855\uDD33\uD806\uDCB0\uD856\uDD87\uC92D\uD85F\uDEA2\u88EB\uD85B\uDCF3\uD863\uDE3F\uD809\uDC89\uD81F\uDDCC\uD859\uDF39\u24E3\uCF70\uD83D\uDC45\uD86F\uDF21\uD821\uDD2C\u4C90\uD83D\uDFCB\uD845\uDED7\uD806\uDE7B\uD84D\uDFDA\u11FC\uD850\uDEB9\uD84C\uDE00\uAB65\u6519\uD841\uDF8F\u28F1\uB12D\uD859\uDF5D\uD863\uDF90\\\"\uD85B\uDD13\uD858\uDF26\u5964\u8F03\u3A46\uD806\uDED1\uD804\uDD76\uD878\uDF66\uD81F\uDD6B\uD878\uDE2C\uD81C\uDD81\uD874\uDC8A\uD862\uDE29\uD800\uDEB4\u0CCD\u547C\uD844\uDDA8\uA88E\uD81C\uDD12\uD81A\uDDD9\uD834\uDF31\u46CE\u6A78\uD841\uDF71\uD86C\uDD02\u3FFC\u9752\uD868\uDEEA\u673C\uD86D\uDC48\u4863\uD857\uDDF9\uD800\uDEC2\uD861\uDF1E\uD81F\uDE93\u9E97\u5EE0\uD84C\uDC52\uC500\u076C\u9381\uD83C\uDC24\uD840\uDFDD\uD840\uDD29\uD855\uDC74\uD859\uDF2E\uD86A\uDE25\u58FE\u4AEC\uD844\uDE6E\u2625\uFD33\u1617\u701C\uD871\uDD2B\uD840\uDE3B\uD861\uDF43\uD86E\uDDFF\uD80C\uDF66\uD85F\uDC58\uD879\uDF62\uD865\uDF9C\uD84C\uDCB6\uD840\uDF3E\u85DC\u2CE2\uD84B\uDD5F\u1EB0\uD86F\uDF9F\uD840\uDD14\uD859\uDD10\u6092\u4CE5\uD850\uDFD2\uD87A\uDFAC\uD84E\uDD3E\u384B\uD81E\uDF1E\u585C\uD873\uDE74\uD83D\uDEC6\uC989\uD860\uDE84\u72ED\uD85D\uDFAD\uD86C\uDFE9\uD81E\uDD2C\uD859\uDCAD\uD840\uDC8E\uD868\uDC17\uD875\uDFD3\uD801\uDD16\u3236\uD844\uDF96\uD874\uDF3B\uC7F3\uCF3C\u6870\u04FD\u2535\uD86C\uDE22\u91C5\uD811\uDD0C\uD86A\uDF1D\uD866\uDD85\uD862\uDD8A\uD860\uDF22\uD83C\uDCA1\uD84C\uDFBD\uD842\uDF84\uD874\uDCBD\uD861\uDE8A\uD85F\uDC97\u5F0C\uD845\uDFE1\uB8A7\u7721\u43BF\uD84E\uDF93\uD867\uDF26\uD854\uDCD9\uD852\uDFFF\uD86D\uDC11\uD879\uDEE9\u950E\uD866\uDE28\uD863\uDF13\u67F4\uD81F\uDF5A\uD85E\uDC8E\u0446\uD868\uDC39\uD81F\uDDE7\u9FAD\uD820\uDC40\uB55E\uD859\uDC6C\uD848\uDECD\uABB1\uD860\uDCBD\u80B6\u9028\u6611\uD844\uDCE1\uD86E\uDCB8\uD81F\uDE1B\uD85C\uDCC2\uD844\uDE78\uD858\uDF2E\uD847\uDF92\uD805\uDC45\uD84C\uDD34\u73D6\uAF33\uDB40\uDD2E\uD877\uDECE\u8B16\uD85D\uDD55\uD878\uDF4A\u4954\uD86D\uDD5B\uD870\uDD63\u29FE\uAFFB\uFE87\u7187\uA0F6\uD807\uDC3C\uD854\uDE4F\uD86D\uDCBC\uD846\uDF9A\uD853\uDF06\uD81F\uDE23\uD842\uDF24\uD821\uDED0\uCF54\uD85F\uDDA1\uD864\uDFC7\uD879\uDD4F\uAD88\uD859\uDEA7\uD861\uDFC9\uD3C5\uD864\uDD81\u3811\u02A8\uD848\uDD8D\uD834\uDD54\uD86F\uDD20\uD868\uDE95\uD83D\uDD7D\u388B\uC799\uD86E\uDC18\u5780\uD84F\uDF5C\u6557\uD84B\uDEA8\uD845\uDE8F\uD856\uDF31\uD865\uDD07\uD84E\uDD7D\uD83E\uDD83\uD856\uDD35\uD857\uDEBA\uD845\uDF42\u68DB\uD850\uDC1D\uCCC0\u996A\uD862\uDC2E\uD861\uDF3A\u288D\uD81F\uDEC7\uD820\uDC94\uD852\uDDD3\uD84A\uDDD2\u6E1A\u9973\uD844\uDC4D\uD81E\uDEA3\uD84D\uDEBC\uD843\uDC64\uD858\uDEB0\uD86E\uDD95\uD81E\uDD63\uD85D\uDCDB\uD821\uDC99\uD87E\uDC6D\uD852\uDEDD\uA7FC\uD85A\uDF0A\u42A9\uD84E\uDE37\uD81E\uDCDB\uD868\uDF71\uD820\uDC1D\uD850\uDDD4\u083D\u5069\uB72A\uAD32\uD83C\uDC62\uD83C\uDF37\uC809\uAE51\u5BEE\uD874\uDEA9\uD874\uDC88\uD842\uDF8C\uA14E\u583F\uD85B\uDF78\uD86B\uDFCE\uD86D\uDC96\u2910\uD864\uDFB6\uD861\uDDDB\uC9D3\uD821\uDFE6\uD85A\uDDF0\u58DA\uD866\uDFF5\uD846\uDC7C\uBCBB\uD875\uDF3B\uD862\uDFDC\uD811\uDCBA\uC649\uD859\uDE21\uD867\uDC6B\uD874\uDC99\u1123\uD840\uDF87\uA6B5\uD868\uDE1D\uD84E\uDCC1\u371C\uD802\uDEE4\u9DDC\uD875\uDD50\uD86E\uDFE4\u94BB\uD821\uDE4F\uD86F\uDE77\uD84A\uDE25\uAB56\u4907\uB212\uD81A\uDF2E\u1DF8\uD86D\uDD5D\uD80C\uDE84\uB594\uD80C\uDF76\uD866\uDD85\uD805\uDCA9\uD840\uDCF5\uD862\uDC6C\uD85F\uDD6C\u8A0B\uD874\uDC24\uB94E\uD81C\uDC20\u5E1C\uD844\uDF81\uD80C\uDC3E\uD844\uDFFC\uD81D\uDE1B\uD866\uDCF4\u80D4\uD853\uDD24\u17C5\uD808\uDE0B\uFBA2\u67F3\u8D19\u9C3E\u66BE\uD87E\uDD65\uB122\uD874\uDE31\u6B21\uD851\uDF1E\uD849\uDD03\u8033\uD81F\uDF03\uD875\uDD29\uD820\uDC07\uD86E\uDD67\u9108\uD83D\uDF65\uD803\uDC0F\uD82C\uDC4A\uDB40\uDD24\uD800\uDCED\u4F14\uD869\uDD2D\uAA12\uD842\uDE86\u1FD2\u98EE\u20A5\uD82C\uDCCB\uC926\uD850\uDD99\uD851\uDE9F\uD860\uDC07\uD868\uDE9D\uD451\u7282\uD86C\uDD47\uD870\uDF45\uDB40\uDD60\u4D27\u4D57\uD876\uDCA4\u8D1D\u6C1C\uD844\uDC66\uD86C\uDD06\u9E6B\u4AB3\uAE21\uD81A\uDF2A\u7CD7\uD854\uDEA6\u05C7\u664A\u4017\uD857\uDC2E\u7551\u0719\uA0F5\u8FD8\uBE6A\u4823\uD879\uDD9E\u16E5\uD862\uDCC5\uD844\uDED3\uA598\uD855\uDD14\uAE3E\uD873\uDFAC\uD865\uDFF0\uD850\uDC83\u9E5E\uD870\uDE74\uD59D\uD83E\uDC6D\u09DD\uD808\uDDD1\uD840\uDF63\uD870\uDF7E\uD875\uDF1A\uD877\uDFE1\uD876\uDF0F\uD802\uDC6B\uD864\uDC4C\uD854\uDFD7\uCF96\uCA8C\u761E\uD872\uDC89\u6F87\uD83A\uDCA9\u5858\uD822\uDCAF\uD821\uDEB6\uD840\uDEAB\uD821\uDC20\uD85D\uDECC\uD867\uDC59\u5F4F\uD84A\uDDB8\uD85D\uDD77\uD801\uDC01\uD86E\uDFFD\uD84E\uDCEC\uD854\uDD61\u30CA\u29B3\uD872\uDC7B\uC52F\uC7C7\u5ED1\uD863\uDF8B\uD855\uDF03\u1B73\uD86B\uDF49\uD862\uDF33\u208E\u1E85\u0F86\u7B67\uD855\uDD03\uD873\uDC8F\u1F8A\uD870\uDC93\u188E\uD859\uDDA4\uD864\uDFFA\u3AED\u1492\uD860\uDE7D\uAD2A\uD835\uDCC5\uD854\uDEA2\uD870\uDF26\u84FD\uAE7B\uD86F\uDC29\u8DA5\u98B0\uD86A\uDCA3\uD81E\uDE65\uCB51\uD85D\uDDD9\u880E\uD867\uDF74\uD873\uDD54\u9F37\uD81F\uDE39\uD845\uDD3D\uFD02\u8FFE\uD847\uDEF2\uD86D\uDE99\uD820\uDD79\uB0EB\uD809\uDCA3\uB6AB\uD805\uDE2B\uD857\uDF8E\uC1C2\uD847\uDE03\u7E09\uA6AB\uD3EE\u32FF\uD874\uDD13\uD835\uDEE1\uD85E\uDC13\uD835\uDD95\uD869\uDC00\u1084\u521E\u5723\u1936\uB338\uD841\uDF8D\u7F49\uD85D\uDC9B\uD877\uDFA8\uD86B\uDD54\uD87A\uDE9C\u6FA5\uD800\uDE94\uD84A\uDF2F\uD851\uDE47\u6BC1\u2713\uD852\uDC0C\uD6DB\uD86B\uDC7D\u812B\uD80C\uDE68\uD85C\uDF8C\uB313\u0F65\uD820\uDEBF\uD822\uDD80\u8C5C\u527D\uD865\uDEF9\u9B09\uD842\uDD96\uD86C\uDCDA\uAD6E\uD84C\uDEAE\uCE9F\uD874\uDC18\u4B30\uD853\uDF10\uD861\uDEA4\uD879\uDCBF\uD859\uDD75\uD86F\uDE14\uD81D\uDE26\u914E\uD820\uDF50\uD84B\uDEDC\uD85E\uDD76\uD856\uDD6B\uC144\uD843\uDE6E\uD83A\uDD2A\u4E93\u27CE\uD862\uDCF5\u54CF\uD879\uDFC8\uD808\uDE24\u3EE4\uFD94\uD86D\uDCA1\uD87E\uDCC7\u791A\u9DF7\u897C\uB85D\uD801\uDC9C\uD84A\uDD48\uD801\uDF20\u7ABC\uD83D\uDC99\uA9F0\u2366\u8E95\uD857\uDF9E\u07DF\uD872\uDD6D\uD854\uDCEF\uD848\uDF2A\u6E7D\uB99D\uD843\uDD20\u9275\uD867\uDE47\uD857\uDCE8\uD820\uDDAB\uC7BD\uFB95\uD81A\uDE50\u37A9\uD82C\uDCC3\u7DE6\u4E23\uD844\uDE6D\uD85C\uDC58\uD802\uDDCE\u8454\uD843\uDDFD\uD840\uDD2F\uD854\uDF77\uD853\uDFA9\uD876\uDF77\u103B\uBC9E\uD82C\uDECD\uD861\uDE96\uD853\uDF48\uD864\uDEAF\u0C30\uD85F\uDC52\uA195\u580F\uD873\uDC63\uD849\uDF9F\u1F9E\uD878\uDF45\u585D\u12FE\uD800\uDD19\uD860\uDC32\uD82C\uDCBB\u33C0\u4D7A\uD84D\uDF4B\u250F\uD857\uDD21\u2CA9\uD81C\uDD5B\uD877\uDD53\u2BA0\uC921\uD85D\uDF79\uD847\uDFBD\u9CA7\uD83C\uDF5B\uD868\uDF75\u3872\uD85E\uDEF2\u6847\uB526\uD849\uDCDC\uD869\uDC1D\u2EBC\uD876\uDEF2\uD868\uDDED\uB5E8\uD848\uDCAC\uD860\uDCF8\uBC8E\u6CAA\u2236\uD869\uDDF7\u21AC\uD801\uDE4B\uBFA5\uD877\uDDA9\uD851\uDDEE\uD85F\uDF33\uD848\uDC8F\uD864\uDCD6\uFF64\uD85E\uDE87\uD856\uDE04\u484C\u3170\uD843\uDDE8\u5A8D\u7EB1\u3F78\uD80C\uDD00\uD801\uDD56\uD857\uDF76\uD874\uDD2E\uD852\uDF28\uD86B\uDFA2\uD808\uDDB5\uB323\uA0C2\uD847\uDD6C\u5988\uD85A\uDF89\uD858\uDC2F\u0732\uD86F\uDFB5\u2623\uD854\uDF73\uD870\uDC50\u6324\u18C8\u8124\uD84B\uDF63\uD83C\uDF36\uD81E\uDE9D\uD86A\uDD71\uFCFB\uD873\uDDDD\uD857\uDF41\uD876\uDC93\u84E5\uD81F\uDFCD\uD869\uDEB3\uD852\uDF7C\uD867\uDDC2\uB0B3\uD81E\uDD10\uD86C\uDE43\uBB58\uD805\uDF38\uD86E\uDEDB\uD866\uDDE4\uBFF4\uD81A\uDCA3\uD858\uDCA2\u26DB\uD865\uDFAB\uAC56\uD835\uDF87\uD86D\uDC20\u8BE5\u9FC5\uD854\uDF98\uCF52\u8911\uD86D\uDD46\uD856\uDDB7\uA576\u0FB2\uD868\uDE23\u191C\uD85A\uDDAD\uD86A\uDEAC\uD849\uDE8D\uD875\uDEE4\u35F0\uD842\uDC92\uD800\uDD33\u2CC0\u23CD\u14E8\uD835\uDD10\u96A8\uD860\uDD4C\uD86C\uDFBD\uD861\uDC52\uD85E\uDEB0\uD81F\uDEC6\uD807\uDC3F\uD858\uDD7E\u24F2\uD863\uDEF7\uD867\uDFDD\u9508\uD859\uDCEC\uD873\uDFED\u39C0\u3251\uFFE0\uD86F\uDE34\uD808\uDC7A\uD85F\uDFD5\uD855\uDDCF\uBF31\uD85D\uDEA7\uD83C\uDC0E\uC629\uD841\uDF6E\uA64F\u863E\uD862\uDF02\uD86D\uDCA2\uD857\uDD2E\u145A\uD805\uDF20\u2018\uD877\uDE36\uD86F\uDC3A\u8CF9\uD86A\uDD51\uD875\uDF8B\uD855\uDCE8\uD3D9\uD83D\uDDF5\uD871\uDC32\u5CF2\uD877\uDEDC\u6567\uD84C\uDD44\u1041\uD867\uDD70\uD844\uDF9B\uD858\uDC9B\u1C1D\uD873\uDFB2\uD862\uDFD7\u7B05\uD863\uDE0A\u47AA\uD80C\uDDEE\u59F9\uD856\uDCAA\uD82C\uDEF9\u3B43\u87EC\uD855\uDDDC\u80A5\uD850\uDC09\u21D5\uD85A\uDFB8\u6E9C\uD86B\uDE2C\uD807\uDD36\uD879\uDE51\u9FA0\uD86E\uDE5C\uD81C\uDEFC\uD847\uDD71\u6440\u2B19\u0084\uA244\uD874\uDF6D\uC75E\uCE78\uD856\uDC24\uA9BD\uD844\uDE5E\uD835\uDCF0\uD83E\uDD64\u54EC\u5614\uD868\uDD4D\uD85C\uDFEB\uD855\uDCF0\uB35D\u86CC\u6C1C\uA2CF\uD86D\uDC1F\u428A\uD803\uDCD0\u3922\u7B1F\uD822\uDDE2\uD860\uDD61\uD811\uDD12\u8A8E\uD801\uDD48\u42FE\uD85A\uDF51\u80CA\uD859\uDF21\uD853\uDE0D\uD86C\uDDE9\uD800\uDE8D\uD853\uDEE8\uD874\uDD49\u763C\uD873\uDDD7\u1B0E\uD85E\uDE78\uD857\uDE23\u4CEE\uD855\uDF16\uB1A0\uCF36\uD801\uDC22\u8151\u166A\uD809\uDCF9\uD85B\uDF00\uD85A\uDF05\uD862\uDF74\uD81E\uDDFA\uC410\u3787\uD863\uDEDC\uD81E\uDEBD\u8A19\u8710\u10A8\uB715\uD806\uDE6D\uD874\uDDEC\uD849\uDC19\uBB7A\uC9D2\uD879\uDFBE\uD865\uDE0C\uD840\uDD35\u3343\u1F9B\uBD98\u7E47\uD841\uDC24\uD864\uDCFD\uD853\uDF20\u4F81\u1A24\u3D41\u3552\uD851\uDC40\uD857\uDD89\u5E21\uD874\uDFCD\uD841\uDDD2\u12CE\u8487\uD875\uDC7A\uD801\uDC62\u73A1\u4D24\u5163\uD836\uDDF1\u5D38\u081E\u24A2\u9EE0\uCC12\uD864\uDC1F\u9BBE\uD878\uDCEF\uD86C\uDCC9\uD842\uDCFB\u814F\uD853\uDDFB\uD861\uDDF9\uD85C\uDDA3\u651F\uD845\uDF52\u1BA2\uD84E\uDDF3\u3244\uD81D\uDFB7\uD84C\uDD28\uD842\uDC3A\uD854\uDF75\u35D2\uD84B\uDC7B\uD855\uDE64\u5384\uD863\uDCDC\uD81A\uDCA5\uD844\uDD91\uDB40\uDC2F\uD86F\uDC45\uD85D\uDFB3\u1B88\uD805\uDF23\uD84B\uDE5F\uD84A\uDFEA\uD84A\uDF9A\uD842\uDCA7\uD862\uDFDF\uD85C\uDD57\u56B6\uD84A\uDE3D\uD87A\uDCE6\u478F\uD877\uDE38\u6C60\uD849\uDE4F\u1D9F\u055A\u3741\uD81F\uDFD3\u69FE\uD84D\uDD18\uDB40\uDC7E\u8AB8\uD81E\uDEF3\u4EDF\uD877\uDD66\uD849\uDDD4\u7C7C\uD854\uDFF3\uD843\uDE55\u6EF4\uD835\uDD09\uD866\uDCA4\u63E2\uD86D\uDF5E\uD81E\uDEC3\uAB09\u529B\uD81B\uDF59\uD84A\uDD19\uD855\uDD04\uD86C\uDF2F\uD863\uDFE6\u783D\u937A\uD84E\uDF0F\uD877\uDCDE\uD845\uDF98\u55C6\uD81A\uDD3F\uD820\uDCAB\uD872\uDC11\uAD96\u5C38\uD338\uD858\uDDF8\uBC6D\uD84D\uDEC9\uD85C\uDF11\uD867\uDCB8\uD834\uDCEE\u306C\u1646\uD842\uDE09\u513B\uD865\uDDCA\u2F1C\uA25F\uD790\uD875\uDF9D\u6791\uD846\uDF46\uD872\uDDF8\uD39C\uD84F\uDE5F\uD83C\uDF32\uD83D\uDF04\u04BB\uD870\uDD56\uD86B\uDE4F\uC028\u9C00\uD81C\uDFE3\uD834\uDD54\uD81E\uDED9\u3A7F\u6D65\u9353\uD793\uD84A\uDD3B\u45FF\uD86A\uDEAB\u141B\uD820\uDF4A\uD857\uDFBB\uD872\uDE41\uD85F\uDC35\u15C3\uACBD\uD81D\uDC2E\uD870\uDC31\uD875\uDE0E\uD861\uDC33\uD871\uDCCD\uD865\uDEEF\uD83E\uDC37\uD1C4\uD846\uDCC1\uD87A\uDDCA\uD821\uDCF7\uD83C\uDC1C\uD81D\uDC4A\uD861\uDCC5\uD869\uDF13\uD840\uDD0E\u23EB\uD851\uDD70\uD847\uDF0E\u4013\uB0EB\u656E\uD865\uDDFD\uD841\uDFF8\u7FB7\u14DE\uD86A\uDD94\uD834\uDCE6\",\"_entity_type\":\"indexNameType\"}", + JsonObject.class + ) + ) + ) ); + + return params; + } + + private GsonHttpEntity gsonEntity; + private String expectedPayloadString; + private int expectedContentLength; + + public void init(List payload) throws IOException { + Gson gson = GsonProvider.create( GsonBuilder::new, true ).getGson(); + this.gsonEntity = new GsonHttpEntity( gson, payload ); + StringBuilder builder = new StringBuilder(); + for ( JsonObject object : payload ) { + gson.toJson( object, builder ); + builder.append( "\n" ); + } + this.expectedPayloadString = builder.toString(); + this.expectedContentLength = StandardCharsets.UTF_8.encode( expectedPayloadString ).limit(); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("params") + void initialContentLength(String ignoredLabel, List payload) throws IOException { + init( payload ); + // The content length cannot be known from the start for large, multi-object payloads + assumeTrue( payload.size() <= 1 || expectedContentLength < 1024 ); + + assertThat( gsonEntity.getContentLength() ).isEqualTo( expectedContentLength ); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("params") + void contentType(String ignoredLabel, List payload) throws IOException { + init( payload ); + String contentType = gsonEntity.getContentType(); + assertThat( contentType ).isEqualTo( "application/json; charset=UTF-8" ); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("params") + void produceContent_noPushBack(String ignoredLabel, List payload) throws IOException { + init( payload ); + int pushBackPeriod = Integer.MAX_VALUE; + for ( int i = 0; i < 2; i++ ) { // Try several times: the result shouldn't change. + assertThat( doProduceContent( gsonEntity, pushBackPeriod ) ) + .isEqualTo( expectedPayloadString ); + assertThat( gsonEntity.getContentLength() ) + .isEqualTo( expectedContentLength ); + } + } + + @ParameterizedTest(name = "{0}") + @MethodSource("params") + void produceContent_pushBack_every5Bytes(String ignoredLabel, List payload) throws IOException { + init( payload ); + int pushBackPeriod = 5; + for ( int i = 0; i < 2; i++ ) { // Try several times: the result shouldn't change. + assertThat( doProduceContent( gsonEntity, pushBackPeriod ) ) + .isEqualTo( expectedPayloadString ); + assertThat( gsonEntity.getContentLength() ) + .isEqualTo( expectedContentLength ); + } + } + + @ParameterizedTest(name = "{0}") + @MethodSource("params") + void produceContent_pushBack_every100Bytes(String ignoredLabel, List payload) + throws IOException { + init( payload ); + int pushBackPeriod = 100; + for ( int i = 0; i < 2; i++ ) { // Try several times: the result shouldn't change. + assertThat( doProduceContent( gsonEntity, pushBackPeriod ) ) + .isEqualTo( expectedPayloadString ); + assertThat( gsonEntity.getContentLength() ) + .isEqualTo( expectedContentLength ); + } + } + + @ParameterizedTest(name = "{0}") + @MethodSource("params") + void produceContent_pushBack_every500Bytes(String ignoredLabel, List payload) + throws IOException { + init( payload ); + int pushBackPeriod = 500; + for ( int i = 0; i < 2; i++ ) { // Try several times: the result shouldn't change. + assertThat( doProduceContent( gsonEntity, pushBackPeriod ) ) + .isEqualTo( expectedPayloadString ); + assertThat( gsonEntity.getContentLength() ) + .isEqualTo( expectedContentLength ); + } + } + + @ParameterizedTest(name = "{0}") + @MethodSource("params") + void writeTo(String ignoredLabel, List payload) throws IOException { + init( payload ); + for ( int i = 0; i < 2; i++ ) { // Try several times: the result shouldn't change. + assertThat( doWriteTo( gsonEntity ) ) + .isEqualTo( expectedPayloadString ); + assertThat( gsonEntity.getContentLength() ) + .isEqualTo( expectedContentLength ); + } + } + + @ParameterizedTest(name = "{0}") + @MethodSource("params") + void getContent(String ignoredLabel, List payload) throws IOException { + init( payload ); + for ( int i = 0; i < 2; i++ ) { // Try several times: the result shouldn't change. + assertThat( doGetContent( gsonEntity ) ) + .isEqualTo( expectedPayloadString ); + assertThat( gsonEntity.getContentLength() ) + .isEqualTo( expectedContentLength ); + } + } + + private String doProduceContent(GsonHttpEntity entity, int pushBackPeriod) throws IOException { + try ( ByteArrayOutputStream outputStream = new ByteArrayOutputStream() ) { + OutputStreamContentEncoder contentEncoder = new OutputStreamContentEncoder( outputStream, pushBackPeriod ); + while ( !contentEncoder.isCompleted() ) { + entity.produce( contentEncoder ); + } + return outputStream.toString( StandardCharsets.UTF_8.name() ); + } + finally { + entity.close(); + } + } + + private String doWriteTo(GsonHttpEntity entity) throws IOException { + try ( ByteArrayOutputStream outputStream = new ByteArrayOutputStream() ) { + entity.writeTo( outputStream ); + return outputStream.toString( StandardCharsets.UTF_8.name() ); + } + } + + private String doGetContent(GsonHttpEntity entity) throws IOException { + try ( InputStream inputStream = entity.getContent(); + Reader reader = new InputStreamReader( inputStream, StandardCharsets.UTF_8 ); + BufferedReader bufferedReader = new BufferedReader( reader ) ) { + StringBuilder builder = new StringBuilder(); + int read; + while ( ( read = bufferedReader.read() ) >= 0 ) { + builder.appendCodePoint( read ); + } + return builder.toString(); + } + } + + private static class OutputStreamContentEncoder implements ContentEncoder, DataStreamChannel { + private boolean complete = false; + private final OutputStream outputStream; + private final int pushBackPeriod; + + private int written = 0; + private boolean pushedBack = false; + + private OutputStreamContentEncoder(OutputStream outputStream, int pushBackPeriod) { + this.outputStream = outputStream; + this.pushBackPeriod = pushBackPeriod; + } + + @Override + public void requestOutput() { + + } + + @Override + public int write(ByteBuffer src) throws IOException { + int toWrite = src.remaining(); + if ( !pushedBack && ( written % pushBackPeriod ) != ( ( written + toWrite ) % pushBackPeriod ) ) { + // push back + pushedBack = true; + return 0; + } + pushedBack = false; + outputStream.write( src.array(), src.arrayOffset(), toWrite ); + written += toWrite; + return toWrite; + } + + @Override + public void endStream() throws IOException { + complete = true; + } + + @Override + public void endStream(List trailers) throws IOException { + complete = true; + } + + @Override + public void complete(List trailers) { + this.complete = true; + } + + @Override + public boolean isCompleted() { + return this.complete; + } + } +} diff --git a/backend/elasticsearch-client/elasticsearch-rest-client/pom.xml b/backend/elasticsearch-client/elasticsearch-rest-client/pom.xml new file mode 100644 index 00000000000..5fbdd1bbf6f --- /dev/null +++ b/backend/elasticsearch-client/elasticsearch-rest-client/pom.xml @@ -0,0 +1,100 @@ + + + + 4.0.0 + + org.hibernate.search + hibernate-search-parent-public + 8.2.0-SNAPSHOT + ../../../build/parents/public + + hibernate-search-backend-elasticsearch-client-rest + + Hibernate Search Backend - Elasticsearch client based on the low-level Elasticsearch client + Hibernate Search Elasticsearch client based on the low-level Elasticsearch client + + + + false + org.hibernate.search.backend.elasticsearch.client.rest + + + + + org.hibernate.search + hibernate-search-engine + + + org.hibernate.search + hibernate-search-backend-elasticsearch-client-common + + + org.elasticsearch.client + elasticsearch-rest-client + + + org.elasticsearch.client + elasticsearch-rest-client-sniffer + + + org.apache.httpcomponents + httpclient + 4.5.14 + + + org.apache.httpcomponents + httpcore + 4.4.16 + + + org.apache.httpcomponents + httpcore-nio + 4.4.16 + + + org.apache.httpcomponents + httpasyncclient + 4.1.5 + + + org.jboss.logging + jboss-logging + + + org.jboss.logging + jboss-logging-annotations + + + com.google.code.gson + gson + + + + + com.fasterxml.jackson.core + jackson-core + + + + + org.hibernate.search + hibernate-search-util-internal-test-common + test + + + + + + + org.moditect + moditect-maven-plugin + + + + diff --git a/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/ElasticsearchHttpClientConfigurationContext.java b/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/ElasticsearchHttpClientConfigurationContext.java new file mode 100644 index 00000000000..765138a6ee9 --- /dev/null +++ b/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/ElasticsearchHttpClientConfigurationContext.java @@ -0,0 +1,41 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.rest; + + +import org.hibernate.search.engine.cfg.ConfigurationPropertySource; +import org.hibernate.search.engine.environment.bean.BeanResolver; +import org.hibernate.search.util.common.annotation.impl.SuppressJQAssistant; + +import org.apache.http.impl.nio.client.HttpAsyncClientBuilder; + +/** + * The context passed to {@link ElasticsearchHttpClientConfigurer}. + */ +@SuppressJQAssistant( + reason = "Apache HTTP Client 5 uses a lot of classes/interfaces in the impl packages to create builders/instances etc. " + + "So while it is bad to expose impl types ... in this case it's what Apache Client expects users to do?") +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 An Apache HTTP client builder, to set the configuration. + * @see the Apache HTTP Client documentation + */ + HttpAsyncClientBuilder clientBuilder(); + +} diff --git a/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/ElasticsearchHttpClientConfigurer.java b/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/ElasticsearchHttpClientConfigurer.java new file mode 100644 index 00000000000..46fcda3c987 --- /dev/null +++ b/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/ElasticsearchHttpClientConfigurer.java @@ -0,0 +1,37 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.rest; + +/** + * An extension point allowing fine tuning of the Apache 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. + */ +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 Apache HTTP client builder + * and configuration properties in particular. + */ + void configure(ElasticsearchHttpClientConfigurationContext context); + +} diff --git a/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/cfg/ClientRestElasticsearchBackendClientSettings.java b/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/cfg/ClientRestElasticsearchBackendClientSettings.java new file mode 100644 index 00000000000..12ac28cb2de --- /dev/null +++ b/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/cfg/ClientRestElasticsearchBackendClientSettings.java @@ -0,0 +1,138 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.rest.cfg; + +import org.hibernate.search.backend.elasticsearch.client.common.cfg.ElasticsearchBackendClientCommonSettings; +import org.hibernate.search.backend.elasticsearch.client.rest.ElasticsearchHttpClientConfigurer; + +/** + * 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 + */ +public final class ClientRestElasticsearchBackendClientSettings { + + private ClientRestElasticsearchBackendClientSettings() { + } + + /** + * 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 reading responses from an Elasticsearch server. + *

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

+ * Defaults to {@link Defaults#READ_TIMEOUT}. + */ + public static final String READ_TIMEOUT = "read_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"; + + /** + * The maximum number of simultaneous connections to the Elasticsearch cluster, + * all hosts taken together. + *

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

+ * Defaults to {@link Defaults#MAX_CONNECTIONS}. + */ + public static final String MAX_CONNECTIONS = "max_connections"; + + /** + * The maximum number of simultaneous connections to each host of the Elasticsearch cluster. + *

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

+ * Defaults to {@link Defaults#MAX_CONNECTIONS_PER_ROUTE}. + */ + public static final String MAX_CONNECTIONS_PER_ROUTE = "max_connections_per_route"; + + /** + * Whether automatic discovery of nodes in the Elasticsearch cluster is enabled. + *

+ * Expects a Boolean value such as {@code true} or {@code false}, + * or a string that can be parsed into a Boolean value. + *

+ * Defaults to {@link Defaults#DISCOVERY_ENABLED}. + */ + public static final String DISCOVERY_ENABLED = "discovery.enabled"; + + /** + * The time interval between two executions of the automatic discovery, if enabled. + *

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

+ * Defaults to {@link Defaults#DISCOVERY_REFRESH_INTERVAL}. + */ + public static final String DISCOVERY_REFRESH_INTERVAL = "discovery.refresh_interval"; + + /** + * How long connections to the Elasticsearch cluster can be kept idle. + *

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

+ * If the response from an Elasticsearch cluster contains a {@code Keep-Alive} header, + * then the effective max idle time will be whichever is lower: + * the duration from the {@code Keep-Alive} header or the value of this property (if set). + *

+ * If this property is not set, only the {@code Keep-Alive} header is considered, + * and if it's absent, idle connections will be kept forever. + */ + public static final String MAX_KEEP_ALIVE = "max_keep_alive"; + + /** + * 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 ElasticsearchBackendClientCommonSettings#USERNAME} or {@link #MAX_CONNECTIONS_PER_ROUTE}. + *

+ * 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 READ_TIMEOUT = 30000; + public static final int CONNECTION_TIMEOUT = 1000; + public static final int MAX_CONNECTIONS = 40; + public static final int MAX_CONNECTIONS_PER_ROUTE = 20; + public static final boolean DISCOVERY_ENABLED = false; + public static final int DISCOVERY_REFRESH_INTERVAL = 10; + } +} diff --git a/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/cfg/spi/ClientRestElasticsearchBackendClientSpiSettings.java b/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/cfg/spi/ClientRestElasticsearchBackendClientSpiSettings.java new file mode 100644 index 00000000000..7f1b9ba2adc --- /dev/null +++ b/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/cfg/spi/ClientRestElasticsearchBackendClientSpiSettings.java @@ -0,0 +1,55 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.rest.cfg.spi; + +import org.hibernate.search.engine.cfg.EngineSettings; + +/** + * Configuration properties for the Elasticsearch backend that are considered SPI (and not API). + */ +public final class ClientRestElasticsearchBackendClientSpiSettings { + + /** + * The prefix expected for the key of every Hibernate Search configuration property. + */ + public static final String PREFIX = EngineSettings.PREFIX + "backend."; + + /** + * An external Elasticsearch 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 Elasticsearch, + * and all other client-related configuration properties + * (hosts/uris, authentication, discovery, timeouts, max connections, configurer, ...) + * will be ignored. + *

+ * Expects a reference to a bean of type {@link org.elasticsearch.client.RestClient}. + *

+ * 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 ClientRestElasticsearchBackendClientSpiSettings() { + } + + /** + * 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/client/impl/ByteBufferContentEncoder.java b/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/impl/ByteBufferContentEncoder.java similarity index 92% rename from backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/ByteBufferContentEncoder.java rename to backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/impl/ByteBufferContentEncoder.java index fe6c7346d0b..ca146cae5b7 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/ByteBufferContentEncoder.java +++ b/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/impl/ByteBufferContentEncoder.java @@ -2,7 +2,7 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright Red Hat Inc. and Hibernate Authors */ -package org.hibernate.search.backend.elasticsearch.client.impl; +package org.hibernate.search.backend.elasticsearch.client.rest.impl; import java.nio.ByteBuffer; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/ElasticsearchClientImpl.java b/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/impl/ClientRestElasticsearchClient.java similarity index 88% rename from backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/ElasticsearchClientImpl.java rename to backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/impl/ClientRestElasticsearchClient.java index 9d96fe1ee49..e8a564b0444 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/ElasticsearchClientImpl.java +++ b/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/impl/ClientRestElasticsearchClient.java @@ -2,7 +2,7 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright Red Hat Inc. and Hibernate Authors */ -package org.hibernate.search.backend.elasticsearch.client.impl; +package org.hibernate.search.backend.elasticsearch.client.rest.impl; import java.io.IOException; import java.io.InputStream; @@ -17,13 +17,13 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchClientImplementor; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchRequest; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchResponse; -import org.hibernate.search.backend.elasticsearch.gson.spi.JsonLogHelper; -import org.hibernate.search.backend.elasticsearch.logging.impl.ElasticsearchClientLog; -import org.hibernate.search.backend.elasticsearch.logging.impl.ElasticsearchMiscLog; -import org.hibernate.search.backend.elasticsearch.logging.impl.ElasticsearchRequestLog; +import org.hibernate.search.backend.elasticsearch.client.common.gson.spi.JsonLogHelper; +import org.hibernate.search.backend.elasticsearch.client.common.logging.spi.ElasticsearchClientLog; +import org.hibernate.search.backend.elasticsearch.client.common.logging.spi.ElasticsearchRequestLog; +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.engine.common.execution.spi.SimpleScheduledExecutor; import org.hibernate.search.engine.common.timing.Deadline; import org.hibernate.search.engine.environment.bean.BeanHolder; @@ -44,7 +44,7 @@ import org.elasticsearch.client.RestClient; import org.elasticsearch.client.sniff.Sniffer; -public class ElasticsearchClientImpl implements ElasticsearchClientImplementor { +public class ClientRestElasticsearchClient implements ElasticsearchClientImplementor { private final BeanHolder restClientHolder; @@ -58,7 +58,7 @@ public class ElasticsearchClientImpl implements ElasticsearchClientImplementor { private final Gson gson; private final JsonLogHelper jsonLogHelper; - ElasticsearchClientImpl(BeanHolder restClientHolder, Sniffer sniffer, + ClientRestElasticsearchClient(BeanHolder restClientHolder, Sniffer sniffer, SimpleScheduledExecutor timeoutExecutorService, Optional requestTimeoutMs, int connectionTimeoutMs, Gson gson, JsonLogHelper jsonLogHelper) { @@ -88,7 +88,7 @@ public T unwrap(Class clientClass) { if ( RestClient.class.isAssignableFrom( clientClass ) ) { return (T) restClientHolder.get(); } - throw ElasticsearchMiscLog.INSTANCE.clientUnwrappingWithUnknownType( clientClass, RestClient.class ); + throw ElasticsearchClientLog.INSTANCE.clientUnwrappingWithUnknownType( clientClass, RestClient.class ); } private CompletableFuture send(ElasticsearchRequest elasticsearchRequest) { @@ -96,7 +96,7 @@ private CompletableFuture send(ElasticsearchRequest elasticsearchReque HttpEntity entity; try { - entity = ElasticsearchClientUtils.toEntity( gson, elasticsearchRequest ); + entity = GsonHttpEntity.toEntity( gson, elasticsearchRequest ); } catch (IOException | RuntimeException e) { completableFuture.completeExceptionally( e ); @@ -199,7 +199,7 @@ private ElasticsearchResponse convertResponse(Response response) { try { JsonObject body = parseBody( response ); return new ElasticsearchResponse( - response.getHost(), + response.getHost().toHostString(), response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase(), body ); diff --git a/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/impl/ClientRestElasticsearchClientBeanConfigurer.java b/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/impl/ClientRestElasticsearchClientBeanConfigurer.java new file mode 100644 index 00000000000..71662c41bc8 --- /dev/null +++ b/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/impl/ClientRestElasticsearchClientBeanConfigurer.java @@ -0,0 +1,20 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.rest.impl; + +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchClientFactory; +import org.hibernate.search.engine.environment.bean.BeanHolder; +import org.hibernate.search.engine.environment.bean.spi.BeanConfigurationContext; +import org.hibernate.search.engine.environment.bean.spi.BeanConfigurer; + +public class ClientRestElasticsearchClientBeanConfigurer implements BeanConfigurer { + @Override + public void configure(BeanConfigurationContext context) { + context.define( + ElasticsearchClientFactory.class, ElasticsearchClientFactory.DEFAULT_BEAN_NAME, + beanResolver -> BeanHolder.of( new ClientRestElasticsearchClientFactory() ) + ); + } +} diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/ElasticsearchClientFactoryImpl.java b/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/impl/ClientRestElasticsearchClientFactory.java similarity index 66% rename from backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/ElasticsearchClientFactoryImpl.java rename to backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/impl/ClientRestElasticsearchClientFactory.java index a48a5ee3c06..769bf2126fa 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/ElasticsearchClientFactoryImpl.java +++ b/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/impl/ClientRestElasticsearchClientFactory.java @@ -2,19 +2,21 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright Red Hat Inc. and Hibernate Authors */ -package org.hibernate.search.backend.elasticsearch.client.impl; +package org.hibernate.search.backend.elasticsearch.client.rest.impl; import java.util.List; import java.util.Optional; -import org.hibernate.search.backend.elasticsearch.ElasticsearchVersion; -import org.hibernate.search.backend.elasticsearch.cfg.ElasticsearchBackendSettings; -import org.hibernate.search.backend.elasticsearch.cfg.spi.ElasticsearchBackendSpiSettings; -import org.hibernate.search.backend.elasticsearch.client.ElasticsearchHttpClientConfigurer; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchClientFactory; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchClientImplementor; -import org.hibernate.search.backend.elasticsearch.gson.spi.GsonProvider; -import org.hibernate.search.backend.elasticsearch.logging.impl.ElasticsearchClientLog; +import org.hibernate.search.backend.elasticsearch.client.common.cfg.ElasticsearchBackendClientCommonSettings; +import org.hibernate.search.backend.elasticsearch.client.common.gson.spi.GsonProvider; +import org.hibernate.search.backend.elasticsearch.client.common.logging.spi.ElasticsearchClientConfigurationLog; +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.rest.ElasticsearchHttpClientConfigurer; +import org.hibernate.search.backend.elasticsearch.client.rest.cfg.ClientRestElasticsearchBackendClientSettings; +import org.hibernate.search.backend.elasticsearch.client.rest.cfg.spi.ClientRestElasticsearchBackendClientSpiSettings; import org.hibernate.search.engine.cfg.ConfigurationPropertySource; import org.hibernate.search.engine.cfg.spi.ConfigurationProperty; import org.hibernate.search.engine.cfg.spi.OptionalConfigurationProperty; @@ -41,93 +43,93 @@ /** * @author Gunnar Morling */ -public class ElasticsearchClientFactoryImpl implements ElasticsearchClientFactory { +public class ClientRestElasticsearchClientFactory implements ElasticsearchClientFactory { private static final OptionalConfigurationProperty> CLIENT_INSTANCE = - ConfigurationProperty.forKey( ElasticsearchBackendSpiSettings.CLIENT_INSTANCE ) + ConfigurationProperty.forKey( ClientRestElasticsearchBackendClientSpiSettings.CLIENT_INSTANCE ) .asBeanReference( RestClient.class ) .build(); private static final OptionalConfigurationProperty> HOSTS = - ConfigurationProperty.forKey( ElasticsearchBackendSettings.HOSTS ) + ConfigurationProperty.forKey( ElasticsearchBackendClientCommonSettings.HOSTS ) .asString().multivalued() .build(); private static final OptionalConfigurationProperty PROTOCOL = - ConfigurationProperty.forKey( ElasticsearchBackendSettings.PROTOCOL ) + ConfigurationProperty.forKey( ElasticsearchBackendClientCommonSettings.PROTOCOL ) .asString() .build(); private static final OptionalConfigurationProperty> URIS = - ConfigurationProperty.forKey( ElasticsearchBackendSettings.URIS ) + ConfigurationProperty.forKey( ElasticsearchBackendClientCommonSettings.URIS ) .asString().multivalued() .build(); private static final ConfigurationProperty PATH_PREFIX = - ConfigurationProperty.forKey( ElasticsearchBackendSettings.PATH_PREFIX ) + ConfigurationProperty.forKey( ElasticsearchBackendClientCommonSettings.PATH_PREFIX ) .asString() - .withDefault( ElasticsearchBackendSettings.Defaults.PATH_PREFIX ) + .withDefault( ElasticsearchBackendClientCommonSettings.Defaults.PATH_PREFIX ) .build(); private static final OptionalConfigurationProperty USERNAME = - ConfigurationProperty.forKey( ElasticsearchBackendSettings.USERNAME ) + ConfigurationProperty.forKey( ElasticsearchBackendClientCommonSettings.USERNAME ) .asString() .build(); private static final OptionalConfigurationProperty PASSWORD = - ConfigurationProperty.forKey( ElasticsearchBackendSettings.PASSWORD ) + ConfigurationProperty.forKey( ElasticsearchBackendClientCommonSettings.PASSWORD ) .asString() .build(); private static final OptionalConfigurationProperty REQUEST_TIMEOUT = - ConfigurationProperty.forKey( ElasticsearchBackendSettings.REQUEST_TIMEOUT ) + ConfigurationProperty.forKey( ClientRestElasticsearchBackendClientSettings.REQUEST_TIMEOUT ) .asIntegerStrictlyPositive() .build(); private static final ConfigurationProperty READ_TIMEOUT = - ConfigurationProperty.forKey( ElasticsearchBackendSettings.READ_TIMEOUT ) + ConfigurationProperty.forKey( ClientRestElasticsearchBackendClientSettings.READ_TIMEOUT ) .asIntegerPositiveOrZeroOrNegative() - .withDefault( ElasticsearchBackendSettings.Defaults.READ_TIMEOUT ) + .withDefault( ClientRestElasticsearchBackendClientSettings.Defaults.READ_TIMEOUT ) .build(); private static final ConfigurationProperty CONNECTION_TIMEOUT = - ConfigurationProperty.forKey( ElasticsearchBackendSettings.CONNECTION_TIMEOUT ) + ConfigurationProperty.forKey( ClientRestElasticsearchBackendClientSettings.CONNECTION_TIMEOUT ) .asIntegerPositiveOrZeroOrNegative() - .withDefault( ElasticsearchBackendSettings.Defaults.CONNECTION_TIMEOUT ) + .withDefault( ClientRestElasticsearchBackendClientSettings.Defaults.CONNECTION_TIMEOUT ) .build(); private static final ConfigurationProperty MAX_TOTAL_CONNECTION = - ConfigurationProperty.forKey( ElasticsearchBackendSettings.MAX_CONNECTIONS ) + ConfigurationProperty.forKey( ClientRestElasticsearchBackendClientSettings.MAX_CONNECTIONS ) .asIntegerStrictlyPositive() - .withDefault( ElasticsearchBackendSettings.Defaults.MAX_CONNECTIONS ) + .withDefault( ClientRestElasticsearchBackendClientSettings.Defaults.MAX_CONNECTIONS ) .build(); private static final ConfigurationProperty MAX_TOTAL_CONNECTION_PER_ROUTE = - ConfigurationProperty.forKey( ElasticsearchBackendSettings.MAX_CONNECTIONS_PER_ROUTE ) + ConfigurationProperty.forKey( ClientRestElasticsearchBackendClientSettings.MAX_CONNECTIONS_PER_ROUTE ) .asIntegerStrictlyPositive() - .withDefault( ElasticsearchBackendSettings.Defaults.MAX_CONNECTIONS_PER_ROUTE ) + .withDefault( ClientRestElasticsearchBackendClientSettings.Defaults.MAX_CONNECTIONS_PER_ROUTE ) .build(); private static final ConfigurationProperty DISCOVERY_ENABLED = - ConfigurationProperty.forKey( ElasticsearchBackendSettings.DISCOVERY_ENABLED ) + ConfigurationProperty.forKey( ClientRestElasticsearchBackendClientSettings.DISCOVERY_ENABLED ) .asBoolean() - .withDefault( ElasticsearchBackendSettings.Defaults.DISCOVERY_ENABLED ) + .withDefault( ClientRestElasticsearchBackendClientSettings.Defaults.DISCOVERY_ENABLED ) .build(); private static final ConfigurationProperty DISCOVERY_REFRESH_INTERVAL = - ConfigurationProperty.forKey( ElasticsearchBackendSettings.DISCOVERY_REFRESH_INTERVAL ) + ConfigurationProperty.forKey( ClientRestElasticsearchBackendClientSettings.DISCOVERY_REFRESH_INTERVAL ) .asIntegerStrictlyPositive() - .withDefault( ElasticsearchBackendSettings.Defaults.DISCOVERY_REFRESH_INTERVAL ) + .withDefault( ClientRestElasticsearchBackendClientSettings.Defaults.DISCOVERY_REFRESH_INTERVAL ) .build(); private static final OptionalConfigurationProperty< BeanReference> CLIENT_CONFIGURER = - ConfigurationProperty.forKey( ElasticsearchBackendSettings.CLIENT_CONFIGURER ) + ConfigurationProperty.forKey( ClientRestElasticsearchBackendClientSettings.CLIENT_CONFIGURER ) .asBeanReference( ElasticsearchHttpClientConfigurer.class ) .build(); private static final OptionalConfigurationProperty MAX_KEEP_ALIVE = - ConfigurationProperty.forKey( ElasticsearchBackendSettings.MAX_KEEP_ALIVE ) + ConfigurationProperty.forKey( ClientRestElasticsearchBackendClientSettings.MAX_KEEP_ALIVE ) .asLongStrictlyPositive() .build(); @@ -135,8 +137,7 @@ public class ElasticsearchClientFactoryImpl implements ElasticsearchClientFactor public ElasticsearchClientImplementor create(BeanResolver beanResolver, ConfigurationPropertySource propertySource, ThreadProvider threadProvider, String threadNamePrefix, SimpleScheduledExecutor timeoutExecutorService, - GsonProvider gsonProvider, - Optional configuredVersion) { + GsonProvider gsonProvider) { Optional requestTimeoutMs = REQUEST_TIMEOUT.get( propertySource ); int connectionTimeoutMs = CONNECTION_TIMEOUT.get( propertySource ); @@ -153,11 +154,11 @@ public ElasticsearchClientImplementor create(BeanResolver beanResolver, Configur ServerUris hosts = ServerUris.fromOptionalStrings( PROTOCOL.get( propertySource ), HOSTS.get( propertySource ), URIS.get( propertySource ) ); restClientHolder = createClient( beanResolver, propertySource, threadProvider, threadNamePrefix, - configuredVersion, hosts, PATH_PREFIX.get( propertySource ) ); + hosts, PATH_PREFIX.get( propertySource ) ); sniffer = createSniffer( propertySource, restClientHolder.get(), hosts ); } - return new ElasticsearchClientImpl( + return new ClientRestElasticsearchClient( restClientHolder, sniffer, timeoutExecutorService, requestTimeoutMs, connectionTimeoutMs, gsonProvider.getGson(), gsonProvider.getLogHelper() @@ -166,7 +167,6 @@ public ElasticsearchClientImplementor create(BeanResolver beanResolver, Configur private BeanHolder createClient(BeanResolver beanResolver, ConfigurationPropertySource propertySource, ThreadProvider threadProvider, String threadNamePrefix, - Optional configuredVersion, ServerUris hosts, String pathPrefix) { RestClientBuilder builder = RestClient.builder( hosts.asHostsArray() ); if ( !pathPrefix.isEmpty() ) { @@ -179,8 +179,13 @@ private BeanHolder createClient(BeanResolver beanResolver, RestClient client = null; List> httpClientConfigurerReferences = beanResolver.allConfiguredForRole( ElasticsearchHttpClientConfigurer.class ); + List> requestInterceptorProviderReferences = + beanResolver.allConfiguredForRole( ElasticsearchRequestInterceptorProvider.class ); + try ( BeanHolder> httpClientConfigurersHolder = - beanResolver.resolve( httpClientConfigurerReferences ) ) { + beanResolver.resolve( httpClientConfigurerReferences ); + BeanHolder> requestInterceptorProvidersHodler = + beanResolver.resolve( requestInterceptorProviderReferences ) ) { client = builder .setRequestConfigCallback( b -> customizeRequestConfig( b, propertySource ) ) .setHttpClientConfigCallback( @@ -188,8 +193,9 @@ private BeanHolder createClient(BeanResolver beanResolver, b, beanResolver, propertySource, threadProvider, threadNamePrefix, - configuredVersion, hosts, - httpClientConfigurersHolder.get(), customConfig + hosts, + httpClientConfigurersHolder.get(), requestInterceptorProvidersHodler.get(), + customConfig ) ) .build(); @@ -236,8 +242,8 @@ private Sniffer createSniffer(ConfigurationPropertySource propertySource, private HttpAsyncClientBuilder customizeHttpClientConfig(HttpAsyncClientBuilder builder, BeanResolver beanResolver, ConfigurationPropertySource propertySource, ThreadProvider threadProvider, String threadNamePrefix, - Optional configuredVersion, ServerUris hosts, Iterable configurers, + Iterable requestInterceptorProviders, Optional> customConfig) { builder.setMaxConnTotal( MAX_TOTAL_CONNECTION.get( propertySource ) ) .setMaxConnPerRoute( MAX_TOTAL_CONNECTION_PER_ROUTE.get( propertySource ) ) @@ -252,7 +258,7 @@ private HttpAsyncClientBuilder customizeHttpClientConfig(HttpAsyncClientBuilder if ( username.isPresent() ) { Optional password = PASSWORD.get( propertySource ); if ( password.isPresent() && !hosts.isSslEnabled() ) { - ElasticsearchClientLog.INSTANCE.usingPasswordOverHttp(); + ElasticsearchClientConfigurationLog.INSTANCE.usingPasswordOverHttp(); } BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider(); @@ -269,12 +275,19 @@ private HttpAsyncClientBuilder customizeHttpClientConfig(HttpAsyncClientBuilder builder.setKeepAliveStrategy( new CustomConnectionKeepAliveStrategy( maxKeepAlive.get() ) ); } - ElasticsearchHttpClientConfigurationContextImpl clientConfigurationContext = - new ElasticsearchHttpClientConfigurationContextImpl( beanResolver, propertySource, builder, configuredVersion ); + ClientRestElasticsearchHttpClientConfigurationContext clientConfigurationContext = + new ClientRestElasticsearchHttpClientConfigurationContext( beanResolver, propertySource, builder ); for ( ElasticsearchHttpClientConfigurer configurer : configurers ) { configurer.configure( clientConfigurationContext ); } + for ( ElasticsearchRequestInterceptorProvider interceptorProvider : requestInterceptorProviders ) { + Optional requestInterceptor = + interceptorProvider.provide( clientConfigurationContext ); + if ( requestInterceptor.isPresent() ) { + builder.addInterceptorLast( new ClientRestHttpRequestInterceptor( requestInterceptor.get() ) ); + } + } if ( customConfig.isPresent() ) { BeanHolder customConfigBeanHolder = customConfig.get(); customConfigBeanHolder.get().configure( clientConfigurationContext ); diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/ElasticsearchHttpClientConfigurationContextImpl.java b/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/impl/ClientRestElasticsearchHttpClientConfigurationContext.java similarity index 56% rename from backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/ElasticsearchHttpClientConfigurationContextImpl.java rename to backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/impl/ClientRestElasticsearchHttpClientConfigurationContext.java index 42a325a71eb..f5ca2c67587 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/ElasticsearchHttpClientConfigurationContextImpl.java +++ b/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/impl/ClientRestElasticsearchHttpClientConfigurationContext.java @@ -2,33 +2,28 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright Red Hat Inc. and Hibernate Authors */ -package org.hibernate.search.backend.elasticsearch.client.impl; +package org.hibernate.search.backend.elasticsearch.client.rest.impl; -import java.util.Optional; - -import org.hibernate.search.backend.elasticsearch.ElasticsearchVersion; -import org.hibernate.search.backend.elasticsearch.client.ElasticsearchHttpClientConfigurationContext; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchRequestInterceptorProvider; +import org.hibernate.search.backend.elasticsearch.client.rest.ElasticsearchHttpClientConfigurationContext; import org.hibernate.search.engine.cfg.ConfigurationPropertySource; import org.hibernate.search.engine.environment.bean.BeanResolver; import org.apache.http.impl.nio.client.HttpAsyncClientBuilder; -final class ElasticsearchHttpClientConfigurationContextImpl - implements ElasticsearchHttpClientConfigurationContext { +final class ClientRestElasticsearchHttpClientConfigurationContext + implements ElasticsearchHttpClientConfigurationContext, ElasticsearchRequestInterceptorProvider.Context { private final BeanResolver beanResolver; private final ConfigurationPropertySource configurationPropertySource; private final HttpAsyncClientBuilder clientBuilder; - private final Optional configuredVersion; - ElasticsearchHttpClientConfigurationContextImpl( + ClientRestElasticsearchHttpClientConfigurationContext( BeanResolver beanResolver, ConfigurationPropertySource configurationPropertySource, - HttpAsyncClientBuilder clientBuilder, - Optional configuredVersion) { + HttpAsyncClientBuilder clientBuilder) { this.beanResolver = beanResolver; this.configurationPropertySource = configurationPropertySource; this.clientBuilder = clientBuilder; - this.configuredVersion = configuredVersion; } @Override @@ -46,9 +41,4 @@ public HttpAsyncClientBuilder clientBuilder() { return clientBuilder; } - @Override - public Optional configuredVersion() { - return configuredVersion; - } - } diff --git a/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/impl/ClientRestHttpRequestInterceptor.java b/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/impl/ClientRestHttpRequestInterceptor.java new file mode 100644 index 00000000000..1ade535b204 --- /dev/null +++ b/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/impl/ClientRestHttpRequestInterceptor.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.rest.impl; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchRequestInterceptor; +import org.hibernate.search.util.common.AssertionFailure; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpEntityEnclosingRequest; +import org.apache.http.HttpRequest; +import org.apache.http.HttpRequestInterceptor; +import org.apache.http.NameValuePair; +import org.apache.http.client.utils.URLEncodedUtils; +import org.apache.http.protocol.HttpContext; +import org.apache.http.protocol.HttpCoreContext; + +record ClientRestHttpRequestInterceptor(ElasticsearchRequestInterceptor elasticsearchRequestInterceptor) + implements HttpRequestInterceptor { + + @Override + public void process(HttpRequest request, HttpContext context) throws IOException { + elasticsearchRequestInterceptor.intercept( + new ClientRestRequestContext( request, context ) + ); + } + + private record ClientRestRequestContext(HttpRequest request, HttpCoreContext coreContext) + implements ElasticsearchRequestInterceptor.RequestContext { + private ClientRestRequestContext(HttpRequest request, HttpContext coreContext) { + this( request, HttpCoreContext.adapt( coreContext ) ); + } + + @Override + public boolean hasContent() { + if ( request instanceof HttpEntityEnclosingRequest enclosingRequest ) { + return enclosingRequest.getEntity() != null; + } + return false; + } + + @Override + public InputStream content() throws IOException { + if ( request instanceof HttpEntityEnclosingRequest enclosingRequest ) { + HttpEntity entity = enclosingRequest.getEntity(); + if ( entity == null ) { + return null; + } + if ( !entity.isRepeatable() ) { + throw new AssertionFailure( "Cannot sign AWS requests with non-repeatable entities" ); + } + return entity.getContent(); + } + else { + return null; + } + } + + @Override + public String scheme() { + return coreContext.getTargetHost().getSchemeName(); + } + + @Override + public String host() { + return coreContext.getTargetHost().getHostName(); + } + + @Override + public Integer port() { + return coreContext.getTargetHost().getPort(); + } + + @Override + public String method() { + return request.getRequestLine().getMethod(); + } + + @Override + public String path() { + String uri = request.getRequestLine().getUri(); + int queryStart = uri.indexOf( '?' ); + if ( queryStart >= 0 ) { + return uri.substring( 0, queryStart ); + } + return uri; + } + + @Override + public Map queryParameters() { + String pathAndQuery = request.getRequestLine().getUri(); + List queryParameters; + int queryStart = pathAndQuery.indexOf( '?' ); + if ( queryStart >= 0 ) { + queryParameters = URLEncodedUtils.parse( pathAndQuery.substring( queryStart + 1 ), StandardCharsets.UTF_8 ); + Map map = new HashMap<>(); + for ( NameValuePair parameter : queryParameters ) { + map.put( parameter.getName(), parameter.getValue() ); + } + return map; + } + return Map.of(); + } + + @Override + public void overrideHeaders(Map> headers) { + for ( Map.Entry> header : headers.entrySet() ) { + String name = header.getKey(); + boolean first = true; + for ( String value : header.getValue() ) { + if ( first ) { + request.setHeader( name, value ); + first = false; + } + else { + request.addHeader( name, value ); + } + } + } + } + + @Override + public String toString() { + return request.toString(); + } + } +} diff --git a/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/impl/CountingOutputStream.java b/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/impl/CountingOutputStream.java new file mode 100644 index 00000000000..1ea595fb21a --- /dev/null +++ b/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/impl/CountingOutputStream.java @@ -0,0 +1,46 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.rest.impl; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +final class CountingOutputStream extends FilterOutputStream { + + private long bytesWritten = 0L; + + public CountingOutputStream(OutputStream out) { + super( out ); + } + + @Override + public void write(int b) throws IOException { + out.write( b ); + count( 1 ); + } + + @Override + public void write(byte[] b) throws IOException { + write( b, 0, b.length ); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + out.write( b, off, len ); + count( len ); + } + + void count(int written) { + if ( written > 0 ) { + bytesWritten += written; + } + } + + public long getBytesWritten() { + return bytesWritten; + } + +} diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/CustomConnectionKeepAliveStrategy.java b/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/impl/CustomConnectionKeepAliveStrategy.java similarity index 93% rename from backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/CustomConnectionKeepAliveStrategy.java rename to backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/impl/CustomConnectionKeepAliveStrategy.java index 4af070102e0..d5fef9cdbb4 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/CustomConnectionKeepAliveStrategy.java +++ b/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/impl/CustomConnectionKeepAliveStrategy.java @@ -2,7 +2,7 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright Red Hat Inc. and Hibernate Authors */ -package org.hibernate.search.backend.elasticsearch.client.impl; +package org.hibernate.search.backend.elasticsearch.client.rest.impl; import org.apache.http.HttpResponse; import org.apache.http.conn.ConnectionKeepAliveStrategy; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/GsonHttpEntity.java b/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/impl/GsonHttpEntity.java similarity index 96% rename from backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/GsonHttpEntity.java rename to backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/impl/GsonHttpEntity.java index ebb2e9fa962..c04afd5f7d6 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/GsonHttpEntity.java +++ b/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/impl/GsonHttpEntity.java @@ -2,7 +2,7 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright Red Hat Inc. and Hibernate Authors */ -package org.hibernate.search.backend.elasticsearch.client.impl; +package org.hibernate.search.backend.elasticsearch.client.rest.impl; import java.io.IOException; import java.io.InputStream; @@ -13,6 +13,7 @@ import java.nio.charset.StandardCharsets; import java.util.List; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchRequest; import org.hibernate.search.util.common.impl.Contracts; import com.google.gson.Gson; @@ -79,6 +80,14 @@ final class GsonHttpEntity implements HttpEntity, HttpAsyncContentProducer { */ private static final int CHAR_BUFFER_SIZE = BYTE_BUFFER_PAGE_SIZE; + public static HttpEntity toEntity(Gson gson, ElasticsearchRequest request) throws IOException { + final List bodyParts = request.bodyParts(); + if ( bodyParts.isEmpty() ) { + return null; + } + return new GsonHttpEntity( gson, bodyParts ); + } + private final Gson gson; private final List bodyParts; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/HttpAsyncContentProducerInputStream.java b/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/impl/HttpAsyncContentProducerInputStream.java similarity index 96% rename from backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/HttpAsyncContentProducerInputStream.java rename to backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/impl/HttpAsyncContentProducerInputStream.java index a3edbbeefd3..ff85f6213fc 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/HttpAsyncContentProducerInputStream.java +++ b/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/impl/HttpAsyncContentProducerInputStream.java @@ -2,7 +2,7 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright Red Hat Inc. and Hibernate Authors */ -package org.hibernate.search.backend.elasticsearch.client.impl; +package org.hibernate.search.backend.elasticsearch.client.rest.impl; import java.io.IOException; import java.io.InputStream; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/ProgressiveCharBufferWriter.java b/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/impl/ProgressiveCharBufferWriter.java similarity index 99% rename from backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/ProgressiveCharBufferWriter.java rename to backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/impl/ProgressiveCharBufferWriter.java index 1c5f294880c..74334f12e09 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/ProgressiveCharBufferWriter.java +++ b/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/impl/ProgressiveCharBufferWriter.java @@ -2,7 +2,7 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright Red Hat Inc. and Hibernate Authors */ -package org.hibernate.search.backend.elasticsearch.client.impl; +package org.hibernate.search.backend.elasticsearch.client.rest.impl; import java.io.IOException; import java.io.Writer; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/ServerUris.java b/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/impl/ServerUris.java similarity index 68% rename from backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/ServerUris.java rename to backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/impl/ServerUris.java index 683e998bb04..d3f5edf47b0 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/ServerUris.java +++ b/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/impl/ServerUris.java @@ -2,14 +2,14 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright Red Hat Inc. and Hibernate Authors */ -package org.hibernate.search.backend.elasticsearch.client.impl; +package org.hibernate.search.backend.elasticsearch.client.rest.impl; import java.util.List; import java.util.Locale; import java.util.Optional; -import org.hibernate.search.backend.elasticsearch.cfg.ElasticsearchBackendSettings; -import org.hibernate.search.backend.elasticsearch.logging.impl.ConfigurationLog; +import org.hibernate.search.backend.elasticsearch.client.common.cfg.ElasticsearchBackendClientCommonSettings; +import org.hibernate.search.backend.elasticsearch.client.common.logging.spi.ElasticsearchClientConfigurationLog; import org.apache.http.HttpHost; @@ -26,18 +26,21 @@ private ServerUris(HttpHost[] hosts, boolean sslEnabled) { static ServerUris fromOptionalStrings(Optional protocol, Optional> hostAndPortStrings, Optional> uris) { if ( !uris.isPresent() ) { - String protocolValue = ( protocol.isPresent() ) ? protocol.get() : ElasticsearchBackendSettings.Defaults.PROTOCOL; + String protocolValue = + ( protocol.isPresent() ) ? protocol.get() : ElasticsearchBackendClientCommonSettings.Defaults.PROTOCOL; List hostAndPortValues = - ( hostAndPortStrings.isPresent() ) ? hostAndPortStrings.get() : ElasticsearchBackendSettings.Defaults.HOSTS; + ( hostAndPortStrings.isPresent() ) + ? hostAndPortStrings.get() + : ElasticsearchBackendClientCommonSettings.Defaults.HOSTS; return fromStrings( protocolValue, hostAndPortValues ); } if ( protocol.isPresent() ) { - throw ConfigurationLog.INSTANCE.uriAndProtocol( uris.get(), protocol.get() ); + throw ElasticsearchClientConfigurationLog.INSTANCE.uriAndProtocol( uris.get(), protocol.get() ); } if ( hostAndPortStrings.isPresent() ) { - throw ConfigurationLog.INSTANCE.uriAndHosts( uris.get(), hostAndPortStrings.get() ); + throw ElasticsearchClientConfigurationLog.INSTANCE.uriAndHosts( uris.get(), hostAndPortStrings.get() ); } return fromStrings( uris.get() ); @@ -45,7 +48,7 @@ static ServerUris fromOptionalStrings(Optional protocol, Optional serverUrisStrings) { if ( serverUrisStrings.isEmpty() ) { - throw ConfigurationLog.INSTANCE.emptyListOfUris(); + throw ElasticsearchClientConfigurationLog.INSTANCE.emptyListOfUris(); } HttpHost[] hosts = new HttpHost[serverUrisStrings.size()]; @@ -59,7 +62,7 @@ private static ServerUris fromStrings(List serverUrisStrings) { https = currentHttps; } else if ( currentHttps != https ) { - throw ConfigurationLog.INSTANCE.differentProtocolsOnUris( serverUrisStrings ); + throw ElasticsearchClientConfigurationLog.INSTANCE.differentProtocolsOnUris( serverUrisStrings ); } } @@ -68,7 +71,7 @@ else if ( currentHttps != https ) { private static ServerUris fromStrings(String protocol, List hostAndPortStrings) { if ( hostAndPortStrings.isEmpty() ) { - throw ConfigurationLog.INSTANCE.emptyListOfHosts(); + throw ElasticsearchClientConfigurationLog.INSTANCE.emptyListOfHosts(); } HttpHost[] hosts = new HttpHost[hostAndPortStrings.size()]; @@ -84,7 +87,7 @@ private static ServerUris fromStrings(String protocol, List hostAndPortS private static HttpHost createHttpHost(String scheme, String hostAndPort) { if ( hostAndPort.indexOf( "://" ) >= 0 ) { - throw ConfigurationLog.INSTANCE.invalidHostAndPort( hostAndPort, null ); + throw ElasticsearchClientConfigurationLog.INSTANCE.invalidHostAndPort( hostAndPort, null ); } String host; int port = -1; @@ -97,7 +100,7 @@ private static HttpHost createHttpHost(String scheme, String hostAndPort) { port = Integer.parseInt( hostAndPort.substring( portIdx + 1 ) ); } catch (final NumberFormatException e) { - throw ConfigurationLog.INSTANCE.invalidHostAndPort( hostAndPort, e ); + throw ElasticsearchClientConfigurationLog.INSTANCE.invalidHostAndPort( hostAndPort, e ); } host = hostAndPort.substring( 0, portIdx ); } diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/StubIOControl.java b/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/impl/StubIOControl.java similarity index 92% rename from backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/StubIOControl.java rename to backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/impl/StubIOControl.java index d3a9186b495..8dc82f6ee8f 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/StubIOControl.java +++ b/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/impl/StubIOControl.java @@ -2,7 +2,7 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright Red Hat Inc. and Hibernate Authors */ -package org.hibernate.search.backend.elasticsearch.client.impl; +package org.hibernate.search.backend.elasticsearch.client.rest.impl; import java.io.IOException; diff --git a/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/logging/impl/ElasticsearchClientLog.java b/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/logging/impl/ElasticsearchClientLog.java new file mode 100644 index 00000000000..84102d28f47 --- /dev/null +++ b/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/logging/impl/ElasticsearchClientLog.java @@ -0,0 +1,29 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.rest.logging.impl; + +import java.lang.invoke.MethodHandles; + +import org.hibernate.search.util.common.logging.CategorizedLogger; +import org.hibernate.search.util.common.logging.impl.LoggerFactory; +import org.hibernate.search.util.common.logging.impl.MessageConstants; + +import org.jboss.logging.annotations.MessageLogger; + +@CategorizedLogger( + category = ElasticsearchClientLog.CATEGORY_NAME, + description = """ + Logs information on low-level Elasticsearch backend operations. + + + This may include warnings about misconfigured Elasticsearch REST clients or index operations. + """ +) +@MessageLogger(projectCode = MessageConstants.PROJECT_CODE) +public interface ElasticsearchClientLog { + String CATEGORY_NAME = "org.hibernate.search.elasticsearch.client"; + + ElasticsearchClientLog INSTANCE = LoggerFactory.make( ElasticsearchClientLog.class, CATEGORY_NAME, MethodHandles.lookup() ); + +} diff --git a/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/package-info.java b/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/package-info.java new file mode 100644 index 00000000000..e60c5ae145f --- /dev/null +++ b/backend/elasticsearch-client/elasticsearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/rest/package-info.java @@ -0,0 +1 @@ +package org.hibernate.search.backend.elasticsearch.client.rest; diff --git a/backend/elasticsearch-client/elasticsearch-rest-client/src/main/resources/META-INF/services/org.hibernate.search.engine.environment.bean.spi.BeanConfigurer b/backend/elasticsearch-client/elasticsearch-rest-client/src/main/resources/META-INF/services/org.hibernate.search.engine.environment.bean.spi.BeanConfigurer new file mode 100644 index 00000000000..1c7d97aec0a --- /dev/null +++ b/backend/elasticsearch-client/elasticsearch-rest-client/src/main/resources/META-INF/services/org.hibernate.search.engine.environment.bean.spi.BeanConfigurer @@ -0,0 +1 @@ +org.hibernate.search.backend.elasticsearch.client.rest.impl.ClientRestElasticsearchClientBeanConfigurer diff --git a/backend/elasticsearch/src/test/java/org/hibernate/search/backend/elasticsearch/client/impl/GsonHttpEntityTest.java b/backend/elasticsearch-client/elasticsearch-rest-client/src/test/java/org/hibernate/search/backend/elasticsearch/client/rest/impl/GsonHttpEntityTest.java similarity index 99% rename from backend/elasticsearch/src/test/java/org/hibernate/search/backend/elasticsearch/client/impl/GsonHttpEntityTest.java rename to backend/elasticsearch-client/elasticsearch-rest-client/src/test/java/org/hibernate/search/backend/elasticsearch/client/rest/impl/GsonHttpEntityTest.java index 28fa52595c1..94cee931310 100644 --- a/backend/elasticsearch/src/test/java/org/hibernate/search/backend/elasticsearch/client/impl/GsonHttpEntityTest.java +++ b/backend/elasticsearch-client/elasticsearch-rest-client/src/test/java/org/hibernate/search/backend/elasticsearch/client/rest/impl/GsonHttpEntityTest.java @@ -2,7 +2,7 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright Red Hat Inc. and Hibernate Authors */ -package org.hibernate.search.backend.elasticsearch.client.impl; +package org.hibernate.search.backend.elasticsearch.client.rest.impl; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assumptions.assumeTrue; @@ -23,7 +23,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import org.hibernate.search.backend.elasticsearch.gson.spi.GsonProvider; +import org.hibernate.search.backend.elasticsearch.client.common.gson.spi.GsonProvider; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; diff --git a/backend/elasticsearch-client/opensearch-rest-client/pom.xml b/backend/elasticsearch-client/opensearch-rest-client/pom.xml new file mode 100644 index 00000000000..8f081fe7883 --- /dev/null +++ b/backend/elasticsearch-client/opensearch-rest-client/pom.xml @@ -0,0 +1,88 @@ + + + + 4.0.0 + + org.hibernate.search + hibernate-search-parent-public + 8.2.0-SNAPSHOT + ../../../build/parents/public + + hibernate-search-backend-elasticsearch-client-opensearch + + Hibernate Search Backend - Elasticsearch client based on the low-level OpenSearch client + Hibernate Search Elasticsearch client based on the low-level OpenSearch client + + + + false + org.hibernate.search.backend.elasticsearch.client.opensearch + + + + + org.hibernate.search + hibernate-search-engine + + + org.hibernate.search + hibernate-search-backend-elasticsearch-client-common + + + org.opensearch.client + opensearch-rest-client + + + org.opensearch.client + opensearch-rest-client-sniffer + + + org.apache.httpcomponents.client5 + httpclient5 + + + org.apache.httpcomponents.core5 + httpcore5 + + + org.jboss.logging + jboss-logging + + + org.jboss.logging + jboss-logging-annotations + + + com.google.code.gson + gson + + + + + com.fasterxml.jackson.core + jackson-core + + + + + org.hibernate.search + hibernate-search-util-internal-test-common + test + + + + + + + org.moditect + moditect-maven-plugin + + + + diff --git a/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/ElasticsearchHttpClientConfigurationContext.java b/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/ElasticsearchHttpClientConfigurationContext.java new file mode 100644 index 00000000000..45d3a27a2b9 --- /dev/null +++ b/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/ElasticsearchHttpClientConfigurationContext.java @@ -0,0 +1,42 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.opensearch; + + +import org.hibernate.search.engine.cfg.ConfigurationPropertySource; +import org.hibernate.search.engine.environment.bean.BeanResolver; +import org.hibernate.search.util.common.annotation.impl.SuppressJQAssistant; + +import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder; + + +/** + * The context passed to {@link ElasticsearchHttpClientConfigurer}. + */ +@SuppressJQAssistant( + reason = "Apache HTTP Client 5 uses a lot of classes/interfaces in the impl packages to create builders/instances etc. " + + "So while it is bad to expose impl types ... in this case it's what Apache Client expects users to do?") +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 An Apache HTTP client builder, to set the configuration. + * @see the Apache HTTP Client documentation + */ + HttpAsyncClientBuilder clientBuilder(); + +} diff --git a/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/ElasticsearchHttpClientConfigurer.java b/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/ElasticsearchHttpClientConfigurer.java new file mode 100644 index 00000000000..2cbf4231e74 --- /dev/null +++ b/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/ElasticsearchHttpClientConfigurer.java @@ -0,0 +1,37 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.opensearch; + +/** + * An extension point allowing fine tuning of the Apache 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. + */ +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 Apache HTTP client builder + * and configuration properties in particular. + */ + void configure(ElasticsearchHttpClientConfigurationContext context); + +} diff --git a/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/cfg/ClientOpenSearchElasticsearchBackendClientSettings.java b/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/cfg/ClientOpenSearchElasticsearchBackendClientSettings.java new file mode 100644 index 00000000000..0f7853612de --- /dev/null +++ b/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/cfg/ClientOpenSearchElasticsearchBackendClientSettings.java @@ -0,0 +1,138 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.opensearch.cfg; + +import org.hibernate.search.backend.elasticsearch.client.common.cfg.ElasticsearchBackendClientCommonSettings; +import org.hibernate.search.backend.elasticsearch.client.opensearch.ElasticsearchHttpClientConfigurer; + +/** + * 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 + */ +public final class ClientOpenSearchElasticsearchBackendClientSettings { + + private ClientOpenSearchElasticsearchBackendClientSettings() { + } + + /** + * 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 reading responses from an Elasticsearch server. + *

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

+ * Defaults to {@link Defaults#READ_TIMEOUT}. + */ + public static final String READ_TIMEOUT = "read_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"; + + /** + * The maximum number of simultaneous connections to the Elasticsearch cluster, + * all hosts taken together. + *

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

+ * Defaults to {@link Defaults#MAX_CONNECTIONS}. + */ + public static final String MAX_CONNECTIONS = "max_connections"; + + /** + * The maximum number of simultaneous connections to each host of the Elasticsearch cluster. + *

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

+ * Defaults to {@link Defaults#MAX_CONNECTIONS_PER_ROUTE}. + */ + public static final String MAX_CONNECTIONS_PER_ROUTE = "max_connections_per_route"; + + /** + * Whether automatic discovery of nodes in the Elasticsearch cluster is enabled. + *

+ * Expects a Boolean value such as {@code true} or {@code false}, + * or a string that can be parsed into a Boolean value. + *

+ * Defaults to {@link Defaults#DISCOVERY_ENABLED}. + */ + public static final String DISCOVERY_ENABLED = "discovery.enabled"; + + /** + * The time interval between two executions of the automatic discovery, if enabled. + *

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

+ * Defaults to {@link Defaults#DISCOVERY_REFRESH_INTERVAL}. + */ + public static final String DISCOVERY_REFRESH_INTERVAL = "discovery.refresh_interval"; + + /** + * How long connections to the Elasticsearch cluster can be kept idle. + *

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

+ * If the response from an Elasticsearch cluster contains a {@code Keep-Alive} header, + * then the effective max idle time will be whichever is lower: + * the duration from the {@code Keep-Alive} header or the value of this property (if set). + *

+ * If this property is not set, only the {@code Keep-Alive} header is considered, + * and if it's absent, idle connections will be kept forever. + */ + public static final String MAX_KEEP_ALIVE = "max_keep_alive"; + + /** + * 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 ElasticsearchBackendClientCommonSettings#USERNAME} or {@link #MAX_CONNECTIONS_PER_ROUTE}. + *

+ * 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 READ_TIMEOUT = 30000; + public static final int CONNECTION_TIMEOUT = 1000; + public static final int MAX_CONNECTIONS = 40; + public static final int MAX_CONNECTIONS_PER_ROUTE = 20; + public static final boolean DISCOVERY_ENABLED = false; + public static final int DISCOVERY_REFRESH_INTERVAL = 10; + } +} diff --git a/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/cfg/spi/ClientOpenSearchElasticsearchBackendClientSpiSettings.java b/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/cfg/spi/ClientOpenSearchElasticsearchBackendClientSpiSettings.java new file mode 100644 index 00000000000..30ade540944 --- /dev/null +++ b/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/cfg/spi/ClientOpenSearchElasticsearchBackendClientSpiSettings.java @@ -0,0 +1,55 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.opensearch.cfg.spi; + +import org.hibernate.search.engine.cfg.EngineSettings; + +/** + * Configuration properties for the Elasticsearch backend that are considered SPI (and not API). + */ +public final class ClientOpenSearchElasticsearchBackendClientSpiSettings { + + /** + * The prefix expected for the key of every Hibernate Search configuration property. + */ + public static final String PREFIX = EngineSettings.PREFIX + "backend."; + + /** + * An external Elasticsearch 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 Elasticsearch, + * and all other client-related configuration properties + * (hosts/uris, authentication, discovery, timeouts, max connections, configurer, ...) + * will be ignored. + *

+ * Expects a reference to a bean of type {@link org.opensearch.client.RestClient}. + *

+ * 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 ClientOpenSearchElasticsearchBackendClientSpiSettings() { + } + + /** + * 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-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/impl/ByteBufferDataStreamChannel.java b/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/impl/ByteBufferDataStreamChannel.java new file mode 100644 index 00000000000..9ce04edc378 --- /dev/null +++ b/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/impl/ByteBufferDataStreamChannel.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.opensearch.impl; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; + +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.nio.ContentEncoder; +import org.apache.hc.core5.http.nio.DataStreamChannel; + + +final class ByteBufferDataStreamChannel implements ContentEncoder, DataStreamChannel { + private final ByteBuffer buffer; + private boolean complete = false; + + ByteBufferDataStreamChannel(ByteBuffer buffer) { + this.buffer = buffer; + if ( !buffer.hasArray() ) { + throw new IllegalArgumentException( getClass().getName() + " requires a ByteBuffer backed by an array." ); + } + } + + @Override + public void requestOutput() { + + } + + @Override + public int write(ByteBuffer src) { + int toWrite = Math.min( src.remaining(), buffer.remaining() ); + src.get( buffer.array(), buffer.arrayOffset() + buffer.position(), toWrite ); + buffer.position( buffer.position() + toWrite ); + return toWrite; + } + + @Override + public void endStream() throws IOException { + complete = true; + } + + @Override + public void endStream(List trailers) throws IOException { + complete = true; + } + + @Override + public void complete(List trailers) throws IOException { + complete = true; + } + + @Override + public boolean isCompleted() { + return complete; + } +} diff --git a/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/impl/ClientOpenSearchElasticsearchClient.java b/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/impl/ClientOpenSearchElasticsearchClient.java new file mode 100644 index 00000000000..eeb7dd493bc --- /dev/null +++ b/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/impl/ClientOpenSearchElasticsearchClient.java @@ -0,0 +1,271 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.opensearch.impl; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.hibernate.search.backend.elasticsearch.client.common.gson.spi.JsonLogHelper; +import org.hibernate.search.backend.elasticsearch.client.common.logging.spi.ElasticsearchClientLog; +import org.hibernate.search.backend.elasticsearch.client.common.logging.spi.ElasticsearchRequestLog; +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.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; + +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.util.Timeout; +import org.opensearch.client.Request; +import org.opensearch.client.RequestOptions; +import org.opensearch.client.Response; +import org.opensearch.client.ResponseException; +import org.opensearch.client.ResponseListener; +import org.opensearch.client.RestClient; +import org.opensearch.client.sniff.Sniffer; + +public class ClientOpenSearchElasticsearchClient implements ElasticsearchClientImplementor { + + private final BeanHolder restClientHolder; + + private final Sniffer sniffer; + + private final SimpleScheduledExecutor timeoutExecutorService; + + private final Optional requestTimeoutMs; + + private final Gson gson; + private final JsonLogHelper jsonLogHelper; + + ClientOpenSearchElasticsearchClient(BeanHolder restClientHolder, Sniffer sniffer, + SimpleScheduledExecutor timeoutExecutorService, + Optional requestTimeoutMs, + Gson gson, JsonLogHelper jsonLogHelper) { + this.restClientHolder = restClientHolder; + this.sniffer = sniffer; + this.timeoutExecutorService = timeoutExecutorService; + this.requestTimeoutMs = requestTimeoutMs; + this.gson = gson; + this.jsonLogHelper = jsonLogHelper; + } + + @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 ( RestClient.class.isAssignableFrom( clientClass ) ) { + return (T) restClientHolder.get(); + } + throw ElasticsearchClientLog.INSTANCE.clientUnwrappingWithUnknownType( clientClass, RestClient.class ); + } + + private CompletableFuture send(ElasticsearchRequest elasticsearchRequest) { + CompletableFuture completableFuture = new CompletableFuture<>(); + + HttpEntity entity; + try { + entity = GsonHttpEntity.toEntity( gson, elasticsearchRequest ); + } + catch (IOException | RuntimeException e) { + completableFuture.completeExceptionally( e ); + return completableFuture; + } + + restClientHolder.get().performRequestAsync( + toRequest( elasticsearchRequest, entity ), + new ResponseListener() { + @Override + public void onSuccess(Response response) { + completableFuture.complete( response ); + } + + @Override + public void onFailure(Exception exception) { + if ( exception instanceof ResponseException ) { + /* + * The client tries to guess what's an error and what's not, but it's too naive. + * A 404 on DELETE is not always important to us, for instance. + * Thus we ignore the exception and do our own checks afterwards. + */ + completableFuture.complete( ( (ResponseException) exception ).getResponse() ); + } + else { + completableFuture.completeExceptionally( exception ); + } + } + } + ); + + Deadline deadline = elasticsearchRequest.deadline(); + if ( deadline == null && !requestTimeoutMs.isPresent() ) { + // 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? + * In any case, the RestClient doesn't return the Future from Apache HTTP client, + * so we can't do much until this changes. + */ + 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 Request toRequest(ElasticsearchRequest elasticsearchRequest, HttpEntity entity) { + Request request = new Request( elasticsearchRequest.method(), elasticsearchRequest.path() ); + setPerRequestSocketTimeout( elasticsearchRequest, request ); + + for ( Entry parameter : elasticsearchRequest.parameters().entrySet() ) { + request.addParameter( parameter.getKey(), parameter.getValue() ); + } + + request.setEntity( entity ); + + return request; + } + + private void setPerRequestSocketTimeout(ElasticsearchRequest elasticsearchRequest, Request 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; + RequestConfig requestConfig = RequestConfig.custom() + .setConnectionRequestTimeout( Timeout.DISABLED ) //Disable lease handling for the connection pool! See also HSEARCH-2681 + .setResponseTimeout( generalRequestTimeoutMs, TimeUnit.MILLISECONDS ) + .build(); + + RequestOptions.Builder requestOptions = RequestOptions.DEFAULT.toBuilder() + .setRequestConfig( requestConfig ); + + request.setOptions( requestOptions ); + } + + private ElasticsearchResponse convertResponse(Response response) { + try { + JsonObject body = parseBody( response ); + return new ElasticsearchResponse( + response.getHost().toHostString(), + response.getStatusLine().getStatusCode(), + response.getStatusLine().getReasonPhrase(), + body ); + } + catch (IOException | RuntimeException e) { + throw ElasticsearchClientLog.INSTANCE.failedToParseElasticsearchResponse( response.getStatusLine().getStatusCode(), + response.getStatusLine().getReasonPhrase(), e.getMessage(), e ); + } + } + + private JsonObject parseBody(Response response) throws IOException { + HttpEntity entity = response.getEntity(); + if ( entity == null ) { + return null; + } + + Charset charset = getCharset( entity ); + try ( InputStream inputStream = entity.getContent(); + Reader reader = new InputStreamReader( inputStream, charset ) ) { + return gson.fromJson( reader, JsonObject.class ); + } + } + + private static Charset getCharset(HttpEntity entity) { + ContentType contentType = ContentType.parse( entity.getContentType() ); + Charset charset = contentType.getCharset(); + return charset != null ? charset : StandardCharsets.UTF_8; + } + + 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.host(), 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.host(), 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. + */ + closer.push( Sniffer::close, this.sniffer ); + // 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 ); + } + } + +} diff --git a/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/impl/ClientOpenSearchElasticsearchClientBeanConfigurer.java b/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/impl/ClientOpenSearchElasticsearchClientBeanConfigurer.java new file mode 100644 index 00000000000..9133fa8f093 --- /dev/null +++ b/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/impl/ClientOpenSearchElasticsearchClientBeanConfigurer.java @@ -0,0 +1,20 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.opensearch.impl; + +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchClientFactory; +import org.hibernate.search.engine.environment.bean.BeanHolder; +import org.hibernate.search.engine.environment.bean.spi.BeanConfigurationContext; +import org.hibernate.search.engine.environment.bean.spi.BeanConfigurer; + +public class ClientOpenSearchElasticsearchClientBeanConfigurer implements BeanConfigurer { + @Override + public void configure(BeanConfigurationContext context) { + context.define( + ElasticsearchClientFactory.class, "opensearch-rest-client", + beanResolver -> BeanHolder.of( new ClientOpenSearchElasticsearchClientFactory() ) + ); + } +} diff --git a/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/impl/ClientOpenSearchElasticsearchClientFactory.java b/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/impl/ClientOpenSearchElasticsearchClientFactory.java new file mode 100644 index 00000000000..df23e61b47a --- /dev/null +++ b/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/impl/ClientOpenSearchElasticsearchClientFactory.java @@ -0,0 +1,358 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.opensearch.impl; + +import java.net.SocketAddress; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import org.hibernate.search.backend.elasticsearch.client.common.cfg.ElasticsearchBackendClientCommonSettings; +import org.hibernate.search.backend.elasticsearch.client.common.gson.spi.GsonProvider; +import org.hibernate.search.backend.elasticsearch.client.common.logging.spi.ElasticsearchClientConfigurationLog; +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.opensearch.ElasticsearchHttpClientConfigurer; +import org.hibernate.search.backend.elasticsearch.client.opensearch.cfg.ClientOpenSearchElasticsearchBackendClientSettings; +import org.hibernate.search.backend.elasticsearch.client.opensearch.cfg.spi.ClientOpenSearchElasticsearchBackendClientSpiSettings; +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; + +import org.apache.hc.client5.http.auth.AuthScope; +import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; +import org.apache.hc.client5.http.config.ConnectionConfig; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder; +import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider; +import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder; +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.nio.ssl.TlsStrategy; +import org.apache.hc.core5.net.NamedEndpoint; +import org.apache.hc.core5.reactor.ssl.TransportSecurityLayer; +import org.apache.hc.core5.util.Timeout; +import org.opensearch.client.RestClient; +import org.opensearch.client.RestClientBuilder; +import org.opensearch.client.sniff.NodesSniffer; +import org.opensearch.client.sniff.OpenSearchNodesSniffer; +import org.opensearch.client.sniff.Sniffer; +import org.opensearch.client.sniff.SnifferBuilder; + + +/** + * @author Gunnar Morling + */ +public class ClientOpenSearchElasticsearchClientFactory implements ElasticsearchClientFactory { + + private static final OptionalConfigurationProperty> CLIENT_INSTANCE = + ConfigurationProperty.forKey( ClientOpenSearchElasticsearchBackendClientSpiSettings.CLIENT_INSTANCE ) + .asBeanReference( RestClient.class ) + .build(); + + private static final OptionalConfigurationProperty> HOSTS = + ConfigurationProperty.forKey( ElasticsearchBackendClientCommonSettings.HOSTS ) + .asString().multivalued() + .build(); + + private static final OptionalConfigurationProperty PROTOCOL = + ConfigurationProperty.forKey( ElasticsearchBackendClientCommonSettings.PROTOCOL ) + .asString() + .build(); + + private static final OptionalConfigurationProperty> URIS = + ConfigurationProperty.forKey( ElasticsearchBackendClientCommonSettings.URIS ) + .asString().multivalued() + .build(); + + private static final ConfigurationProperty PATH_PREFIX = + ConfigurationProperty.forKey( ElasticsearchBackendClientCommonSettings.PATH_PREFIX ) + .asString() + .withDefault( ElasticsearchBackendClientCommonSettings.Defaults.PATH_PREFIX ) + .build(); + + private static final OptionalConfigurationProperty USERNAME = + ConfigurationProperty.forKey( ElasticsearchBackendClientCommonSettings.USERNAME ) + .asString() + .build(); + + private static final OptionalConfigurationProperty PASSWORD = + ConfigurationProperty.forKey( ElasticsearchBackendClientCommonSettings.PASSWORD ) + .asString() + .build(); + + private static final OptionalConfigurationProperty REQUEST_TIMEOUT = + ConfigurationProperty.forKey( ClientOpenSearchElasticsearchBackendClientSettings.REQUEST_TIMEOUT ) + .asIntegerStrictlyPositive() + .build(); + + private static final ConfigurationProperty READ_TIMEOUT = + ConfigurationProperty.forKey( ClientOpenSearchElasticsearchBackendClientSettings.READ_TIMEOUT ) + .asIntegerPositiveOrZeroOrNegative() + .withDefault( ClientOpenSearchElasticsearchBackendClientSettings.Defaults.READ_TIMEOUT ) + .build(); + + private static final ConfigurationProperty CONNECTION_TIMEOUT = + ConfigurationProperty.forKey( ClientOpenSearchElasticsearchBackendClientSettings.CONNECTION_TIMEOUT ) + .asIntegerPositiveOrZeroOrNegative() + .withDefault( ClientOpenSearchElasticsearchBackendClientSettings.Defaults.CONNECTION_TIMEOUT ) + .build(); + + private static final ConfigurationProperty MAX_TOTAL_CONNECTION = + ConfigurationProperty.forKey( ClientOpenSearchElasticsearchBackendClientSettings.MAX_CONNECTIONS ) + .asIntegerStrictlyPositive() + .withDefault( ClientOpenSearchElasticsearchBackendClientSettings.Defaults.MAX_CONNECTIONS ) + .build(); + + private static final ConfigurationProperty MAX_TOTAL_CONNECTION_PER_ROUTE = + ConfigurationProperty.forKey( ClientOpenSearchElasticsearchBackendClientSettings.MAX_CONNECTIONS_PER_ROUTE ) + .asIntegerStrictlyPositive() + .withDefault( ClientOpenSearchElasticsearchBackendClientSettings.Defaults.MAX_CONNECTIONS_PER_ROUTE ) + .build(); + + private static final ConfigurationProperty DISCOVERY_ENABLED = + ConfigurationProperty.forKey( ClientOpenSearchElasticsearchBackendClientSettings.DISCOVERY_ENABLED ) + .asBoolean() + .withDefault( ClientOpenSearchElasticsearchBackendClientSettings.Defaults.DISCOVERY_ENABLED ) + .build(); + + private static final ConfigurationProperty DISCOVERY_REFRESH_INTERVAL = + ConfigurationProperty.forKey( ClientOpenSearchElasticsearchBackendClientSettings.DISCOVERY_REFRESH_INTERVAL ) + .asIntegerStrictlyPositive() + .withDefault( ClientOpenSearchElasticsearchBackendClientSettings.Defaults.DISCOVERY_REFRESH_INTERVAL ) + .build(); + + private static final OptionalConfigurationProperty< + BeanReference> CLIENT_CONFIGURER = + ConfigurationProperty.forKey( ClientOpenSearchElasticsearchBackendClientSettings.CLIENT_CONFIGURER ) + .asBeanReference( ElasticsearchHttpClientConfigurer.class ) + .build(); + + private static final OptionalConfigurationProperty MAX_KEEP_ALIVE = + ConfigurationProperty.forKey( ClientOpenSearchElasticsearchBackendClientSettings.MAX_KEEP_ALIVE ) + .asLongStrictlyPositive() + .build(); + + @Override + public ElasticsearchClientImplementor create(BeanResolver beanResolver, ConfigurationPropertySource propertySource, + ThreadProvider threadProvider, String threadNamePrefix, + SimpleScheduledExecutor timeoutExecutorService, + GsonProvider gsonProvider) { + Optional requestTimeoutMs = REQUEST_TIMEOUT.get( propertySource ); + + Optional> providedRestClientHolder = CLIENT_INSTANCE.getAndMap( + propertySource, beanResolver::resolve ); + + BeanHolder restClientHolder; + Sniffer sniffer; + if ( providedRestClientHolder.isPresent() ) { + restClientHolder = providedRestClientHolder.get(); + sniffer = null; + } + else { + ServerUris hosts = ServerUris.fromOptionalStrings( PROTOCOL.get( propertySource ), + HOSTS.get( propertySource ), URIS.get( propertySource ) ); + restClientHolder = createClient( beanResolver, propertySource, threadProvider, threadNamePrefix, + hosts, PATH_PREFIX.get( propertySource ) ); + sniffer = createSniffer( propertySource, restClientHolder.get(), hosts ); + } + + return new ClientOpenSearchElasticsearchClient( + restClientHolder, sniffer, timeoutExecutorService, requestTimeoutMs, + gsonProvider.getGson(), gsonProvider.getLogHelper() + ); + } + + private BeanHolder createClient(BeanResolver beanResolver, ConfigurationPropertySource propertySource, + ThreadProvider threadProvider, String threadNamePrefix, + ServerUris hosts, String pathPrefix) { + RestClientBuilder builder = RestClient.builder( hosts.asHostsArray() ); + if ( !pathPrefix.isEmpty() ) { + builder.setPathPrefix( pathPrefix ); + } + + Optional> customConfig = CLIENT_CONFIGURER + .getAndMap( propertySource, beanResolver::resolve ); + + RestClient client = null; + List> httpClientConfigurerReferences = + beanResolver.allConfiguredForRole( ElasticsearchHttpClientConfigurer.class ); + List> requestInterceptorProviderReferences = + beanResolver.allConfiguredForRole( ElasticsearchRequestInterceptorProvider.class ); + try ( BeanHolder> httpClientConfigurersHolder = + beanResolver.resolve( httpClientConfigurerReferences ); + BeanHolder> requestInterceptorProvidersHodler = + beanResolver.resolve( requestInterceptorProviderReferences ) ) { + client = builder + .setRequestConfigCallback( b -> customizeRequestConfig( b, propertySource ) ) + .setHttpClientConfigCallback( + b -> customizeHttpClientConfig( + b, + beanResolver, propertySource, + threadProvider, threadNamePrefix, + hosts, httpClientConfigurersHolder.get(), requestInterceptorProvidersHodler.get(), + customConfig + ) + ) + .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 Sniffer createSniffer(ConfigurationPropertySource propertySource, + RestClient client, ServerUris hosts) { + boolean discoveryEnabled = DISCOVERY_ENABLED.get( propertySource ); + if ( discoveryEnabled ) { + SnifferBuilder builder = Sniffer.builder( client ) + .setSniffIntervalMillis( + DISCOVERY_REFRESH_INTERVAL.get( propertySource ) * 1_000 // The configured value is in seconds + ); + + // https discovery support + if ( hosts.isSslEnabled() ) { + NodesSniffer hostsSniffer = new OpenSearchNodesSniffer( + client, + OpenSearchNodesSniffer.DEFAULT_SNIFF_REQUEST_TIMEOUT, // 1sec + OpenSearchNodesSniffer.Scheme.HTTPS ); + builder.setNodesSniffer( hostsSniffer ); + } + return builder.build(); + } + else { + return null; + } + } + + private HttpAsyncClientBuilder customizeHttpClientConfig(HttpAsyncClientBuilder builder, + BeanResolver beanResolver, ConfigurationPropertySource propertySource, + ThreadProvider threadProvider, String threadNamePrefix, + ServerUris hosts, Iterable configurers, + Iterable requestInterceptorProviders, + Optional> customConfig) { + builder.setThreadFactory( threadProvider.createThreadFactory( threadNamePrefix + " - Transport thread" ) ); + + PoolingAsyncClientConnectionManagerBuilder connectionManagerBuilder = + PoolingAsyncClientConnectionManagerBuilder.create() + .setMaxConnTotal( MAX_TOTAL_CONNECTION.get( propertySource ) ) + .setMaxConnPerRoute( MAX_TOTAL_CONNECTION_PER_ROUTE.get( propertySource ) ); + + if ( !hosts.isSslEnabled() ) { + // In this case disable the SSL capability as it might have an impact on + // bootstrap time, for example consuming entropy for no reason + // connectionManagerBuilder.setTlsStrategy( + // ClientTlsStrategyBuilder.create() + // .setSslContext( + // SSLContextBuilder.create() + // .loadTrustMaterial(null, new TrustAllStrategy()) + // .buildAsync() + // ) + // .setHostnameVerifier(NoopHostnameVerifier.INSTANCE) + // .build() + // ); + connectionManagerBuilder.setTlsStrategy( NoopTlsStrategy.INSTANCE ); + } + + builder.setConnectionManager( + connectionManagerBuilder + .setDefaultConnectionConfig( + ConnectionConfig.copy( ConnectionConfig.DEFAULT ) + .setConnectTimeout( CONNECTION_TIMEOUT.get( propertySource ), TimeUnit.MILLISECONDS ) + .setSocketTimeout( READ_TIMEOUT.get( propertySource ), TimeUnit.MILLISECONDS ) + .build() + ) + .build() + ); + + Optional username = USERNAME.get( propertySource ); + if ( username.isPresent() ) { + Optional password = PASSWORD.get( propertySource ).map( String::toCharArray ); + if ( password.isPresent() && !hosts.isSslEnabled() ) { + ElasticsearchClientConfigurationLog.INSTANCE.usingPasswordOverHttp(); + } + + BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider(); + credentialsProvider.setCredentials( + new AuthScope( null, null, -1, null, null ), + new UsernamePasswordCredentials( username.get(), password.orElse( null ) ) + ); + + builder.setDefaultCredentialsProvider( credentialsProvider ); + } + + Optional maxKeepAlive = MAX_KEEP_ALIVE.get( propertySource ); + if ( maxKeepAlive.isPresent() ) { + builder.setKeepAliveStrategy( new CustomConnectionKeepAliveStrategy( maxKeepAlive.get() ) ); + } + + ClientOpenSearchElasticsearchHttpClientConfigurationContext clientConfigurationContext = + new ClientOpenSearchElasticsearchHttpClientConfigurationContext( beanResolver, propertySource, builder ); + + for ( ElasticsearchHttpClientConfigurer configurer : configurers ) { + configurer.configure( clientConfigurationContext ); + } + for ( ElasticsearchRequestInterceptorProvider interceptorProvider : requestInterceptorProviders ) { + Optional requestInterceptor = + interceptorProvider.provide( clientConfigurationContext ); + if ( requestInterceptor.isPresent() ) { + builder.addRequestInterceptorLast( new ClientOpenSearchHttpRequestInterceptor( requestInterceptor.get() ) ); + } + } + if ( customConfig.isPresent() ) { + BeanHolder customConfigBeanHolder = customConfig.get(); + customConfigBeanHolder.get().configure( clientConfigurationContext ); + } + + return builder; + } + + private RequestConfig.Builder customizeRequestConfig(RequestConfig.Builder builder, + ConfigurationPropertySource propertySource) { + return builder + .setConnectionRequestTimeout( Timeout.DISABLED ) //Disable lease handling for the connection pool! See also HSEARCH-2681 + .setResponseTimeout( READ_TIMEOUT.get( propertySource ), TimeUnit.MILLISECONDS ); + } + + private static class NoopTlsStrategy implements TlsStrategy { + private static final NoopTlsStrategy INSTANCE = new NoopTlsStrategy(); + + private NoopTlsStrategy() { + } + + @SuppressWarnings("deprecation") + @Override + public boolean upgrade(TransportSecurityLayer sessionLayer, HttpHost host, SocketAddress localAddress, + SocketAddress remoteAddress, Object attachment, Timeout handshakeTimeout) { + throw new UnsupportedOperationException( "upgrade is not supported." ); + } + + @Override + public void upgrade(TransportSecurityLayer sessionLayer, NamedEndpoint endpoint, Object attachment, + Timeout handshakeTimeout, FutureCallback callback) { + if ( callback != null ) { + callback.completed( sessionLayer ); + } + } + } +} diff --git a/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/impl/ClientOpenSearchElasticsearchHttpClientConfigurationContext.java b/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/impl/ClientOpenSearchElasticsearchHttpClientConfigurationContext.java new file mode 100644 index 00000000000..4d45960c801 --- /dev/null +++ b/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/impl/ClientOpenSearchElasticsearchHttpClientConfigurationContext.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.opensearch.impl; + +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchRequestInterceptorProvider; +import org.hibernate.search.backend.elasticsearch.client.opensearch.ElasticsearchHttpClientConfigurationContext; +import org.hibernate.search.engine.cfg.ConfigurationPropertySource; +import org.hibernate.search.engine.environment.bean.BeanResolver; + +import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder; + +final class ClientOpenSearchElasticsearchHttpClientConfigurationContext + implements ElasticsearchHttpClientConfigurationContext, ElasticsearchRequestInterceptorProvider.Context { + private final BeanResolver beanResolver; + private final ConfigurationPropertySource configurationPropertySource; + private final HttpAsyncClientBuilder clientBuilder; + + ClientOpenSearchElasticsearchHttpClientConfigurationContext( + BeanResolver beanResolver, + ConfigurationPropertySource configurationPropertySource, + HttpAsyncClientBuilder clientBuilder) { + this.beanResolver = beanResolver; + this.configurationPropertySource = configurationPropertySource; + this.clientBuilder = clientBuilder; + } + + @Override + public BeanResolver beanResolver() { + return beanResolver; + } + + @Override + public ConfigurationPropertySource configurationPropertySource() { + return configurationPropertySource; + } + + @Override + public HttpAsyncClientBuilder clientBuilder() { + return clientBuilder; + } + +} diff --git a/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/impl/ClientOpenSearchHttpRequestInterceptor.java b/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/impl/ClientOpenSearchHttpRequestInterceptor.java new file mode 100644 index 00000000000..eda90a14caf --- /dev/null +++ b/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/impl/ClientOpenSearchHttpRequestInterceptor.java @@ -0,0 +1,142 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.opensearch.impl; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchRequestInterceptor; +import org.hibernate.search.util.common.AssertionFailure; + +import org.apache.hc.client5.http.protocol.HttpClientContext; +import org.apache.hc.core5.http.EntityDetails; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.HttpEntityContainer; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpRequestInterceptor; +import org.apache.hc.core5.http.NameValuePair; +import org.apache.hc.core5.http.nio.AsyncEntityProducer; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.net.URIBuilder; + +record ClientOpenSearchHttpRequestInterceptor(ElasticsearchRequestInterceptor elasticsearchRequestInterceptor) + implements HttpRequestInterceptor { + + @Override + public void process(HttpRequest request, EntityDetails entity, HttpContext context) throws IOException { + elasticsearchRequestInterceptor.intercept( + new ClientOpenSearchRequestContext( request, entity, context ) + ); + } + + private record ClientOpenSearchRequestContext(HttpRequest request, EntityDetails entity, HttpClientContext clientContext) + implements ElasticsearchRequestInterceptor.RequestContext { + private ClientOpenSearchRequestContext(HttpRequest request, EntityDetails entity, HttpContext context) { + this( request, entity, HttpClientContext.cast( context ) ); + } + + @Override + public boolean hasContent() { + return entity != null; + } + + @Override + public InputStream content() throws IOException { + HttpEntity localEntity = null; + if ( entity instanceof HttpEntity httpEntity ) { + localEntity = httpEntity; + } + else if ( request instanceof HttpEntityContainer entityContainer ) { + localEntity = entityContainer.getEntity(); + } + + if ( localEntity != null ) { + if ( !localEntity.isRepeatable() ) { + throw new AssertionFailure( "Cannot sign AWS requests with non-repeatable entities" ); + } + return localEntity.getContent(); + } + + if ( entity instanceof AsyncEntityProducer producer ) { + if ( !producer.isRepeatable() ) { + throw new AssertionFailure( "Cannot sign AWS requests with non-repeatable entities" ); + } + return new HttpAsyncEntityProducerInputStream( producer, 1024 ); + } + return null; + } + + @Override + public String scheme() { + return clientContext.getHttpRoute().getTargetHost().getSchemeName(); + } + + @Override + public String host() { + return clientContext.getHttpRoute().getTargetHost().getHostName(); + } + + @Override + public Integer port() { + return clientContext.getHttpRoute().getTargetHost().getPort(); + } + + @Override + public String method() { + return request.getMethod(); + } + + @Override + public String path() { + try { + return request.getUri().getPath(); + } + catch (URISyntaxException e) { + return request.getPath(); + } + } + + @Override + public Map queryParameters() { + try { + List queryParameters = new URIBuilder( request.getUri() ).getQueryParams(); + Map map = new HashMap<>(); + for ( NameValuePair parameter : queryParameters ) { + map.put( parameter.getName(), parameter.getValue() ); + } + return map; + } + catch (URISyntaxException e) { + return Map.of(); + } + } + + @Override + public void overrideHeaders(Map> headers) { + for ( Map.Entry> header : headers.entrySet() ) { + String name = header.getKey(); + boolean first = true; + for ( String value : header.getValue() ) { + if ( first ) { + request.setHeader( name, value ); + first = false; + } + else { + request.addHeader( name, value ); + } + } + } + } + + @Override + public String toString() { + return request.toString(); + } + } +} diff --git a/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/impl/CountingOutputStream.java b/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/impl/CountingOutputStream.java new file mode 100644 index 00000000000..12b5e8e7c55 --- /dev/null +++ b/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/impl/CountingOutputStream.java @@ -0,0 +1,46 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.opensearch.impl; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +final class CountingOutputStream extends FilterOutputStream { + + private long bytesWritten = 0L; + + public CountingOutputStream(OutputStream out) { + super( out ); + } + + @Override + public void write(int b) throws IOException { + out.write( b ); + count( 1 ); + } + + @Override + public void write(byte[] b) throws IOException { + write( b, 0, b.length ); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + out.write( b, off, len ); + count( len ); + } + + void count(int written) { + if ( written > 0 ) { + bytesWritten += written; + } + } + + public long getBytesWritten() { + return bytesWritten; + } + +} diff --git a/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/impl/CustomConnectionKeepAliveStrategy.java b/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/impl/CustomConnectionKeepAliveStrategy.java new file mode 100644 index 00000000000..3ed33918615 --- /dev/null +++ b/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/impl/CustomConnectionKeepAliveStrategy.java @@ -0,0 +1,37 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.opensearch.impl; + + +import java.util.concurrent.TimeUnit; + +import org.apache.hc.client5.http.ConnectionKeepAliveStrategy; +import org.apache.hc.client5.http.impl.DefaultConnectionKeepAliveStrategy; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.util.TimeValue; + +final class CustomConnectionKeepAliveStrategy implements ConnectionKeepAliveStrategy { + + private final TimeValue maxKeepAlive; + private final long maxKeepAliveMilliseconds; + + CustomConnectionKeepAliveStrategy(long maxKeepAlive) { + this.maxKeepAlive = TimeValue.of( maxKeepAlive, TimeUnit.MILLISECONDS ); + this.maxKeepAliveMilliseconds = maxKeepAlive; + } + + @Override + public TimeValue getKeepAliveDuration(HttpResponse response, HttpContext context) { + // get a keep alive from a request header if one is present + TimeValue keepAliveDuration = DefaultConnectionKeepAliveStrategy.INSTANCE.getKeepAliveDuration( response, context ); + long keepAliveDurationMilliseconds = keepAliveDuration.toMilliseconds(); + // if the keep alive timeout from a request is less than configured one - let's honor it: + if ( keepAliveDurationMilliseconds > 0 && keepAliveDurationMilliseconds < maxKeepAliveMilliseconds ) { + return keepAliveDuration; + } + return maxKeepAlive; + } +} diff --git a/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/impl/GsonHttpEntity.java b/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/impl/GsonHttpEntity.java new file mode 100644 index 00000000000..63900b05260 --- /dev/null +++ b/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/impl/GsonHttpEntity.java @@ -0,0 +1,321 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.opensearch.impl; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Set; + +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchRequest; +import org.hibernate.search.util.common.impl.Contracts; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; + +import org.apache.hc.core5.function.Supplier; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.nio.AsyncEntityProducer; +import org.apache.hc.core5.http.nio.DataStreamChannel; + +/** + * Optimised adapter to encode GSON objects into HttpEntity instances. + * The naive approach was using various StringBuilders; the objects we + * need to serialise into JSON might get large and this was causing the + * internal StringBuilder buffers to need frequent resizing and cause + * problems with excessive allocations. + * + * Rather than trying to guess reasonable default sizes for these buffers, + * we can defer the serialisation to write directly into the ByteBuffer + * of the HTTP client, this has the additional benefit of making the + * intermediary buffers short lived. + * + * The one complexity to watch for is flow control: when writing into + * the output buffer chances are that not all bytes are accepted; in + * this case we have to hold on the remaining portion of data to + * be written when the flow control is re-enabled. + * + * A side effect of this strategy is that the total content length which + * is being produced is not known in advance. Not reporting the length + * in advance to the Apache Http client causes it to use chunked-encoding, + * which is great for large blocks but not optimal for small messages. + * For this reason we attempt to start encoding into a small buffer + * upfront: if all data we need to produce fits into that then we can + * report the content length; if not the encoding completion will be deferred + * but not resetting so to avoid repeating encoding work. + * + * @author Sanne Grinovero (C) 2017 Red Hat Inc. + */ +final class GsonHttpEntity implements HttpEntity, AsyncEntityProducer { + + private static final Charset CHARSET = StandardCharsets.UTF_8; + + private static final String CONTENT_TYPE = ContentType.APPLICATION_JSON.toString(); + + /** + * The size of byte buffer pages in {@link ProgressiveCharBufferWriter} + * It's a rather large size: a tradeoff for very large JSON + * documents as we do heavy bulking, and not too large to + * be a penalty for small requests. + * 1024 has been shown to produce reasonable, TLAB only garbage. + */ + private static final int BYTE_BUFFER_PAGE_SIZE = 1024; + + /** + * We want the char buffer and byte buffer pages of approximately + * the same size, however one is in characters and the other in bytes. + * Considering we hardcoded UTF-8 as encoding, which has an average + * conversion ratio of almost 1.0, this should be close enough. + */ + private static final int CHAR_BUFFER_SIZE = BYTE_BUFFER_PAGE_SIZE; + + public static HttpEntity toEntity(Gson gson, ElasticsearchRequest request) throws IOException { + final List bodyParts = request.bodyParts(); + if ( bodyParts.isEmpty() ) { + return null; + } + return new GsonHttpEntity( gson, bodyParts ); + } + + private final Gson gson; + private final List bodyParts; + + /** + * We don't want to compute the length in advance as it would defeat the optimisations + * for large bulks. + * Still it's possible that we happen to find out, for example if a Digest from all + * content needs to be computed, or if the content is small enough as we attempt + * to serialise at least one page. + */ + private long contentLength; + + /** + * We can lazily compute the contentLength, but we need to avoid changing the value + * we report over time as this confuses the Apache HTTP client as it initially defines + * the encoding strategy based on this, then assumes it can rely on this being + * a constant. + * After the {@link #getContentLength()} was invoked at least once, freeze + * the value. + */ + private boolean contentLengthWasProvided = false; + + /** + * Since flow control might hint to stop producing data, + * while we can't interrupt the rendering of a single JSON body + * we can avoid starting the rendering of any subsequent JSON body. + * So keep track of the next body which still needs to be rendered; + * to allow the output to be "repeatable" we also need to reset this + * at the end. + */ + private int nextBodyToEncodeIndex = 0; + + /** + * Adaptor from string output rendered into the actual output sink. + * We keep this as a field level attribute as we might have + * partially rendered JSON stored in its buffers while flow control + * refuses to accept more bytes. + */ + private ProgressiveCharBufferWriter writer = + new ProgressiveCharBufferWriter( CHARSET, CHAR_BUFFER_SIZE, BYTE_BUFFER_PAGE_SIZE ); + + public GsonHttpEntity(Gson gson, List bodyParts) throws IOException { + Contracts.assertNotNull( gson, "gson" ); + Contracts.assertNotNull( bodyParts, "bodyParts" ); + this.gson = gson; + this.bodyParts = bodyParts; + this.contentLength = -1; + attemptOnePassEncoding(); + } + + @Override + public boolean isRepeatable() { + return true; + } + + @Override + public void failed(Exception cause) { + + } + + @Override + public boolean isChunked() { + return false; + } + + @Override + public Set getTrailerNames() { + return Set.of(); + } + + @Override + public long getContentLength() { + this.contentLengthWasProvided = true; + return this.contentLength; + } + + @Override + public String getContentType() { + return CONTENT_TYPE; + } + + @Override + public String getContentEncoding() { + //Apparently this is the correct value: + return null; + } + + @Override + public InputStream getContent() { + return new HttpAsyncEntityProducerInputStream( this, BYTE_BUFFER_PAGE_SIZE ); + } + + @Override + public void writeTo(OutputStream out) throws IOException { + /* + * For this method we use no pagination, so ignore the mutable fields. + * + * Note we don't close the counting stream or the writer, + * because we must not close the output stream that was passed as a parameter. + */ + CountingOutputStream countingStream = new CountingOutputStream( out ); + Writer outWriter = new OutputStreamWriter( countingStream, CHARSET ); + for ( JsonObject bodyPart : bodyParts ) { + gson.toJson( bodyPart, outWriter ); + outWriter.append( '\n' ); + } + outWriter.flush(); + //Now we finally know the content size in bytes: + hintContentLength( countingStream.getBytesWritten() ); + } + + @Override + public boolean isStreaming() { + return false; + } + + @Override + public Supplier> getTrailers() { + return null; + } + + @Override + public void close() { + //Nothing to close but let's make sure we re-wind the stream + //so that we can start from the beginning if needed + this.nextBodyToEncodeIndex = 0; + //Discard previous buffers as they might contain in-process content: + this.writer = new ProgressiveCharBufferWriter( CHARSET, CHAR_BUFFER_SIZE, BYTE_BUFFER_PAGE_SIZE ); + } + + /** + * Let's see if we can fully encode the content with a minimal write, + * i.e. only one body part. + * This will allow us to keep the memory consumption reasonable + * while also being able to hint the client about the {@link #getContentLength()}. + * Incidentally, having this information would avoid chunked output encoding + * which is ideal precisely for small messages which can fit into a single buffer. + * + * @throws IOException This is unlikely to be caused by a real IO operation as there's no output buffer yet, + * but it could also be triggered by the UTF8 encoding operations. + */ + private void attemptOnePassEncoding() throws IOException { + // Essentially attempt to use the writer without going NPE on the output sink + // as it's not set yet. + triggerFullWrite(); + if ( nextBodyToEncodeIndex == bodyParts.size() ) { + writer.flush(); + // The buffer's content length so far is the final content length, + // as we know the entire content has been encoded already. + hintContentLength( writer.contentLength() ); + } + } + + /** + * Higher level write loop. It will start writing the JSON objects + * from either the beginning or the next object which wasn't written yet + * but simply stop and return as soon as the sink can't accept more data. + * Checking state of writer.flowControlPushingBack will reveal if everything + * was written. + * @throws IOException If writing fails. + */ + private void triggerFullWrite() throws IOException { + while ( nextBodyToEncodeIndex < bodyParts.size() ) { + JsonObject bodyPart = bodyParts.get( nextBodyToEncodeIndex++ ); + gson.toJson( bodyPart, writer ); + writer.append( '\n' ); + writer.flush(); + if ( writer.isFlowControlPushingBack() ) { + //Just quit: return control to the caller and trust we'll be called again. + return; + } + } + } + + @Override + public int available() { + return 0; + } + + @Override + public void produce(DataStreamChannel channel) throws IOException { + Contracts.assertNotNull( channel, "channel" ); + // Warning: this method is possibly invoked multiple times, depending on the output buffers + // to have available space ! + // Production of data is expected to complete only after we invoke ContentEncoder#complete. + + //Re-set the encoder as it might be a different one than a previously used instance: + writer.setOutput( channel ); + + //First write unfinished business from previous attempts + writer.resumePendingWrites(); + if ( writer.isFlowControlPushingBack() ) { + //Just quit: return control to the caller and trust we'll be called again. + return; + } + + triggerFullWrite(); + + if ( writer.isFlowControlPushingBack() ) { + //Just quit: return control to the caller and trust we'll be called again. + return; + } + writer.flushToOutput(); + if ( writer.isFlowControlPushingBack() ) { + //Just quit: return control to the caller and trust we'll be called again. + return; + } + + // If we haven't aborted yet, we finished! + + // The buffer's content length so far is the final content length, + // as we know the entire content has been encoded already. + // Hint at the content length. + // Note this is only useful if produceContent was called by some process + // that is not the HTTP client itself (e.g. for request signing), + // because the HTTP Client itself will request the size before it starts writing content. + hintContentLength( writer.contentLength() ); + + channel.endStream(); + this.releaseResources(); + } + + private void hintContentLength(long contentLength) { + if ( !contentLengthWasProvided ) { + this.contentLength = contentLength; + } + } + + @Override + public void releaseResources() { + close(); + } +} diff --git a/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/impl/HttpAsyncEntityProducerInputStream.java b/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/impl/HttpAsyncEntityProducerInputStream.java new file mode 100644 index 00000000000..97f532e96ad --- /dev/null +++ b/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/impl/HttpAsyncEntityProducerInputStream.java @@ -0,0 +1,83 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.opensearch.impl; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; + +import org.apache.hc.core5.http.nio.AsyncEntityProducer; + + +final class HttpAsyncEntityProducerInputStream extends InputStream { + private final AsyncEntityProducer entityProducer; + private final ByteBuffer buffer; + private final ByteBufferDataStreamChannel contentEncoder; + + public HttpAsyncEntityProducerInputStream(AsyncEntityProducer entityProducer, int bufferSize) { + this.entityProducer = entityProducer; + this.buffer = ByteBuffer.allocate( bufferSize ); + this.buffer.limit( 0 ); + this.contentEncoder = new ByteBufferDataStreamChannel( buffer ); + } + + @Override + public int read() throws IOException { + int read = readFromBuffer(); + if ( read < 0 && !contentEncoder.isCompleted() ) { + writeToBuffer(); + read = readFromBuffer(); + } + return read; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + int offset = off; + int length = len; + while ( length > 0 && ( buffer.remaining() > 0 || !contentEncoder.isCompleted() ) ) { + if ( buffer.remaining() == 0 ) { + writeToBuffer(); + } + int bytesRead = readFromBuffer( b, offset, length ); + offset += bytesRead; + length -= bytesRead; + } + int totalBytesRead = offset - off; + if ( totalBytesRead == 0 && contentEncoder.isCompleted() ) { + return -1; + } + return totalBytesRead; + } + + @Override + public void close() { + entityProducer.releaseResources(); + } + + private void writeToBuffer() throws IOException { + buffer.clear(); + entityProducer.produce( contentEncoder ); + buffer.flip(); + } + + private int readFromBuffer() { + if ( buffer.hasRemaining() ) { + return buffer.get(); + } + else { + return -1; + } + } + + private int readFromBuffer(byte[] bytes, int offset, int length) { + int toRead = Math.min( buffer.remaining(), length ); + if ( toRead > 0 ) { + buffer.get( bytes, offset, toRead ); + } + return toRead; + } + +} diff --git a/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/impl/ProgressiveCharBufferWriter.java b/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/impl/ProgressiveCharBufferWriter.java new file mode 100644 index 00000000000..e25330c4542 --- /dev/null +++ b/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/impl/ProgressiveCharBufferWriter.java @@ -0,0 +1,288 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.opensearch.impl; + +import java.io.IOException; +import java.io.Writer; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CoderResult; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Iterator; + +import org.apache.hc.core5.http.nio.ContentEncoder; +import org.apache.hc.core5.http.nio.DataStreamChannel; + +/** + * A writer to a ContentEncoder, using an automatically growing, paged buffer + * to store input when flow control pushes back. + *

+ * To be used when your input source is not reactive (uses {@link Writer}), + * but you have multiple elements to write and thus could take advantage of + * reactive output to some extent. + * + * @author Sanne Grinovero + */ +class ProgressiveCharBufferWriter extends Writer { + + private final CharsetEncoder charsetEncoder; + + /** + * Size of buffer pages. + */ + private final int pageSize; + + /** + * A higher-level buffer for chars, so that we don't have + * to wrap every single incoming char[] into a CharBuffer. + */ + private final CharBuffer charBuffer; + + /** + * Filled buffer pages to be written, in write order. + */ + private final Deque needWritingPages = new ArrayDeque<>( 5 ); + + /** + * Current buffer page, potentially null, + * which may have some content but isn't full yet. + */ + private ByteBuffer currentPage; + + /** + * Initially null: must be set before writing is started and each + * time it's resumed as it might change between writes during + * chunked encoding. + */ + private DataStreamChannel channel; + + /** + * Set this to true when we detect clogging, so we can stop trying. + * Make sure to reset this when the HTTP Client hints so. + * It's never dangerous to re-enable, just not efficient to try writing + * unnecessarily. + */ + private boolean flowControlPushingBack = false; + + private int contentLength = 0; + + public ProgressiveCharBufferWriter(Charset charset, int charBufferSize, int pageSize) { + this.charsetEncoder = charset.newEncoder(); + this.pageSize = pageSize; + this.charBuffer = CharBuffer.allocate( charBufferSize ); + } + + /** + * Set the encoder to write to when buffers are full. + */ + public void setOutput(DataStreamChannel channel) { + this.channel = channel; + } + + // Overrides super.write(int) to remove the synchronized() wrapper. + // WARNING: when you update this method, make sure to update ALL write(...) methods. + @Override + public void write(int c) throws IOException { + if ( 1 > charBuffer.remaining() ) { + flush(); + } + charBuffer.put( (char) c ); + } + + // Overrides super.write(String, int, int) to remove the synchronized() wrapper. + // WARNING: when you update this method, make sure to update ALL write(...) methods. + @Override + public void write(String str, int off, int len) throws IOException { + // See write(char[], int, int) for comments. + if ( len > charBuffer.capacity() ) { + flush(); + writeToByteBuffer( CharBuffer.wrap( str, off, off + len ) ); + } + else if ( len > charBuffer.remaining() ) { + flush(); + charBuffer.put( str, off, off + len ); + } + else { + charBuffer.put( str, off, off + len ); + } + } + + // WARNING: when you update this method, make sure to update ALL write(...) methods. + @Override + public void write(char[] cbuf, int off, int len) throws IOException { + if ( len > charBuffer.capacity() ) { + /* + * "cbuf" won't fit in our char buffer, so we'll just write + * everything to the byte buffer (first the pending chars in the + * char buffer, then "cbuf"). + */ + flush(); + writeToByteBuffer( CharBuffer.wrap( cbuf, off, len ) ); + } + else if ( len > charBuffer.remaining() ) { + /* + * We flush the buffer before writing anything in this case. + * + * If we did not, we'd run the risk of splitting a 3 or 4-byte + * character in two parts (one at the end of the buffer before + * flushing it, and the other at the beginning after flushing it), + * and the encoder would fail when encoding the second part. + * + * See HSEARCH-2886. + */ + flush(); + charBuffer.put( cbuf, off, len ); + } + else { + charBuffer.put( cbuf, off, len ); + } + } + + @Override + public void flush() throws IOException { + if ( charBuffer.position() == 0 ) { + return; + } + charBuffer.flip(); + writeToByteBuffer( charBuffer ); + charBuffer.clear(); + + // don't flush byte buffers to output as we want to control that flushing independently. + } + + @Override + public void close() { + // Nothing to do + } + + /** + * Send all full buffer pages to the {@link #setOutput(DataStreamChannel) output}. + *

+ * Flow control may push back, in which case this method or {@link #flushToOutput()} + * should be called again later. + * + * @throws IOException when {@link ContentEncoder#write(ByteBuffer)} fails. + */ + public void resumePendingWrites() throws IOException { + flush(); + flowControlPushingBack = false; + attemptFlushPendingBuffers( false ); + } + + /** + * @return {@code true} if the {@link #setOutput(DataStreamChannel) output} pushed + * back the last time a write was attempted, {@code false} otherwise. + */ + public boolean isFlowControlPushingBack() { + return flowControlPushingBack; + } + + /** + * Send all buffer pages to the {@link #setOutput(DataStreamChannel) output}, + * Even those that are not full yet + *

+ * Flow control may push back, in which case this method should be called again later. + * + * @throws IOException when {@link ContentEncoder#write(ByteBuffer)} fails. + */ + public void flushToOutput() throws IOException { + flush(); + flowControlPushingBack = false; + attemptFlushPendingBuffers( true ); + } + + /** + * @return The length of the content stored in the byte buffers so far, in bytes. + * This does include the content that has already been written to the {@link #setOutput(DataStreamChannel) output}, + * but not the content of the char buffer (which can be flushed to byte buffers using {@link #flush()}). + */ + public int contentLength() { + return contentLength; + } + + private void writeToByteBuffer(CharBuffer input) throws IOException { + while ( true ) { + if ( currentPage == null ) { + currentPage = ByteBuffer.allocate( pageSize ); + } + int initialPagePosition = currentPage.position(); + CoderResult coderResult = charsetEncoder.encode( input, currentPage, false ); + contentLength += ( currentPage.position() - initialPagePosition ); + if ( coderResult.equals( CoderResult.UNDERFLOW ) ) { + return; + } + else if ( coderResult.equals( CoderResult.OVERFLOW ) ) { + // Avoid storing buffers if we can simply flush them + attemptFlushPendingBuffers( true ); + if ( currentPage != null ) { + /* + * We couldn't flush the current page, but it's full, + * so let's move it out of the way. + */ + currentPage.flip(); + needWritingPages.add( currentPage ); + currentPage = null; + } + } + else { + //Encoding exception + coderResult.throwException(); + return; //Unreachable + } + } + } + + /** + * @return {@code true} if this buffer contains content to be written, {@code false} otherwise. + */ + private boolean hasRemaining() { + return !needWritingPages.isEmpty() || currentPage != null && currentPage.position() > 0; + } + + private void attemptFlushPendingBuffers(boolean flushCurrentPage) throws IOException { + if ( channel == null ) { + flowControlPushingBack = true; + } + if ( flowControlPushingBack || !hasRemaining() ) { + // Nothing to do + return; + } + Iterator iterator = needWritingPages.iterator(); + while ( iterator.hasNext() && !flowControlPushingBack ) { + ByteBuffer buffer = iterator.next(); + boolean written = write( buffer ); + if ( written ) { + iterator.remove(); + } + else { + flowControlPushingBack = true; + } + } + if ( flushCurrentPage && !flowControlPushingBack && currentPage != null && currentPage.position() > 0 ) { + // The encoder still accepts some input, and we are allowed to flush the current page. Let's do. + currentPage.flip(); + boolean written = write( currentPage ); + if ( !written ) { + flowControlPushingBack = true; + needWritingPages.add( currentPage ); + } + currentPage = null; + } + } + + private boolean write(ByteBuffer buffer) throws IOException { + final int toWrite = buffer.remaining(); + // We should never do 0-length writes, see HSEARCH-2854 + if ( toWrite == 0 ) { + return true; + } + final int actuallyWritten = channel.write( buffer ); + return toWrite == actuallyWritten; + } + +} diff --git a/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/impl/ServerUris.java b/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/impl/ServerUris.java new file mode 100644 index 00000000000..8ca151e9709 --- /dev/null +++ b/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/impl/ServerUris.java @@ -0,0 +1,126 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.opensearch.impl; + +import java.net.URISyntaxException; +import java.util.List; +import java.util.Locale; +import java.util.Optional; + +import org.hibernate.search.backend.elasticsearch.client.common.cfg.ElasticsearchBackendClientCommonSettings; +import org.hibernate.search.backend.elasticsearch.client.common.logging.spi.ElasticsearchClientConfigurationLog; + +import org.apache.hc.core5.http.HttpHost; + + +final class ServerUris { + + private final HttpHost[] hosts; + private final boolean sslEnabled; + + private ServerUris(HttpHost[] hosts, boolean sslEnabled) { + this.hosts = hosts; + this.sslEnabled = sslEnabled; + } + + static ServerUris fromOptionalStrings(Optional protocol, Optional> hostAndPortStrings, + Optional> uris) { + if ( !uris.isPresent() ) { + String protocolValue = + ( protocol.isPresent() ) ? protocol.get() : ElasticsearchBackendClientCommonSettings.Defaults.PROTOCOL; + List hostAndPortValues = + ( hostAndPortStrings.isPresent() ) + ? hostAndPortStrings.get() + : ElasticsearchBackendClientCommonSettings.Defaults.HOSTS; + return fromStrings( protocolValue, hostAndPortValues ); + } + + if ( protocol.isPresent() ) { + throw ElasticsearchClientConfigurationLog.INSTANCE.uriAndProtocol( uris.get(), protocol.get() ); + } + + if ( hostAndPortStrings.isPresent() ) { + throw ElasticsearchClientConfigurationLog.INSTANCE.uriAndHosts( uris.get(), hostAndPortStrings.get() ); + } + + return fromStrings( uris.get() ); + } + + private static ServerUris fromStrings(List serverUrisStrings) { + if ( serverUrisStrings.isEmpty() ) { + throw ElasticsearchClientConfigurationLog.INSTANCE.emptyListOfUris(); + } + + HttpHost[] hosts = new HttpHost[serverUrisStrings.size()]; + Boolean https = null; + for ( int i = 0; i < serverUrisStrings.size(); ++i ) { + String uri = serverUrisStrings.get( i ); + try { + HttpHost host = HttpHost.create( uri ); + hosts[i] = host; + String scheme = host.getSchemeName(); + boolean currentHttps = "https".equals( scheme ); + if ( https == null ) { + https = currentHttps; + } + else if ( currentHttps != https ) { + throw ElasticsearchClientConfigurationLog.INSTANCE.differentProtocolsOnUris( serverUrisStrings ); + } + } + catch (URISyntaxException e) { + throw ElasticsearchClientConfigurationLog.INSTANCE.invalidUri( uri, e.getMessage(), e ); + } + } + + return new ServerUris( hosts, https ); + } + + private static ServerUris fromStrings(String protocol, List hostAndPortStrings) { + if ( hostAndPortStrings.isEmpty() ) { + throw ElasticsearchClientConfigurationLog.INSTANCE.emptyListOfHosts(); + } + + HttpHost[] hosts = new HttpHost[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 ) { + HttpHost host = createHttpHost( scheme, hostAndPortStrings.get( i ) ); + hosts[i] = host; + } + return new ServerUris( hosts, "https".equals( scheme ) ); + } + + private static HttpHost createHttpHost(String scheme, String hostAndPort) { + if ( hostAndPort.indexOf( "://" ) >= 0 ) { + throw ElasticsearchClientConfigurationLog.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 ElasticsearchClientConfigurationLog.INSTANCE.invalidHostAndPort( hostAndPort, e ); + } + host = hostAndPort.substring( 0, portIdx ); + } + return new HttpHost( scheme, host, port ); + } + + HttpHost[] asHostsArray() { + return hosts; + } + + boolean isSslEnabled() { + return sslEnabled; + } + +} diff --git a/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/logging/impl/ElasticsearchClientLog.java b/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/logging/impl/ElasticsearchClientLog.java new file mode 100644 index 00000000000..3ac0d6b0d03 --- /dev/null +++ b/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/logging/impl/ElasticsearchClientLog.java @@ -0,0 +1,29 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.opensearch.logging.impl; + +import java.lang.invoke.MethodHandles; + +import org.hibernate.search.util.common.logging.CategorizedLogger; +import org.hibernate.search.util.common.logging.impl.LoggerFactory; +import org.hibernate.search.util.common.logging.impl.MessageConstants; + +import org.jboss.logging.annotations.MessageLogger; + +@CategorizedLogger( + category = ElasticsearchClientLog.CATEGORY_NAME, + description = """ + Logs information on low-level Elasticsearch backend operations. + + + This may include warnings about misconfigured Elasticsearch REST clients or index operations. + """ +) +@MessageLogger(projectCode = MessageConstants.PROJECT_CODE) +public interface ElasticsearchClientLog { + String CATEGORY_NAME = "org.hibernate.search.elasticsearch.client"; + + ElasticsearchClientLog INSTANCE = LoggerFactory.make( ElasticsearchClientLog.class, CATEGORY_NAME, MethodHandles.lookup() ); + +} diff --git a/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/package-info.java b/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/package-info.java new file mode 100644 index 00000000000..c5c9f3c1d70 --- /dev/null +++ b/backend/elasticsearch-client/opensearch-rest-client/src/main/java/org/hibernate/search/backend/elasticsearch/client/opensearch/package-info.java @@ -0,0 +1 @@ +package org.hibernate.search.backend.elasticsearch.client.opensearch; diff --git a/backend/elasticsearch-client/opensearch-rest-client/src/main/resources/META-INF/services/org.hibernate.search.engine.environment.bean.spi.BeanConfigurer b/backend/elasticsearch-client/opensearch-rest-client/src/main/resources/META-INF/services/org.hibernate.search.engine.environment.bean.spi.BeanConfigurer new file mode 100644 index 00000000000..31236902b96 --- /dev/null +++ b/backend/elasticsearch-client/opensearch-rest-client/src/main/resources/META-INF/services/org.hibernate.search.engine.environment.bean.spi.BeanConfigurer @@ -0,0 +1 @@ +org.hibernate.search.backend.elasticsearch.client.opensearch.impl.ClientOpenSearchElasticsearchClientBeanConfigurer diff --git a/backend/elasticsearch-client/opensearch-rest-client/src/test/java/org/hibernate/search/backend/elasticsearch/client/opensearch/impl/GsonHttpEntityTest.java b/backend/elasticsearch-client/opensearch-rest-client/src/test/java/org/hibernate/search/backend/elasticsearch/client/opensearch/impl/GsonHttpEntityTest.java new file mode 100644 index 00000000000..1193b3ad3ad --- /dev/null +++ b/backend/elasticsearch-client/opensearch-rest-client/src/test/java/org/hibernate/search/backend/elasticsearch/client/opensearch/impl/GsonHttpEntityTest.java @@ -0,0 +1,343 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.opensearch.impl; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.Reader; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.hibernate.search.backend.elasticsearch.client.common.gson.spi.GsonProvider; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.nio.ContentEncoder; +import org.apache.hc.core5.http.nio.DataStreamChannel; + +class GsonHttpEntityTest { + + public static List params() { + List params = new ArrayList<>(); + Gson gson = GsonProvider.create( GsonBuilder::new, true ).getGson(); + + JsonObject bodyPart1 = JsonParser.parseString( "{ \"foo\": \"bar\" }" ).getAsJsonObject(); + JsonObject bodyPart2 = JsonParser.parseString( "{ \"foobar\": 235 }" ).getAsJsonObject(); + JsonObject bodyPart3 = JsonParser.parseString( "{ \"obj1\": " + bodyPart1.toString() + + ", \"obj2\": " + bodyPart2.toString() + "}" ).getAsJsonObject(); + + for ( List jsonObjects : Arrays.>asList( + Collections.emptyList(), + Collections.singletonList( bodyPart1 ), + Collections.singletonList( bodyPart2 ), + Collections.singletonList( bodyPart3 ), + Arrays.asList( bodyPart1, bodyPart2, bodyPart3 ), + Arrays.asList( bodyPart3, bodyPart2, bodyPart1 ) + ) ) { + params.add( Arguments.of( jsonObjects.toString(), jsonObjects ) ); + } + params.add( Arguments.of( + "50 small objects", + Stream.generate( () -> Arrays.asList( bodyPart1, bodyPart2, bodyPart3 ) ) + .flatMap( List::stream ).limit( 50 ).collect( Collectors.toList() ) + ) ); + params.add( Arguments.of( + "200 small objects", + Stream.generate( () -> Arrays.asList( bodyPart1, bodyPart2, bodyPart3 ) ) + .flatMap( List::stream ).limit( 200 ).collect( Collectors.toList() ) + ) ); + params.add( Arguments.of( + "10,000 small objects", + Stream.generate( () -> Arrays.asList( bodyPart1, bodyPart2, bodyPart3 ) ) + .flatMap( List::stream ).limit( 10_000 ).collect( Collectors.toList() ) + ) ); + params.add( Arguments.of( + "200 large objects", + Stream.generate( () -> { + // Generate one large object + JsonObject object = new JsonObject(); + JsonArray array = new JsonArray(); + object.add( "array", array ); + Stream.generate( () -> Arrays.asList( bodyPart1, bodyPart2, bodyPart3 ) ) + .flatMap( List::stream ).limit( 1_000 ).forEach( array::add ); + return object; + } ) + // Reproduce the large object multiple times + .limit( 200 ).collect( Collectors.toList() ) + ) ); + params.add( Arguments.of( + "1 very large object", + Stream.generate( () -> { + JsonObject object = new JsonObject(); + JsonArray array = new JsonArray(); + object.add( "array", array ); + Stream.generate( () -> Arrays.asList( bodyPart1, bodyPart2, bodyPart3 ) ) + .flatMap( List::stream ).limit( 100_000 ).forEach( array::add ); + return object; + } ) + // Reproduce the large object multiple times + .limit( 1 ).collect( Collectors.toList() ) + ) ); + + params.add( Arguments.of( + "Reproducer for HSEARCH-4239", + // Yes these objects are weird, but then this is a weird edge-case. + Arrays.asList( + gson.fromJson( "{\"index\":{\"_index\":\"indexname-write\",\"_id\":\"0\"}}", JsonObject.class ), + gson.fromJson( "{\"content\":\"..........................................................." + + "...........—.....................—.......—....—....................................." + + "...—.....................................................................——........." + + "...................................................................................." + + "........................—..........................................................." + + "...................................................................................." + + "...........................................—........................................" + + "............—............—.........................................................." + + "...................................................................................." + + "................................................................——.....—............" + + ".................—.................................................................." + + ".............................——....................................................." + + "..........................................\",\"_entity_type\":\"indexNameType\"}", + JsonObject.class ) + ) + ) ); + + params.add( Arguments.of( + "Reproducer for HSEARCH-4254", + Arrays.asList( + gson.fromJson( + // This is randomly generated content that happens to reproduce the problem. + // If it means anything, that's purely coincidental. + // I'm sorry if it's offensive, + // but if I change even one character the problem no longer appears, + // so I'm stuck with this content. + "{\"content\":\"\u010D\uFC7A\uCBED\uD857\uDFC9\uB5FA\u17B2\uD83E\uDD21\u15C5\uFF3D\uD397\u3F7B\u7822\uD84F\uDDB2\uD859\uDD21\uD871\uDFCC\uD852\uDDC3\uD848\uDCFD\uD85E\uDEAF\uD81E\uDD10\uD855\uDDF1\u2D67\uD859\uDC62\uD85C\uDC89\u7D75\uD82F\uDC09\uD800\uDFAA\uD803\uDC35\u1BE9\u4183\u068A\uC61E\uD85D\uDDC7\uD3CC\uD873\uDFD6\u3DA6\uD842\uDE91\uD85A\uDC3D\uD865\uDCF0\uD81C\uDDEE\u718A\uD84D\uDEB3\uC2C2\uD86C\uDF47\u8F9D\u4801\uD84C\uDED9\uD86B\uDE9C\uC759\uD862\uDF42\uCE83\u7B02\u526D\u978E\u3936\u7C94\u6746\uD866\uDF98\uD844\uDFC8\uD851\uDF8E\u4A98\u789E\uD85B\uDF01\uD855\uDE5B\uD87A\uDF2F\uD869\uDF29\u2AFF\uD821\uDD33\uD86C\uDE14\uD87E\uDCD7\uFF1F\uD853\uDEA4\uD847\uDD4B\uD85A\uDD5D\uD81A\uDCF1\u7F23\u7A6C\uD848\uDC48\uD84D\uDD06\uD821\uDED2\u2ED6\uD83E\uDD56\u591B\uD84C\uDD84\uD85D\uDE1B\uD877\uDEFE\uC624\u6DEA\u73BC\uD821\uDE30\u23A7\uD7DB\uD862\uDD93\u3B3C\u0ED4\uD846\uDC7B\u0959\uD86D\uDE2A\uD86B\uDC2B\uD86D\uDE87\u76A3\uA10F\uD872\uDEB1\uA604\uA852\uD1D5\uD856\uDEBD\uD844\uDD82\uD820\uDEB4\\u0006\uD81E\uDF6F\uD870\uDD2D\uD856\uDF4E\uD875\uDE78\uB7DB\uCF98\uD856\uDEAF\uD87A\uDEC9\u04A8\uD848\uDFEF\uD852\uDE49\uC933\u12AE\uD84F\uDC63\uD850\uDECE\uD874\uDDAC\uD872\uDC7D\u7F76\uD869\uDE37\uD2A0\uD4C7\uD868\uDDBC\uD848\uDC31\u86EA\u7276\u48B9\u8412\uB216\uD848\uDF0D\u7102\uD869\uDC63\u9018\u2FFA\uD844\uDD1E\u91DD\uFAA3\uD877\uDF17\u8617\uD821\uDEC9\uD860\uDCA7\u66B1\uD808\uDCF5\uD807\uDC85\u483B\u1CE0\u7DF3\uD878\uDDA4\u14BA\u3558\uB82D\uD845\uDE49\uD871\uDEFC\uD87E\uDDCC\uA1C2\uB195\uD874\uDE28\u3AD4\uD87A\uDF69\u12E4\u6787\uD850\uDC86\u414D\uD84B\uDCFC\u14DB\u3259\uD85A\uDD6B\u15A8\uD87E\uDCB3\uD868\uDE40\uD81D\uDF4B\uD821\uDDAF\uD81F\uDC9E\u1BC8\uD805\uDE1A\uD84B\uDE33\uC4E3\u9D76\uD849\uDEA5\u1230\u9DE0\u2F65\u2BBD\u604E\uD848\uDC6B\uD835\uDF6B\u27E6\uD846\uDF6C\u77AB\uD865\uDD55\u15EC\u380D\uD857\uDEA7\uD859\uDEA3\uD841\uDC58\uD86D\uDC68\uD85B\uDCCC\uD864\uDE04\uD874\uDEEB\uD879\uDC9A\uD874\uDFF5\uD83C\uDF9A\uD879\uDD5E\u4C5C\uD80C\uDE45\uCAB3\uD81F\uDFFB\uD81F\uDDC1\uD844\uDF64\u3DB6\uD862\uDF7D\uD870\uDC1B\uD853\uDC91\uD864\uDE99\uD845\uDD16\uD84D\uDCC5\uD870\uDF76\uD86C\uDCA2\uD821\uDC02\uD820\uDFF9\uD865\uDF2C\u75B5\u9AA4\uD87A\uDDCE\u3004\u8355\uA2D8\uD84B\uDCED\uD809\uDD01\u763F\uD845\uDE39\u610B\uD84A\uDDD8\u89DC\uD842\uDE17\u9797\uD842\uDFC2\uD83E\uDC70\uD804\uDD72\u732E\uD86C\uDEA0\uAE24\uD822\uDD4F\uD86A\uDDF1\uD847\uDDA7\uD876\uDDB9\uD85F\uDC41\u6428\uD85A\uDCE4\uD84F\uDE6C\uD868\uDE62\uD860\uDEDC\uD83E\uDC05\uD83A\uDC3A\uC710\u332D\uD17F\u3CBF\uC2C0\uD86A\uDE20\uD858\uDCB3\uD858\uDDDE\uD855\uDFF6\u5E77\uD860\uDD85\u16D8\uD867\uDCD2\uD84E\uDFB1\u3978\uD853\uDD69\u07D4\uD874\uDC5B\uD84F\uDFDC\uD86A\uDF87\uCA0C\u3FE1\uD84A\uDF3F\u2EEA\uD821\uDDA7\uAADD\uD80C\uDF61\uA8D3\uD860\uDFFB\u1386\uD86B\uDE4B\uD874\uDF01\uD855\uDE10\u104A\u8C65\uD85E\uDEC4\uD85E\uDD20\u810A\uD81A\uDCD0\u2B3D\uD842\uDEFF\uD821\uDC28\uD877\uDE3E\uD856\uDE83\uD854\uDFC9\uD87A\uDE84\u01D4\uD85F\uDED8\uC53E\uD841\uDF04\uD857\uDE31\uCDB9\uD877\uDF5A\u99E2\uC3A4\uD83E\uDD2E\u1BA9\uD852\uDD5B\uD848\uDE9D\uD870\uDED3\uD849\uDD9F\u3D3F\uD857\uDEB0\u4193\u053A\u5B90\uD852\uDFBA\uD878\uDE00\u738C\uD878\uDF7E\uD868\uDF86\u7A08\uD868\uDFF1\uD81A\uDF1A\uD80C\uDD52\uD85C\uDE22\uD870\uDD12\uD81C\uDC35\uD871\uDD2C\uD877\uDE45\uD84F\uDD4B\u37DB\uD86C\uDC11\u9E19\uD85B\uDDA7\uD84D\uDEE8\uD86F\uDF6D\uD86A\uDDFC\u4CD5\u4459\uD85A\uDD6E\uD844\uDF3C\uCB39\uD809\uDD36\uA240\uD84B\uDC56\uD847\uDD46\uD852\uDD78\uD84A\uDC25\u316E\uD85C\uDF62\uD86E\uDD3E\u720D\u3C94\u99ED\uD489\u66A3\u4325\u663D\uB04B\u60D3\uCFB2\uD840\uDEF8\uD83A\uDCAE\uD843\uDCC0\u97C6\uD84D\uDC8E\uD858\uDCEF\uD87A\uDE11\uD84D\uDC23\uD86F\uDCE1\uD875\uDF57\uD84F\uDD00\u9AAA\uD835\uDF15\uD84E\uDC0E\uD822\uDC79\uFC38\u6D95\uD876\uDE83\uD84B\uDEAB\uC7A6\uD849\uDEA8\uD866\uDCE5\uD836\uDD60\uCD9E\uD802\uDC78\uD867\uDEFA\uD800\uDCE1\uD112\uD804\uDE23\u74A7\uD86A\uDC5F\u8E99\uD811\uDDFA\uD848\uDCF0\uD875\uDC60\u38D9\uD808\uDD5A\uD820\uDF59\uD86E\uDDD2\u95A5\uA72E\u2756\uD845\uDF91\u610C\uD820\uDEF5\u6F33\uD868\uDFE3\uD859\uDE88\uD874\uDC80\uD85D\uDE43\uD85E\uDD15\uD875\uDEAE\u8DD8\u8BAD\u5D7E\u3491\uD858\uDE1E\u2A4F\u95EE\uD806\uDE5F\u2995\uD869\uDDDE\uD81A\uDF74\uFDB4\u8EAD\uD802\uDDE3\uD841\uDFA9\u9B32\u0F03\uD863\uDFC3\u20E6\uD85E\uDDB0\u928E\uB839\uD86C\uDDC7\uD840\uDDFF\uD870\uDED5\uD873\uDC91\uD850\uDECB\uD84A\uDD73\uD854\uDCFB\uBB90\uBA0D\uD834\uDD2F\u38CB\uD805\uDC83\u5AB3\uD847\uDCC4\uD85B\uDC14\u73A1\u13CE\uD846\uDF08\u136E\uA032\uF956\uA1B9\uD820\uDD41\uD85E\uDDC3\uD870\uDEB0\uD86D\uDC05\uD86F\uDECB\uD85E\uDF16\uD802\uDC1E\uC74E\u581C\uD808\uDF01\u5991\uD805\uDE0D\uD86F\uDE88\uD851\uDC40\uD86A\uDD24\uD865\uDFA8\u6DE1\uD804\uDCF4\uD84E\uDE5A\uD86E\uDFB3\u4F8E\u7F02\uD84F\uDCD1\uD863\uDC15\uD859\uDC82\uD834\uDE40\uD863\uDF4D\uD86C\uDC3A\uC599\uD01E\u82DB\uD871\uDF39\uD840\uDF44\uD81F\uDCA1\uD85E\uDDEE\u9745\uD845\uDC31\uD856\uDD79\uAB72\uD66D\uB29F\u37B0\u96DD\uB781\uD85A\uDF54\uCD02\uD86C\uDE6E\u2CFF\u367D\u7DA2\uD854\uDE05\u7D83\uD808\uDE70\uD85A\uDE12\uD853\uDD97\u2ACC\uD85C\uDEE5\uD872\uDE5C\uD85A\uDEBA\uD875\uDC4F\u7008\uD867\uDFC3\u1C13\u9888\u9DCA\u9FE9\uD84D\uDCA8\u5530\uD855\uDEE2\uD841\uDD5D\uD877\uDE59\uD867\uDD7A\uD85C\uDF97\uD80C\uDDD3\uD847\uDF4A\uD876\uDF1A\uC279\uD874\uDF6F\u1339\uD86A\uDC20\uD841\uDFAB\u09CC\uD811\uDC98\u4228\u5DCF\u2A82\uD86B\uDCF4\u069F\uD860\uDF40\u93F0\uD808\uDF0B\uD841\uDC86\uD870\uDD41\u610B\u63EE\uD81A\uDCBE\u9A85\uD809\uDC91\uD852\uDDAA\u6974\uD847\uDDA3\uD85B\uDDBA\uD840\uDE4C\u56BA\uD876\uDED3\uD867\uDDF8\uD808\uDC2F\uD867\uDC05\uD84B\uDC14\uD867\uDDB4\u4DF0\u82E6\uD845\uDEE2\uD81C\uDE14\uD802\uDD83\uBC7E\uD85D\uDC2C\uD846\uDD1E\uD873\uDCCC\uD860\uDC8A\uD878\uDC92\uD874\uDE65\uD861\uDC28\uD854\uDC3E\uD84C\uDC11\uD856\uDD11\uD81D\uDEB0\uB07A\u89A4\uD85A\uDED9\uD81C\uDEE2\uD805\uDC0D\uD834\uDC22\uD864\uDC4F\u240B\uD86A\uDE80\uD876\uDFC4\uD85E\uDCFF\uD871\uDE7C\u1987\uD835\uDC8F\u1990\uD863\uDFE1\uD81A\uDF26\uD802\uDC0E\uD853\uDE0B\uD841\uDE95\u59E7\u1E02\uD864\uDF58\uD877\uDEE1\uD82C\uDEC0\uD86B\uDD8A\u391D\u03BB\uD863\uDE29\uD84B\uDEDB\u8F02\uD2A0\u22ED\uD863\uDF74\u70A9\uD856\uDE3A\uBC76\uD861\uDD78\uD868\uDD9C\uD851\uDFF6\u8860\uD841\uDD85\uFB84\u110B\uD850\uDCD4\uD875\uDE6D\uD845\uDD20\uDB40\uDD9D\uD853\uDD7A\uD841\uDEA3\u0D05\uD869\uDDC0\uD856\uDDA8\uD870\uDD1F\uD86C\uDCF9\uD80C\uDF2A\uD804\uDCD4\uD85B\uDDF5\u9EF2\uD844\uDF27\u8536\uD85B\uDFE7\u581F\u6C39\uD856\uDE79\uA50D\uD864\uDCD9\uD879\uDEF7\uD841\uDE38\u3D49\u1B50\uD807\uDC8D\uD811\uDDAF\u82CC\u3F64\u851E\uD847\uDF0B\u0923\u3000\uD85A\uDE7E\uAC0D\u9344\uD801\uDEE5\u02EF\uD0D1\uD877\uDEDD\uD861\uDE7E\u9910\uD86B\uDEC1\uD85D\uDF3F\u6CBF\u27B2\u7869\uD822\uDC60\uD57E\uD860\uDD92\uD85F\uDCC0\uD86E\uDD44\uD872\uDE59\uD81D\uDE21\u956A\uD85F\uDD7A\u3A62\u95F5\uD878\uDF26\u5F0E\u2934\uD82C\uDC91\uD862\uDDA7\uD848\uDFD4\uD81D\uDE6F\uD86A\uDD5B\uD845\uDF26\u9C4F\uD82C\uDEBF\uD87A\uDE9B\uD849\uDC8A\u13D8\uD248\uBF92\uD853\uDDD6\uD862\uDEEE\uD7DA\uD863\uDCB6\uD860\uDDE9\uD859\uDCD6\uD856\uDCE8\u7612\uD84B\uDE38\uD82C\uDD70\uD85B\uDF2B\uD869\uDC4A\u396C\u7728\u41F4\uD859\uDC5D\uD83E\uDC3E\u648C\uD877\uDD93\u7358\uD845\uDE51\uD857\uDF95\uD870\uDE49\u93D4\uBDF1\u5E2E\u8197\u24E3\uD856\uDE54\u55CF\uD81F\uDF7A\uB6AC\uD865\uDF8B\uD81F\uDDD8\u6F19\uD84B\uDC82\uD867\uDD94\uB377\uD847\uDD74\uD875\uDFC4\uD86E\uDEF9\uD87A\uDD0C\u25C6\uD877\uDCF0\uD864\uDEE8\uD804\uDDC0\uD359\uD835\uDFC0\u813D\u2AC1\uD86C\uDD80\uD807\uDC58\u3750\uD805\uDCC7\u51D0\uD845\uDC5D\uD847\uDEB2\uD821\uDD63\uD869\uDE68\uD811\uDD5F\u76C0\uB127\u9D0D\u88DE\u163E\uD865\uDC75\uD85F\uDE50\uD857\uDF48\uD865\uDF68\uD87A\uDFCE\uD802\uDC89\uD86E\uDDE2\uD808\uDF58\uD850\uDC8F\u0714\uC90D\uD845\uDF49\u8EC9\uD856\uDE91\uD804\uDC61\uD847\uDE44\uD5FA\uD811\uDDB6\uD840\uDE71\uD38F\uFF3B\uD866\uDF1B\uD845\uDF61\uD871\uDFC5\u858A\uA12D\u53A8\uD820\uDF73\uD843\uDCCE\uD857\uDC68\uD84E\uDD33\uD867\uDC17\u26D1\u3573\u5701\u7BE5\uD870\uDF52\uD853\uDECD\u427E\uD873\uDF75\u51ED\uD857\uDF19\uD852\uDF3D\uD85F\uDC4E\uD84C\uDC13\uD842\uDDDE\uD868\uDDB4\uD871\uDEC5\uD871\uDC24\u8C19\uD85A\uDEF1\uD863\uDDDE\u2967\uD86A\uDEA6\uD857\uDC74\uD85B\uDF23\u3294\u7DC1\u7A9C\u7B1F\uD83B\uDE68\uD86D\uDCFC\uD80C\uDE3F\u2880\u488F\uD84C\uDE42\u70D2\uD861\uDC7D\uD821\uDFE3\uD848\uDEBD\uD841\uDD80\u69B6\uD848\uDEB5\u916E\uD803\uDC36\uD84E\uDE70\uD86E\uDC95\u0438\u95FC\uD870\uDE9A\uD7C5\uD875\uDF29\uCB39\uD860\uDDAF\uD86A\uDCBA\u1321\uD811\uDDD7\uD871\uDF2E\u35DF\uD873\uDCBB\u5373\u9A9A\u3055\uD84E\uDE62\uD85E\uDE7E\u7467\uB87D\uC42C\uD82C\uDD91\uD857\uDD05\uD84A\uDF2D\u9A18\uD81F\uDD42\u50FA\u8C62\uD84A\uDC70\u49AD\uD871\uDFD9\uD850\uDEA3\u3AF4\u198C\uB6FF\uA697\uD855\uDFEC\uD81D\uDD1A\uD878\uDC50\u9F9C\u1D34\u4FAF\uDB40\uDC64\u75C1\uD835\uDDF1\uD854\uDCEA\uD842\uDFC7\uD853\uDFD1\uD81C\uDD99\uD874\uDD2F\uBC5A\uD873\uDE4F\uD872\uDF8E\uD852\uDDF1\uD85F\uDC4C\uD842\uDF01\uD86D\uDD9A\uD84F\uDC70\uD867\uDD15\u0788\uD820\uDE7D\uDB40\uDDD3\uD82C\uDCCF\uD871\uDC2D\uD834\uDCD0\u9669\uD877\uDE12\uD872\uDDAB\uD843\uDC43\uD84E\uDC8D\u5840\uD840\uDD5D\uD858\uDFF2\uD855\uDF37\uB5A5\uD821\uDE6E\uD805\uDDCF\uD7DB\uD86A\uDC9F\uD861\uDC05\uD81E\uDE8B\u0718\uD852\uDEFE\uD80C\uDE1A\uD83D\uDC0D\uD86E\uDFBC\uD834\uDDCA\uFA86\u2E04\u0568\uC32B\uD871\uDE34\uD854\uDEC4\uD867\uDC93\uD85E\uDD31\u46A7\u8205\uD843\uDE2A\u0771\uD83C\uDE38\uD808\uDDD3\uD867\uDC11\uFD9F\uD84F\uDF7B\u552F\uA586\uD82F\uDC28\uD87A\uDCA9\u32B7\u39A4\uD802\uDC0F\uD806\uDE8B\u5E70\uBCA8\uD81D\uDCA2\uD840\uDC46\u8F5D\uD83D\uDF70\u47EF\uD85F\uDD3D\uA34E\uFE33\uD872\uDF71\u49D7\uD862\uDD8F\uD81D\uDE4A\uAF06\uCF0E\uD862\uDF2A\uD86E\uDE3B\uD867\uDE05\uD772\uD856\uDEC3\uD86F\uDC2A\uD878\uDCED\uD84C\uDF2E\u83F7\uD866\uDDD0\uD800\uDFB3\u33D4\uD81C\uDEAA\u0653\u7E17\uD858\uDEAB\u70BB\uD821\uDE30\uD85C\uDC09\uD856\uDC6E\u8D9E\uD854\uDF1D\uD86D\uDFF1\uAB78\u8511\uD858\uDE6C\uD820\uDE40\uBC40\u6F4C\u7F3E\uD858\uDE13\uC515\uB0D4\uD842\uDD61\uD852\uDFD8\uD821\uDCD1\uD025\uD858\uDF86\uD875\uDD75\uD86B\uDF7E\uCC14\u8184\uD855\uDF8C\uD844\uDF84\u486F\u7C5F\u607F\uD871\uDF9F\uD85D\uDCBE\u35A4\uD867\uDD8B\uD821\uDE68\uD84E\uDEC4\uC454\uD836\uDCFE\u41CE\uD861\uDFFE\uD852\uDD45\uD821\uDE08\u6B23\uD834\uDD8F\u1A62\uD843\uDF2D\uD864\uDD6A\uD871\uDD5E\uBA1E\uD802\uDF86\uD879\uDD5A\uD840\uDF1E\uD806\uDE00\u2954\uD86D\uDC96\u69A7\uD873\uDC6B\u97C4\uD862\uDE7C\uD872\uDD8E\u4655\uD871\uDC87\uD852\uDECD\uD80C\uDE09\uD84F\uDE14\uD80C\uDC55\uA462\uD872\uDCA9\u8FF6\u112F\uD867\uDDC1\uD874\uDDE7\uB42B\uD84E\uDCA6\uD87A\uDD28\uD876\uDE08\uD844\uDC59\u3339\uD2B2\u1846\uD84F\uDF01\uD843\uDC35\uD844\uDFCF\u3454\uD834\uDF19\uD864\uDFE1\uD81D\uDDDF\uD811\uDD67\uD875\uDC96\u249A\uD802\uDC55\uB8CA\u36E9\uD85D\uDEC2\u554B\uD867\uDEE7\u51AA\uD864\uDC4E\uFDAF\uD869\uDC06\uD854\uDC33\uADF4\u961C\uD861\uDF19\uD869\uDCDB\uD845\uDE48\uD800\uDF5D\uD853\uDFB7\uD86A\uDF06\uD875\uDD36\u7FF0\u5E62\uD865\uDD3A\uD860\uDFAC\uD821\uDEB3\uD862\uDCDC\uBEAA\uD87A\uDEBC\uD811\uDCDC\u85E4\u0361\u2034\u7908\uD845\uDD86\uD874\uDF9A\uD863\uDE70\u7BEE\uD85A\uDFE5\uD836\uDD4F\uC8AB\uD859\uDD0B\u4E29\uD86A\uDC59\uD83D\uDFC6\uD853\uDE9A\uD859\uDC89\u8F4E\u7B05\uD856\uDE6F\uD801\uDEA0\uD861\uDCA2\uA9CC\uD866\uDCAB\uD804\uDE29\uD845\uDF88\u4E60\u2C88\u6647\u7778\uA4AE\uD848\uDFB4\uD872\uDFCC\uD840\uDC2F\uD836\uDC88\uD873\uDDAE\uD849\uDC79\u560F\uD850\uDF73\uD85C\uDF65\uD135\u3BEE\uD84F\uDE98\uD848\uDEAC\uD854\uDD9A\uD877\uDCA7\uD84D\uDE3C\uCBBF\u4540\uD86E\uDD73\u64A0\uD836\uDC2D\uD861\uDF2D\uD81D\uDF78\uD867\uDC15\u250C\uD85F\uDDF0\u2FA3\uBF93\uD870\uDDAA\uD83C\uDCF3\uD86C\uDDE2\uD857\uDF46\uD852\uDE3D\uD868\uDFAC\uD846\uDDCC\uD81E\uDC20\uD86D\uDCFC\u1BB4\uB299\uD820\uDD71\uD867\uDFF3\uD840\uDF70\uD868\uDE0C\uD850\uDCE4\uD868\uDE55\uD85B\uDD96\u7655\uD80C\uDDBA\uD84C\uDF25\uD85C\uDD4B\uD861\uDF4C\uD84E\uDC4F\uD861\uDD1D\uD86E\uDD52\uD802\uDD9F\uD844\uDEB3\u6A9E\uD86F\uDFA0\uB34B\uD80C\uDF65\uD866\uDDF2\uD85F\uDCCA\uB9F3\uD85E\uDE08\uD84E\uDD9E\u183B\uD849\uDF5E\uD85A\uDC39\uD84A\uDD4D\uD852\uDC47\u6858\u011F\uD875\uDF8A\u84DA\u368B\uD876\uDE95\uD81F\uDEFE\uD87A\uDDE4\uA756\u037D\uD855\uDC7A\uD867\uDF03\uAFFC\u5FE2\uD86D\uDC47\uD872\uDE7C\uD856\uDD97\u3AE1\uD874\uDD8C\uD861\uDE70\uD860\uDC9D\uD698\uD81E\uDDF3\u35F0\u93C3\u2793\uD836\uDDBC\uD861\uDD09\uD85E\uDEEE\u9AEF\u1A16\uD849\uDE6D\u813F\uBA8B\uD821\uDC2E\uC8BE\uD83D\uDE4B\u833C\uD855\uDC8F\uD820\uDC58\u1ECD\u5ED5\uD85D\uDC49\u41F2\uD856\uDCBA\u4B3C\u246D\uD81D\uDD4C\u8A06\uD86F\uDD04\u19F6\uD85D\uDDB3\uD82C\uDE45\uD845\uDE39\uD846\uDD6C\u9D84\u1FBF\uD81C\uDF8B\uD872\uDE66\uD848\uDE60\uB7D0\uD859\uDC7A\uD822\uDC9B\u6337\uD855\uDD30\u4E11\u11DA\uD87A\uDCAD\uD811\uDCDE\uD83E\uDC94\uD6EC\u291D\uD81F\uDDE2\uD853\uDFE5\uD856\uDF8A\u4380\u26C1\uD848\uDED7\uD840\uDED8\u9B25\uD873\uDD9F\u302E\uD856\uDEB6\u8A9C\u8AE4\u9243\uD822\uDC9B\uD857\uDD59\uD86A\uDF91\u93BA\uD858\uDCC3\uD85E\uDD7D\uD873\uDCC8\uD85B\uDE59\uD855\uDD6E\\u2029\uD849\uDCB4\uD859\uDFF4\uD81C\uDC89\uD840\uDE15\u16E1\uD834\uDDA4\u689C\u1C70\uD85C\uDFAF\uD84C\uDE96\u6A60\uD851\uDD68\u3783\uD873\uDC9E\u35E1\u8C93\u1B6C\uD821\uDDC1\uC5E0\uDB40\uDD46\uD844\uDC60\u72FC\uD811\uDCCB\uD2F6\uD850\uDE8E\uF992\uD7E2\uB5B6\uD843\uDD4A\uD874\uDC3B\u40E4\u4F3A\u2B63\uD841\uDC98\u90A5\uD878\uDC20\uD821\uDE10\uD81A\uDF0C\uD853\uDF8C\uD836\uDE82\uD877\uDF38\uD867\uDFAF\uD846\uDC72\u2EEB\uD858\uDC64\uD851\uDC29\uD85B\uDCEB\uD851\uDD90\u50B0\uCF83\uD858\uDC19\uD855\uDE6C\uD84F\uDC96\uD878\uDF50\uFEA1\uD873\uDDA8\u0B71\uD820\uDE87\uD866\uDC3C\uD84E\uDF1B\uD855\uDEC8\uD875\uDDCB\u112E\u9220\uD863\uDD49\uB12C\uD84E\uDC02\uD865\uDC4B\uD869\uDF09\u936C\uD800\uDEBF\uD1FD\uD843\uDE31\u4D72\uA279\uD86A\uDC28\u8D16\uD858\uDEAF\u64A7\u100F\uA926\uD85A\uDC90\u86CF\u0664\u9DF3\u3D52\u22AF\uD86B\uDF1E\uD804\uDE80\uCEB3\u6265\u7519\uD81F\uDE52\uC4DB\uD865\uDD91\uD875\uDD25\uD867\uDE8E\uD85A\uDCEC\u67ED\uD857\uDC30\uD849\uDC78\uD841\uDC19\uD874\uDE96\uD841\uDEB1\uD874\uDE7D\uB05F\uD86A\uDC8D\uC01B\uA1E5\uD879\uDC39\uD861\uDC27\uD84D\uDD46\uBD5B\u6619\u7A05\uD851\uDEEB\uD835\uDF3C\uD81E\uDEB9\uD879\uDD0B\u092D\uD874\uDC91\uD864\uDFD6\uD848\uDEBC\uD847\uDDE8\u47BC\uD84B\uDC3F\u503D\uD862\uDF7A\uD851\uDC3E\u36B0\u7459\uA742\uD859\uDD91\uD85A\uDE3E\uC59C\uAAE6\u877C\uD840\uDCB4\uD859\uDC99\uD805\uDF1D\u98F8\uC819\uD85E\uDE2E\uD850\uDFB3\uD874\uDF7C\uD850\uDE51\u7484\uD873\uDCAA\uD857\uDC15\uD84C\uDCDF\u4B44\uD855\uDD04\uD863\uDED1\u47F0\uD858\uDF64\u01C4\u1504\uCF5C\u2E05\uD859\uDFE9\uD843\uDE85\uD846\uDE59\uD848\uDEB1\uD86F\uDEB0\uD873\uDF43\uA8A6\uD842\uDF95\uD878\uDC79\uD877\uDF0F\u4BB8\u663B\uA245\u5299\uD86A\uDF07\uD843\uDF2E\uD84D\uDE1F\u35DD\u87DF\uD83C\uDDF1\u79E4\uD84B\uDD87\uD84D\uDFF4\uD84B\uDE31\uD81C\uDC5B\uD855\uDE1C\u9AA0\u88BB\uD862\uDE94\u6BB7\uD86D\uDDC5\uD869\uDE77\u2D0C\uD801\uDD52\u300A\uD81C\uDE01\uD841\uDE12\uD81F\uDCB2\uD86E\uDDF0\uD820\uDC27\uD860\uDF12\u3C58\uD855\uDDE0\uD870\uDD44\u66BA\uD811\uDE32\uD878\uDC7C\uFF6B\uD846\uDE61\u8F4A\uD84B\uDEC8\uD865\uDF83\u8623\u8499\uD873\uDF41\u62B7\u61AF\uCFD3\uD843\uDC92\u33E5\uD85E\uDFD3\uD86B\uDF32\uD85E\uDF40\u4F9E\uD81E\uDF0F\uD847\uDE10\u6D8F\uD85F\uDFA0\uD87E\uDD2C\u13B3\uB70F\uD872\uDE3C\uD802\uDEC8\uD806\uDCE4\uD848\uDE9D\uD800\uDF30\u83FC\u776A\u2402\uD843\uDF67\uD867\uDCA8\uB6F7\u833E\uD820\uDFCE\uD87A\uDCF6\uD835\uDEED\uB3A3\uBDDE\uD851\uDF4E\uD867\uDF7A\uD84C\uDCDA\uC01B\uD879\uDD17\uD84F\uDDF9\uD878\uDE74\uD855\uDFA0\u2608\u30AF\uD859\uDDAE\uD874\uDDC1\u659E\uD873\uDF8B\uBA09\uD873\uDE87\uD879\uDD29\uD874\uDDC4\uD875\uDC3E\uFBFC\u047C\u5C00\u3EED\u5613\u2D07\uFF8D\u8AC4\uD855\uDDFD\uA153\uB518\u2B91\uD809\uDC6C\uD86A\uDF17\uD1F9\uD802\uDC2C\uD861\uDDD3\uFF38\uD80C\uDD65\uB375\uBE10\uD869\uDF49\uD862\uDDB0\uD86B\uDD7F\uD85E\uDEEE\uD874\uDF44\uD85E\uDD38\u1929\uD872\uDF71\uD855\uDC49\uAD04\uCCED\u3FB7\u1B03\u55E8\uD84E\uDE46\uD853\uDEFB\uD870\uDE40\uD849\uDFAF\uD875\uDF65\u07AF\uD863\uDFE6\uD802\uDEEE\uB8D1\u49E8\uD82C\uDD17\u5104\uD875\uDDAC\uD86B\uDDF6\uD846\uDF30\uD86E\uDFAA\u7BDE\u581A\uD81A\uDDBA\u7017\u2A45\uAC50\uD86D\uDDED\u6562\uD81D\uDF9D\uD84D\uDEFE\u9F00\uA566\uFCEF\u4B7C\u511B\u1C71\u8B8C\uD870\uDD18\uD81F\uDCEB\uD836\uDD4A\u6483\uD869\uDC59\uD86E\uDFC7\uD80C\uDD92\uD510\uD85C\uDFED\uA048\uD862\uDF75\uD877\uDDBB\uD865\uDEA8\uD867\uDF02\uD841\uDDAB\uD86B\uDEB6\uB009\u69E6\uD81C\uDDBA\uD844\uDD69\uD840\uDF41\uD811\uDCB5\uD85E\uDF5B\u78EA\uD86E\uDC52\uD84E\uDE70\uD879\uDDBB\uD10D\uC930\uD769\uA0DE\uD860\uDD27\uD84A\uDC4A\uD834\uDDCF\u8BE6\uBEAD\uD81D\uDE16\uD820\uDF52\uD80C\uDFB1\uD84C\uDDD6\uD870\uDCAF\u8886\uA105\uD808\uDCA9\u6758\uD879\uDF66\u4595\uD86C\uDC68\uD841\uDEC7\uD862\uDD65\u8A16\u712F\uC730\u5A0F\uD864\uDD8E\uD857\uDD5C\uD864\uDEA1\uD878\uDE43\uD87A\uDE0F\uD82F\uDC16\uD870\uDC99\uD800\uDC2E\u738B\u137C\u4EEA\uD852\uDD2E\uD860\uDEF3\uD876\uDC6D\uD802\uDEE2\uD804\uDDC6\u37DB\u7AF4\uD86F\uDDBD\u64FF\u056A\uD84F\uDEE2\uC93E\uD869\uDED5\uD800\uDF3F\u7F4A\u6FB4\uD861\uDF0C\uD846\uDE2A\u6641\u47AA\uD81D\uDCFB\u7419\uD84A\uDE98\uD809\uDC53\u6310\uD845\uDDBF\uD873\uDD5F\uD844\uDDD7\u3595\uD841\uDD4B\uD840\uDCFC\uD846\uDC1E\uB128\uD85A\uDD04\u4B93\uD87A\uDEF0\uD86F\uDF70\uD854\uDE47\uD808\uDF2A\uD836\uDDD2\uD878\uDF70\uD2CC\u9BFB\uD873\uDF95\uD81C\uDD3E\u8AC6\uD85E\uDCE1\u5369\uD853\uDDEC\uD803\uDE6A\u3E63\u85BD\uD82F\uDC9E\u0519\uD879\uDDB1\uD855\uDE15\u6401\uD871\uDF39\uD841\uDC8A\uD857\uDC4D\uD872\uDCB6\u8911\uD822\uDE0F\uD811\uDDD0\uD866\uDF4C\uD870\uDF8C\uD83E\uDC09\uD834\uDE16\uD820\uDE23\u8990\u3C7B\uD872\uDD61\uD849\uDDA4\u1243\uD845\uDF31\uD856\uDDD5\u2AD4\uD81F\uDF0C\uFAB5\uD809\uDD3B\u03E1\uD821\uDC12\uFE85\u4FFF\u2A22\uD876\uDDB2\uD876\uDE9F\uD870\uDCEE\uD855\uDCEF\uD865\uDC96\uD84D\uDDD9\uD86A\uDD62\uD844\uDF86\u5516\uD82F\uDC55\uD847\uDF31\uD868\uDFA2\u4C1F\uCDDA\uD875\uDDCB\u67CD\u852B\uB616\uD808\uDD8B\uD85F\uDF91\uAEC4\uD82F\uDC02\u7BC1\uD879\uDEE9\uD843\uDFB5\uD804\uDE0E\uD85E\uDFFA\u4AE9\uD848\uDD51\uD808\uDF49\uD862\uDCAF\uD81A\uDDDB\uD856\uDFC6\u6603\uD87E\uDDF1\u3F71\uD841\uDD8D\uD868\uDE1B\uD853\uDF6A\uD83B\uDE2D\uD87A\uDE90\u66E1\uD84A\uDC05\uD843\uDDD7\uD876\uDD21\uD862\uDFB6\u7640\u0092\uD845\uDC11\uD840\uDDE6\uD85A\uDE06\uD845\uDEF1\u2DD8\uD850\uDDD5\u526C\u0943\uD871\uDFEA\u08E7\uC9A9\uA0C8\uD873\uDDFF\u7D06\uD873\uDD5D\u146E\uCE31\u52E9\uD858\uDE16\uD849\uDD24\uD875\uDED2\uD868\uDDF9\u3E00\uA98F\uD81D\uDC5A\uD847\uDD1D\u85EF\uD806\uDE1C\u35A4\uD836\uDEAF\uA899\u7138\uD836\uDD4C\uD874\uDC5E\u7192\u69C9\uB01C\u8B54\uD842\uDF3F\uD84A\uDED3\uD85A\uDD34\uD86E\uDF76\u0B5D\uD875\uDD2B\uD873\uDDEA\u41ED\uD863\uDFD8\u3EEE\uD83C\uDF45\uD87A\uDC68\uD850\uDCFC\u2C83\uD801\uDC3D\uD855\uDC8D\uD86A\uDD77\u9986\uD822\uDE91\u3D03\u8CD9\uCCDC\uD821\uDC34\uD87E\uDCD5\uD81E\uDCC2\uD802\uDF28\u6310\uB32E\uBCA1\uD865\uDF5A\u5143\uD843\uDD31\u9C49\uD873\uDE8C\u2797\uD85A\uDE34\uD872\uDF6E\uD879\uDD70\u3A54\uD854\uDE26\uD86B\uDF20\uD81F\uDE8B\uD865\uDDA2\u75E0\uD80C\uDC3B\uFE7E\u1D08\uD84D\uDCE1\u628F\u2C31\uCD70\uD847\uDF86\uD84D\uDDC5\uD85E\uDD0F\uD879\uDEF3\uD845\uDE3C\uD866\uDD3B\uD864\uDE41\u3E2E\uD81F\uDD9A\uD848\uDE7C\uD861\uDC48\u7D00\u3C08\u7A91\uD868\uDCE0\uD86B\uDD16\uD84F\uDF51\uD80C\uDECA\uD853\uDFB3\uD85A\uDEA0\uD862\uDC59\uD874\uDCAA\uD879\uDD71\uBE48\u3D8E\u79F4\uCA2A\u8043\u43E6\u557E\uD86C\uDD29\uD873\uDE86\uD875\uDE80\uD835\uDE2D\uD83E\uDC34\u2E91\uD870\uDEA1\uAF93\u3A90\u8941\u1A5B\uD845\uDC02\u81B8\uD852\uDFEF\uD84D\uDF43\uD859\uDF73\u0D37\uD81F\uDD2D\uA241\uAC55\uD85C\uDDBF\u6477\uD863\uDDB7\uD86D\uDEE2\uD85F\uDD26\uD84C\uDE0B\uD76F\u205B\u8B92\uD84E\uDFA6\u9228\u7966\uD86A\uDF5D\u2606\uD84F\uDD7F\uD858\uDD7A\u581B\uD874\uDCCD\uD804\uDECF\u64BD\uD863\uDD22\uD822\uDC21\u9C88\u689E\uD86E\uDC4E\u857D\u0E39\uD83D\uDFAD\uD81E\uDF9A\u6338\uD869\uDC89\uD85B\uDC4D\u4774\uD81A\uDF8D\uC736\uD85A\uDF4C\uD836\uDDEB\uD850\uDFFB\u4424\uD84F\uDEFC\uBDAC\uD821\uDE90\uD85E\uDF4D\uD81F\uDCEC\uD033\uD840\uDEAE\uD85A\uDD6C\uD86F\uDF76\uD864\uDF1A\uD857\uDCDF\uFA73\uD852\uDF73\u052F\u9525\uD809\uDD2F\uD83D\uDD10\uD845\uDC8D\uD84D\uDE9D\uD84F\uDC2C\uD858\uDC4B\uD84F\uDD0B\uD87A\uDF09\uD877\uDFE9\uD859\uDD15\uD801\uDC69\uD81D\uDEB7\uD86C\uDCDC\uD846\uDC75\uD846\uDD5E\u9EDA\u7E8F\uDB40\uDDBA\uD85E\uDD0D\uD84F\uDF82\u20ED\u4C5C\u8DDA\uD81C\uDEBC\uD847\uDE7B\uD842\uDD69\uD858\uDD6F\u8FFB\u3741\uD863\uDF98\uD850\uDE38\uD869\uDC6A\uD87A\uDFC5\uD849\uDEC1\u3992\u2287\uD870\uDFDA\uBE71\uD840\uDDF7\uD843\uDD27\u37AB\uD808\uDCD0\uD865\uDD04\uD853\uDE2A\uD849\uDC92\uD86A\uDCDA\uD866\uDF24\uD862\uDF41\uD853\uDF8A\u63E4\uD852\uDDC3\uD858\uDF5E\uD86C\uDEA5\uD84C\uDD12\uD86D\uDDB0\u6E21\uD804\uDD40\uD574\uD869\uDD4D\uD840\uDEE0\u83F1\uD85D\uDFFB\uD13B\u600D\uBC1B\u2C85\uD851\uDD41\u6AAD\u407C\u8F1F\uAC4B\uDB40\uDDC1\uD83D\uDCB0\uD848\uDEEF\uD861\uDC62\uD85A\uDCE8\uD86B\uDC85\uD873\uDFA4\u484B\u1524\uD878\uDD8C\uA9E7\u8171\u86A2\u97A5\u239B\uD835\uDFAF\u73BF\uD85A\uDE79\uC17B\uD85A\uDD66\uD85C\uDCA4\uD834\uDE0D\uD83D\uDF3E\u8755\u6193\uD862\uDC35\uD86B\uDE7B\u9D92\uD822\uDC7E\uD879\uDF43\uFE23\uD86E\uDE5C\uB917\uD86F\uDECC\uD846\uDDFB\uD81A\uDCA5\uA8D2\uD81B\uDF3F\uD81C\uDFAC\uCCF2\uD84B\uDF81\u8391\uD866\uDC69\uD875\uDCCC\uD1B9\uD860\uDDD9\uAB01\uFCBF\u4807\uD859\uDECB\uD86E\uDE24\u71E8\uD857\uDFBB\uD802\uDC68\uD843\uDD71\uD865\uDEC9\u1695\u9246\uD845\uDFED\uD83D\uDF6C\u053B\uD84C\uDFBC\uD86F\uDE33\u2526\uD83C\uDD93\uD874\uDD20\uD85E\uDF48\uD83C\uDF29\uD846\uDE9F\uD86D\uDF57\u91ED\uD87A\uDDA8\u86EC\uD851\uDD42\uD876\uDC27\uD873\uDE0F\u58A6\uD862\uDEC7\u7535\uD860\uDFFC\uB5E9\u3D34\u32F3\uD841\uDC92\uD66E\uD83C\uDF4C\uD855\uDD20\u7FE3\uD807\uDC58\uD822\uDE45\uD84D\uDDF1\uBD8F\u26F2\uD848\uDC35\uD81C\uDE37\uA8B7\uD84E\uDDA2\uD866\uDD60\u6433\uD86B\uDFBE\u431B\u43E5\uD861\uDC8A\uD849\uDCC0\uD862\uDCD5\uD844\uDFBA\uD864\uDEBD\uD804\uDEA5\uD878\uDD37\uD87A\uDFA1\uD856\uDCFF\u6EBC\uD87E\uDD2E\uD848\uDE9B\u55B8\uD846\uDCED\uD81C\uDC50\uD845\uDCEF\uD86F\uDD66\u0C08\uD801\uDEF6\uD85C\uDE64\uC6BE\uD84B\uDE9B\u3320\uD861\uDD3F\uD81F\uDFDC\uD859\uDD76\uD851\uDDE0\u261F\uD835\uDCF6\u3400\uD853\uDEBE\u9F4D\u311A\uFE0E\uD85E\uDFCD\uD865\uDD09\uD852\uDEB8\u070D\uD847\uDEF8\u04E1\u53FF\uD842\uDE64\u55C7\uD85A\uDE5F\uD87A\uDE0F\uD835\uDC1F\uD842\uDE63\u6F3D\u1744\uD847\uDE23\uD873\uDD5A\uA990\u04DE\uD835\uDCB3\uD840\uDE4E\u4E63\uD845\uDDA1\uD86F\uDCCF\uD842\uDF08\uD85B\uDE72\uD84B\uDFB6\uD846\uDE0F\uD809\uDD1B\uD855\uDF29\uD84A\uDCE9\uD864\uDC38\uD81B\uDF2F\uA2EF\uD853\uDCE5\u14BA\u9435\u7FD2\uD875\uDE7E\uD81A\uDC8E\uD80C\uDFF4\uD820\uDD92\u7967\uD841\uDEEE\u8393\u3C83\uD87E\uDC8A\u844A\uD84A\uDE53\u4895\uA34E\uD861\uDD73\uD854\uDE98\u88DC\u1BA4\u7B6B\u0F1C\uD873\uDDD7\uD86C\uDF01\uD168\uAD5A\uD876\uDF29\uD860\uDC40\uD80C\uDF2C\uD879\uDEB5\uD801\uDEF0\u0544\u8421\uD83D\uDF08\uD857\uDF51\uD867\uDD9D\uD867\uDFB1\uB404\u4724\uB132\u081E\uD86A\uDE8D\u3597\u9DDA\u751E\uC58C\uB2E7\uD875\uDD65\uD82C\uDCF0\uD842\uDEE6\uD81A\uDE64\uD848\uDF86\u9AA0\u87A1\uD866\uDFD4\uD855\uDD29\uD871\uDDD0\u45C1\u8AE2\uD851\uDF00\u62DE\uD855\uDDEE\uD84A\uDC9C\uD84D\uDFCB\uD805\uDE58\uD820\uDEED\uD846\uDE6E\u7B32\uD878\uDEE2\uD84A\uDC92\uD808\uDE7C\uD84E\uDE47\uD805\uDCB8\uCD1B\uD804\uDD1E\uD803\uDCA3\uD808\uDF1F\uD846\uDD23\uD860\uDF17\uD84C\uDE52\u9C27\u2C53\uD85D\uDF8E\u8A55\uD800\uDE96\uD85A\uDF07\u7E8F\uD858\uDD45\uD85F\uDF5A\uD854\uDD14\u5C1D\uD861\uDFE6\uD842\uDCC2\uD81A\uDD70\u8128\uD86D\uDEFE\uD86D\uDDC4\uD844\uDFB7\uD862\uDF98\uD85C\uDF21\uD86D\uDF9B\u0DA5\uAF3F\u966B\uD81C\uDD62\uD83E\uDC83\uD86B\uDFAF\u9CF6\uD85F\uDE74\u47E9\u25FE\uD864\uDC07\u3108\u7467\uD85C\uDE1B\uD83D\uDC9B\u83F6\u37DE\u12F9\uD851\uDC05\uD83C\uDCA6\uA223\u344A\u6039\u2361\uD855\uDC44\u1439\u4758\uC9FD\u7820\uD855\uDE93\u3D7D\u5F8A\uD83C\uDE13\uA643\uD85F\uDC9D\u8550\uD85A\uDC95\uD835\uDEC8\u541E\uD802\uDC0F\uAF1B\uCFEC\uD855\uDEE5\uA0EB\uD821\uDC2C\u406F\u5739\uD87E\uDC91\u23DE\uD870\uDF41\uD110\u516F\uD843\uDC58\uD846\uDD53\uD81C\uDD62\uD834\uDF19\uD835\uDD7E\uD808\uDEEC\uD86D\uDD72\uD878\uDC92\u8273\uD85B\uDE1C\uD867\uDC1E\u2B1D\uD870\uDC31\uD851\uDF1F\uD85E\uDF7C\u324B\uCC6C\uD85E\uDE44\u7F85\uD841\uDFD8\uD851\uDC71\uD879\uDEDC\uD87A\uDF05\uD801\uDE5F\uD864\uDF5B\u119E\u29B3\uD844\uDED6\uD85B\uDE57\u853D\uD856\uDFD0\u486F\uD869\uDC7C\uD875\uDD73\uD87A\uDDED\uCE3C\uD81A\uDC28\uC85E\uD85C\uDDFC\u373C\uC174\uD868\uDE6D\uD83D\uDE28\uD808\uDC39\u74B1\u6BE3\uDB40\uDDC3\u8907\u4DF5\uD860\uDEB6\uD864\uDFAB\u0F55\uD84B\uDE0A\u1851\uD81D\uDDDB\uD850\uDD7E\uFF71\uD82C\uDE36\uD859\uDE0F\uD841\uDE0D\uD83C\uDFB4\uD848\uDF8B\u3476\uD80C\uDF25\uD7E9\uD85B\uDC9B\uD80C\uDC78\u38A0\uD84D\uDE0F\uD871\uDC10\uD86F\uDEAA\uD802\uDD98\uD84A\uDFF2\uD822\uDDC2\uB39B\u301D\uD84D\uDDF5\uBDC5\uD865\uDD56\uD879\uDE8E\uD855\uDE0A\uD821\uDC85\uC25D\uD84F\uDDA6\uD841\uDC52\uD878\uDCD5\u4C4F\u2E9C\uD821\uDC6C\u6083\u719F\uD84C\uDC47\u8D6B\uD86B\uDFE8\u056E\u4703\u60EF\uFF21\uFE9C\u6B09\uD85A\uDD10\uD213\uDB40\uDD23\uFBF8\uD86D\uDC88\uD854\uDD1E\u8FB2\uD841\uDFA0\uD857\uDFFE\u9618\uD870\uDF1C\uB3B7\uD861\uDEF6\uD854\uDCC6\uA2B8\uB8D1\u95E1\uD84E\uDEE8\u3253\uA58D\uD851\uDDF4\u6C4C\u9963\uD86D\uDF98\uD868\uDF4F\uD820\uDC90\uD86D\uDCC1\u6ACC\uD804\uDC28\uD877\uDC66\uD86A\uDD7B\uD876\uDEA4\u3E90\uD80D\uDC01\uDB40\uDD2A\u8813\uD850\uDCC0\u02D5\uD1F9\uD86D\uDFFF\uD850\uDFF7\uD81C\uDC5B\u97C6\u6C4E\uD83D\uDF6D\uD848\uDF70\u75DA\uD861\uDE21\uD841\uDEB4\uD84A\uDF01\uD842\uDE83\uB65E\u8367\u22E8\u5885\uD82C\uDC27\uCC2E\uD85C\uDD77\uD858\uDCD0\uD876\uDFF0\u0670\u4ED4\uBF9C\uD83E\uDD48\uD841\uDDA2\uD674\u9A95\uD862\uDFF0\uAEFD\uAE14\uC0C5\uD84C\uDC39\uD850\uDD59\u4FEB\u5F87\u825C\uD845\uDF61\u5B57\uB83F\uD82C\uDCF8\uD84D\uDF12\uD86A\uDCB9\uD854\uDC37\uD849\uDEC3\uD848\uDD2D\u7CD9\uD877\uDEE7\uAF4A\uD81E\uDFC3\uD87A\uDF33\u90AC\uD85E\uDC14\uD836\uDD7A\uD81A\uDF10\uD867\uDD36\uD844\uDCDD\u2467\u39E9\uD85D\uDC7C\u9721\u88E6\uDB40\uDC3B\uD860\uDFAD\uA375\uD83B\uDE8C\uBB67\uD873\uDEF3\u4571\uD864\uDFEF\uC9B5\uAF14\uD86D\uDD8F\u7799\uB5C2\uD81C\uDCD9\uD872\uDE5D\uD82C\uDC33\uD86B\uDD40\u5D1D\uBE98\uD811\uDC4E\u4E84\uD87A\uDFCF\uD809\uDD30\uAD3D\u3A83\u643D\u4521\uD86E\uDF23\u3665\uD873\uDD9C\u2DED\uD820\uDF5F\uD3B2\uD862\uDD37\u0269\uBB59\u0AB2\uCB7A\uD847\uDD0C\u5BD3\uD83D\uDD32\u29A4\uD836\uDC84\u6F08\u4E4A\uD878\uDDE8\uD876\uDC69\u16F6\u21E9\uD804\uDC5C\u4265\u5C3F\uD851\uDCE7\u547B\uD848\uDDE7\uD855\uDF7F\uD865\uDC73\uD85A\uDEAA\uD849\uDFA4\uD84F\uDE0B\uD861\uDF83\uD87A\uDF2A\u0989\uD875\uDE78\u9E1E\uD841\uDC45\u832F\uD836\uDDB7\uD85A\uDF6A\u70EE\uD81E\uDFCA\uD85F\uDFED\u07C3\uD85F\uDDE7\u9312\uD807\uDC10\uD86F\uDE41\uC88A\uD86E\uDD85\uD811\uDD10\u0CC2\u96E9\uD85A\uDC52\u8A9C\uD879\uDE7B\uD80C\uDCFC\uD85F\uDEB9\uD851\uDC50\uD85D\uDFAA\u4780\uD835\uDC8C\uD866\uDC10\uD868\uDD03\u0352\uC2C0\u7CF7\uADF9\uD82C\uDC78\uD86C\uDE47\uD848\uDF7E\uD855\uDC11\uD846\uDD34\u7A5E\uD875\uDD13\uD84A\uDC0B\uD2CA\uD835\uDC5A\uD81F\uDDD1\uD84B\uDE88\u7036\uABCA\uD865\uDF42\uC62B\uC51C\uD84B\uDEF2\u8136\u3C33\uD871\uDC9B\uAEA1\u7784\u5CB1\uD81D\uDF0E\u4971\uD849\uDCDE\uD86D\uDE44\uD85B\uDC86\uD847\uDD0C\u17E2\uD85A\uDC55\uD84C\uDED6\u9649\u62A9\u33E2\uD836\uDE4D\uD44B\u7BFE\uD836\uDE79\u5C62\uD865\uDE86\uD858\uDDE0\uD80C\uDF54\uD84F\uDEA1\uD843\uDDEF\uD803\uDC44\u2123\uD87A\uDF55\uD877\uDDBA\u5CD9\uD834\uDCA8\uD836\uDCBB\uD808\uDC65\u16DC\u1F6F\uD87E\uDC12\u4CF3\uD81D\uDD46\uD821\uDCB6\uD86E\uDF12\uA539\u0837\u76F4\u1108\u95D1\u34CD\uD805\uDEB4\u3766\u03BE\uD81C\uDCBB\u8312\uD865\uDC63\u4A8B\uD804\uDE36\uD820\uDF88\uB3EB\u9584\uD86F\uDD25\uD843\uDCBB\uDB40\uDC4A\u3B3E\uD84D\uDC6B\uFA9F\u15E2\uC5B8\uD856\uDC76\uD867\uDE6E\u9CC0\u7353\uD855\uDC47\uD854\uDFFC\uD873\uDF6B\uD855\uDD49\uD85F\uDC00\u8B73\uD841\uDDA5\u9238\u589C\uD855\uDEB7\uD85E\uDD4A\uD846\uDD96\uD808\uDE62\uD82C\uDCF9\uD81E\uDD7C\u6940\uD844\uDECC\uD861\uDEE4\uD87A\uDE04\u5D8B\uD84F\uDC28\u1BB5\uD846\uDFF7\u8D69\uD860\uDEBB\uD85D\uDF3A\uD85A\uDE53\uD864\uDD95\uD85F\uDFA3\uC23F\u4739\uD878\uDEF7\uD800\uDE88\uD86B\uDDDC\uD840\uDE18\uD854\uDFA1\u654D\uD842\uDDD6\uD834\uDF55\uD821\uDD33\uB3B4\uD85B\uDEC5\u7A0A\uD862\uDDC3\uD876\uDDF9\uD875\uDE5F\uD877\uDC9E\uD804\uDC9C\uD878\uDFA2\u7BB7\uA576\uD848\uDECB\uD81C\uDF99\uD85C\uDFAB\uD835\uDC26\uD84E\uDD0A\uD860\uDE5A\u82BB\uD857\uDEEB\uD81F\uDF05\u0974\uD860\uDD22\uD876\uDD43\uD84A\uDC59\uD834\uDDB0\uD81F\uDE09\uD87A\uDC53\uD875\uDE1F\u28B6\uD870\uDFEB\uD840\uDEAC\uAFA6\uD83D\uDEBB\u082A\u8A68\u3517\u6411\uD856\uDDA4\uD86F\uDE37\uD855\uDD82\uD854\uDEA4\uD835\uDC02\uD871\uDE24\uD821\uDC4B\uD81C\uDEBE\uD862\uDE5E\u56BE\u89CC\uBF6F\uD877\uDF59\uD81E\uDE8C\u9FC2\uD808\uDDB3\uD874\uDE47\u169A\uD875\uDCCB\uC1C9\uD835\uDEBF\u67C0\u676B\uD836\uDDA4\uD841\uDC14\u8569\uD81C\uDEEB\uD856\uDCE6\uD85C\uDEFA\u88ED\uD847\uDD12\uD873\uDED8\uD850\uDC14\u61FD\uD85B\uDF1B\u5C37\uB979\uD840\uDC78\uD860\uDE41\uD84E\uDFE8\uD808\uDC9B\u47CC\u7928\uD86A\uDFDC\uD850\uDE9B\uD821\uDC95\uD811\uDD42\uD863\uDE15\u73B4\uD852\uDCB6\u1662\u96CB\uD866\uDCF7\uD84E\uDE0C\uD83E\uDD8E\u68D6\uD85A\uDCD3\u4F31\u8A54\uD821\uDED2\uD871\uDF20\u5EEA\u4DF6\uD859\uDE43\u2C7D\u2166\uD868\uDD22\uD835\uDE6B\u0854\uD85E\uDE0F\uD84B\uDEB9\uD871\uDD60\uD848\uDFDD\u961B\uCD9C\uD86C\uDD57\u8491\u13A6\uD807\uDD1C\u25A9\u22A6\uD84E\uDCC1\uD81C\uDE8F\uD821\uDF0F\uD85F\uDCDE\u2E1A\u23EB\uD85F\uDD24\uD81D\uDDA5\uD84C\uDFE3\uC41D\u76C7\uD85E\uDF8C\uD81D\uDED0\uD864\uDE62\uD861\uDF24\uAC4F\uD872\uDCC7\uD846\uDEBD\uD84E\uDD6C\uD5CC\uD836\uDC01\uD847\uDEB7\uD841\uDFEF\uD84C\uDD80\uD809\uDCD9\u6DA0\uD869\uDC90\u0331\uD7E1\u9720\u193B\uD879\uDF8C\uD857\uDF72\uD848\uDEF1\uD834\uDD16\u04B6\uBE33\uD878\uDF6C\uD843\uDEE6\uD808\uDF5B\uD855\uDC4A\u118A\uD85C\uDD92\uFAC0\uD851\uDDA1\u1C7D\u80D8\uC33E\u6C68\uD876\uDE42\u40DE\uD84B\uDC9A\uD855\uDCE3\uD844\uDEF6\uD873\uDFF2\u8F62\uD84E\uDD59\u43EC\uD841\uDFCB\uD851\uDE76\u6F27\uD857\uDD2B\u9821\uD836\uDE3C\uD820\uDE02\u45CE\u2B81\u07AB\uD84D\uDC6A\uD81C\uDFF4\u6F91\u11CD\uD808\uDD6C\uD849\uDE1C\uD843\uDCDA\u5180\uD86B\uDF40\uD862\uDD1C\uD808\uDF04\uD852\uDD14\uD872\uDDF2\uD848\uDC43\u42E1\u0607\uD81D\uDC18\uD83A\uDC04\uD855\uDF29\uD820\uDED0\uD85E\uDE13\uA78E\uD860\uDC73\uD806\uDE5A\uD864\uDEA2\uD848\uDECF\uB6A7\uD860\uDD12\uA8A9\u29A6\u638A\uD867\uDCAB\uD874\uDCD0\uD85D\uDF05\uD81D\uDD2B\uD843\uDEA9\uCCEF\u636A\u8445\u5B47\uD869\uDE66\uD84C\uDE79\uD86D\uDDEF\uD870\uDFEE\u30DB\uD86E\uDD53\uD85B\uDCA1\uD868\uDE4C\uAB24\uD852\uDD5B\uD851\uDE0D\u7525\uD84E\uDF26\uD86E\uDD8C\u1D38\uD85F\uDF73\uD86E\uDD63\uD866\uDEFA\u3E6F\u1D19\uD843\uDF8C\uFF88\u205D\u858A\uD85A\uDEAC\uD86F\uDC19\uD874\uDE7B\uD879\uDE47\uB1A3\u878B\uD85D\uDCF2\uD862\uDCCF\uD876\uDDD2\uD847\uDCF0\uD845\uDF22\u909E\uD84D\uDDA9\uD859\uDDDA\uADD5\u4B9E\uBB44\u745F\u67A9\uD803\uDC96\u7A4D\uD85E\uDE46\uD87A\uDC6B\uB69E\uCF38\uD851\uDF62\uD801\uDF50\uD82F\uDC77\u8932\uD875\uDE7B\u1610\uD52A\u6A51\uD5B4\uD852\uDD63\u0C60\uD86D\uDCD8\uD822\uDE11\uC621\uD802\uDCFD\u8D9E\u98B4\u6F03\uD849\uDCFB\u1B30\u1922\uD846\uDFF9\uD855\uDF14\uDB40\uDD16\uD867\uDC1D\uD848\uDD5E\uD850\uDC3D\u3E97\uD840\uDE88\uD86C\uDC85\u485C\u8A3C\u5C45\uBBDE\uAA41\u6AEA\uD855\uDF96\u8381\uD84F\uDFC0\u7381\uD81C\uDF14\uD86B\uDF20\u1985\uD879\uDCAA\uD80C\uDD5A\uD872\uDC36\uA3CD\u7499\uD85D\uDCF2\u18F4\uD857\uDDB7\uD851\uDDE6\uD834\uDDD0\u3372\uD877\uDCFE\u9DF7\u6C12\uD851\uDC70\uA53D\uBC94\u5449\uD849\uDFE4\u8D71\u23A7\uD85E\uDE58\\u000b\uD85E\uDFCE\uD2DA\uD859\uDCF0\uD873\uDC08\u9C20\u2AB6\u7488\uD879\uDE04\u3487\uD86E\uDFD2\u80DA\u445F\uD801\uDCDF\uD84D\uDD95\u6683\u91C8\uD841\uDD28\uD81D\uDE3B\uD821\uDC21\uD853\uDF02\uA9AC\uD81C\uDDE7\uD846\uDD74\uD854\uDD56\uD802\uDC35\uD846\uDF52\u4DA1\uD875\uDCF7\uCCE7\uD854\uDD16\uD869\uDC44\u4E6C\uD85D\uDC13\uD873\uDCBC\uD852\uDF11\uD809\uDC8A\u09A1\uD834\uDC56\uD840\uDE4C\uD878\uDCE6\u600F\uD85D\uDF5A\u59A5\uA8B1\uD86F\uDC4A\u63A8\uD86C\uDEE0\u9DFD\uD86D\uDC26\uD81F\uDCA5\uD84D\uDC97\uD86D\uDC55\u3A08\uD81C\uDC58\uD821\uDCBB\uD800\uDEB4\u28E6\uD860\uDF98\uD801\uDCE5\u7E28\uD864\uDFF4\uD821\uDC39\uD81E\uDCD8\uD81A\uDCE2\uC7B9\uD83C\uDF26\uD847\uDDCB\uD84D\uDE60\uD811\uDE38\u7532\uD81A\uDC16\u5513\u30F6\u3C8E\uC116\uD811\uDDA9\uD846\uDEC2\u52F4\u5C1C\uD80C\uDDC9\u10A6\uCABD\uD844\uDFE3\u094C\uD863\uDF3C\uD84D\uDD9D\uD821\uDDF6\uD855\uDCBC\uD822\uDC12\u70E0\uD859\uDC5F\u623A\uD877\uDF0F\uD85D\uDD75\uD866\uDFE2\uD86A\uDF1C\u4D12\uD822\uDC6F\u81E1\uD877\uDC3A\u4B42\uD872\uDC66\uD87A\uDCD9\uD850\uDF84\uD878\uDE8B\u2941\u2F62\u8A09\uD81C\uDC5C\uD836\uDE88\uD84F\uDE7D\u5152\u188A\uD80C\uDF17\uD82C\uDDEF\uD86A\uDE2D\uD868\uDCDC\uD81F\uDF1A\u9249\u8D10\uFE44\uD858\uDEFF\uD850\uDEBA\uD853\uDE41\uA863\uD803\uDE72\uD879\uDCF5\u050E\uD846\uDD5F\u5124\uA2C5\uD86E\uDED3\u6FED\uD820\uDF3E\uA502\uD846\uDE7C\uD453\uD866\uDE6E\u05DE\uD855\uDD5F\uD848\uDD52\uD84C\uDF2B\uD846\uDE2D\uD863\uDCAC\uD869\uDE0B\uD86E\uDC67\uD821\uDE37\uD842\uDC2A\uD802\uDE65\uD862\uDC66\u919D\uD83C\uDC43\u6A39\uD866\uDCCC\uB78D\u5F30\uD874\uDE53\u29DB\u3432\uC9BA\uD83A\uDC90\uD821\uDDA7\uD851\uDEF4\uF9C3\uD85F\uDE34\uD81E\uDEC3\uD866\uDCCE\uD835\uDD9E\uD87A\uDE2D\uD86C\uDD15\u0449\u8128\uD86A\uDC4F\uD874\uDD3E\uD84C\uDED9\uD85C\uDFEC\uD851\uDE65\uD856\uDEA9\uD802\uDDA6\uD85B\uDFC3\u5E4F\uD835\uDCD7\uD86E\uDE7A\uD84B\uDC33\uD3F9\uD878\uDC56\uD846\uDFE7\uD867\uDF70\u4325\u3CE0\u95F3\uA6A1\u690B\u012A\uD81A\uDC11\u55E8\uD850\uDC04\u8321\uD879\uDE6D\u58D0\uD853\uDD57\uFDF5\u76E3\uD805\uDE8A\uD81C\uDCBF\uD81B\uDF71\uD849\uDDDA\uD866\uDD72\uD869\uDF7A\uD83C\uDC5C\uD867\uDD0B\u2CF1\uD84D\uDD0E\u94E4\u67AE\uD876\uDF2A\uD85F\uDEEE\uD834\uDC31\uCDFD\uD811\uDDFA\uD85B\uDD2C\uD822\uDE71\uD81F\uDE1A\uD83D\uDDCC\u03E2\uD84F\uDEB5\uD86A\uDEE9\uD2D7\uD873\uDD0E\u24F3\uD871\uDC53\uA93B\uD857\uDD08\uD859\uDEF7\uD822\uDD0B\uD869\uDCCF\u870F\uD86E\uDCD7\u2C00\uD85E\uDF01\uD866\uDC8E\u749B\uD85C\uDF13\uD868\uDD84\uD865\uDFF4\uD87E\uDD9B\uD846\uDDF9\u99E7\uC088\uD85D\uDFD8\u875E\uD855\uDD33\uD806\uDCB0\uD856\uDD87\uC92D\uD85F\uDEA2\u88EB\uD85B\uDCF3\uD863\uDE3F\uD809\uDC89\uD81F\uDDCC\uD859\uDF39\u24E3\uCF70\uD83D\uDC45\uD86F\uDF21\uD821\uDD2C\u4C90\uD83D\uDFCB\uD845\uDED7\uD806\uDE7B\uD84D\uDFDA\u11FC\uD850\uDEB9\uD84C\uDE00\uAB65\u6519\uD841\uDF8F\u28F1\uB12D\uD859\uDF5D\uD863\uDF90\\\"\uD85B\uDD13\uD858\uDF26\u5964\u8F03\u3A46\uD806\uDED1\uD804\uDD76\uD878\uDF66\uD81F\uDD6B\uD878\uDE2C\uD81C\uDD81\uD874\uDC8A\uD862\uDE29\uD800\uDEB4\u0CCD\u547C\uD844\uDDA8\uA88E\uD81C\uDD12\uD81A\uDDD9\uD834\uDF31\u46CE\u6A78\uD841\uDF71\uD86C\uDD02\u3FFC\u9752\uD868\uDEEA\u673C\uD86D\uDC48\u4863\uD857\uDDF9\uD800\uDEC2\uD861\uDF1E\uD81F\uDE93\u9E97\u5EE0\uD84C\uDC52\uC500\u076C\u9381\uD83C\uDC24\uD840\uDFDD\uD840\uDD29\uD855\uDC74\uD859\uDF2E\uD86A\uDE25\u58FE\u4AEC\uD844\uDE6E\u2625\uFD33\u1617\u701C\uD871\uDD2B\uD840\uDE3B\uD861\uDF43\uD86E\uDDFF\uD80C\uDF66\uD85F\uDC58\uD879\uDF62\uD865\uDF9C\uD84C\uDCB6\uD840\uDF3E\u85DC\u2CE2\uD84B\uDD5F\u1EB0\uD86F\uDF9F\uD840\uDD14\uD859\uDD10\u6092\u4CE5\uD850\uDFD2\uD87A\uDFAC\uD84E\uDD3E\u384B\uD81E\uDF1E\u585C\uD873\uDE74\uD83D\uDEC6\uC989\uD860\uDE84\u72ED\uD85D\uDFAD\uD86C\uDFE9\uD81E\uDD2C\uD859\uDCAD\uD840\uDC8E\uD868\uDC17\uD875\uDFD3\uD801\uDD16\u3236\uD844\uDF96\uD874\uDF3B\uC7F3\uCF3C\u6870\u04FD\u2535\uD86C\uDE22\u91C5\uD811\uDD0C\uD86A\uDF1D\uD866\uDD85\uD862\uDD8A\uD860\uDF22\uD83C\uDCA1\uD84C\uDFBD\uD842\uDF84\uD874\uDCBD\uD861\uDE8A\uD85F\uDC97\u5F0C\uD845\uDFE1\uB8A7\u7721\u43BF\uD84E\uDF93\uD867\uDF26\uD854\uDCD9\uD852\uDFFF\uD86D\uDC11\uD879\uDEE9\u950E\uD866\uDE28\uD863\uDF13\u67F4\uD81F\uDF5A\uD85E\uDC8E\u0446\uD868\uDC39\uD81F\uDDE7\u9FAD\uD820\uDC40\uB55E\uD859\uDC6C\uD848\uDECD\uABB1\uD860\uDCBD\u80B6\u9028\u6611\uD844\uDCE1\uD86E\uDCB8\uD81F\uDE1B\uD85C\uDCC2\uD844\uDE78\uD858\uDF2E\uD847\uDF92\uD805\uDC45\uD84C\uDD34\u73D6\uAF33\uDB40\uDD2E\uD877\uDECE\u8B16\uD85D\uDD55\uD878\uDF4A\u4954\uD86D\uDD5B\uD870\uDD63\u29FE\uAFFB\uFE87\u7187\uA0F6\uD807\uDC3C\uD854\uDE4F\uD86D\uDCBC\uD846\uDF9A\uD853\uDF06\uD81F\uDE23\uD842\uDF24\uD821\uDED0\uCF54\uD85F\uDDA1\uD864\uDFC7\uD879\uDD4F\uAD88\uD859\uDEA7\uD861\uDFC9\uD3C5\uD864\uDD81\u3811\u02A8\uD848\uDD8D\uD834\uDD54\uD86F\uDD20\uD868\uDE95\uD83D\uDD7D\u388B\uC799\uD86E\uDC18\u5780\uD84F\uDF5C\u6557\uD84B\uDEA8\uD845\uDE8F\uD856\uDF31\uD865\uDD07\uD84E\uDD7D\uD83E\uDD83\uD856\uDD35\uD857\uDEBA\uD845\uDF42\u68DB\uD850\uDC1D\uCCC0\u996A\uD862\uDC2E\uD861\uDF3A\u288D\uD81F\uDEC7\uD820\uDC94\uD852\uDDD3\uD84A\uDDD2\u6E1A\u9973\uD844\uDC4D\uD81E\uDEA3\uD84D\uDEBC\uD843\uDC64\uD858\uDEB0\uD86E\uDD95\uD81E\uDD63\uD85D\uDCDB\uD821\uDC99\uD87E\uDC6D\uD852\uDEDD\uA7FC\uD85A\uDF0A\u42A9\uD84E\uDE37\uD81E\uDCDB\uD868\uDF71\uD820\uDC1D\uD850\uDDD4\u083D\u5069\uB72A\uAD32\uD83C\uDC62\uD83C\uDF37\uC809\uAE51\u5BEE\uD874\uDEA9\uD874\uDC88\uD842\uDF8C\uA14E\u583F\uD85B\uDF78\uD86B\uDFCE\uD86D\uDC96\u2910\uD864\uDFB6\uD861\uDDDB\uC9D3\uD821\uDFE6\uD85A\uDDF0\u58DA\uD866\uDFF5\uD846\uDC7C\uBCBB\uD875\uDF3B\uD862\uDFDC\uD811\uDCBA\uC649\uD859\uDE21\uD867\uDC6B\uD874\uDC99\u1123\uD840\uDF87\uA6B5\uD868\uDE1D\uD84E\uDCC1\u371C\uD802\uDEE4\u9DDC\uD875\uDD50\uD86E\uDFE4\u94BB\uD821\uDE4F\uD86F\uDE77\uD84A\uDE25\uAB56\u4907\uB212\uD81A\uDF2E\u1DF8\uD86D\uDD5D\uD80C\uDE84\uB594\uD80C\uDF76\uD866\uDD85\uD805\uDCA9\uD840\uDCF5\uD862\uDC6C\uD85F\uDD6C\u8A0B\uD874\uDC24\uB94E\uD81C\uDC20\u5E1C\uD844\uDF81\uD80C\uDC3E\uD844\uDFFC\uD81D\uDE1B\uD866\uDCF4\u80D4\uD853\uDD24\u17C5\uD808\uDE0B\uFBA2\u67F3\u8D19\u9C3E\u66BE\uD87E\uDD65\uB122\uD874\uDE31\u6B21\uD851\uDF1E\uD849\uDD03\u8033\uD81F\uDF03\uD875\uDD29\uD820\uDC07\uD86E\uDD67\u9108\uD83D\uDF65\uD803\uDC0F\uD82C\uDC4A\uDB40\uDD24\uD800\uDCED\u4F14\uD869\uDD2D\uAA12\uD842\uDE86\u1FD2\u98EE\u20A5\uD82C\uDCCB\uC926\uD850\uDD99\uD851\uDE9F\uD860\uDC07\uD868\uDE9D\uD451\u7282\uD86C\uDD47\uD870\uDF45\uDB40\uDD60\u4D27\u4D57\uD876\uDCA4\u8D1D\u6C1C\uD844\uDC66\uD86C\uDD06\u9E6B\u4AB3\uAE21\uD81A\uDF2A\u7CD7\uD854\uDEA6\u05C7\u664A\u4017\uD857\uDC2E\u7551\u0719\uA0F5\u8FD8\uBE6A\u4823\uD879\uDD9E\u16E5\uD862\uDCC5\uD844\uDED3\uA598\uD855\uDD14\uAE3E\uD873\uDFAC\uD865\uDFF0\uD850\uDC83\u9E5E\uD870\uDE74\uD59D\uD83E\uDC6D\u09DD\uD808\uDDD1\uD840\uDF63\uD870\uDF7E\uD875\uDF1A\uD877\uDFE1\uD876\uDF0F\uD802\uDC6B\uD864\uDC4C\uD854\uDFD7\uCF96\uCA8C\u761E\uD872\uDC89\u6F87\uD83A\uDCA9\u5858\uD822\uDCAF\uD821\uDEB6\uD840\uDEAB\uD821\uDC20\uD85D\uDECC\uD867\uDC59\u5F4F\uD84A\uDDB8\uD85D\uDD77\uD801\uDC01\uD86E\uDFFD\uD84E\uDCEC\uD854\uDD61\u30CA\u29B3\uD872\uDC7B\uC52F\uC7C7\u5ED1\uD863\uDF8B\uD855\uDF03\u1B73\uD86B\uDF49\uD862\uDF33\u208E\u1E85\u0F86\u7B67\uD855\uDD03\uD873\uDC8F\u1F8A\uD870\uDC93\u188E\uD859\uDDA4\uD864\uDFFA\u3AED\u1492\uD860\uDE7D\uAD2A\uD835\uDCC5\uD854\uDEA2\uD870\uDF26\u84FD\uAE7B\uD86F\uDC29\u8DA5\u98B0\uD86A\uDCA3\uD81E\uDE65\uCB51\uD85D\uDDD9\u880E\uD867\uDF74\uD873\uDD54\u9F37\uD81F\uDE39\uD845\uDD3D\uFD02\u8FFE\uD847\uDEF2\uD86D\uDE99\uD820\uDD79\uB0EB\uD809\uDCA3\uB6AB\uD805\uDE2B\uD857\uDF8E\uC1C2\uD847\uDE03\u7E09\uA6AB\uD3EE\u32FF\uD874\uDD13\uD835\uDEE1\uD85E\uDC13\uD835\uDD95\uD869\uDC00\u1084\u521E\u5723\u1936\uB338\uD841\uDF8D\u7F49\uD85D\uDC9B\uD877\uDFA8\uD86B\uDD54\uD87A\uDE9C\u6FA5\uD800\uDE94\uD84A\uDF2F\uD851\uDE47\u6BC1\u2713\uD852\uDC0C\uD6DB\uD86B\uDC7D\u812B\uD80C\uDE68\uD85C\uDF8C\uB313\u0F65\uD820\uDEBF\uD822\uDD80\u8C5C\u527D\uD865\uDEF9\u9B09\uD842\uDD96\uD86C\uDCDA\uAD6E\uD84C\uDEAE\uCE9F\uD874\uDC18\u4B30\uD853\uDF10\uD861\uDEA4\uD879\uDCBF\uD859\uDD75\uD86F\uDE14\uD81D\uDE26\u914E\uD820\uDF50\uD84B\uDEDC\uD85E\uDD76\uD856\uDD6B\uC144\uD843\uDE6E\uD83A\uDD2A\u4E93\u27CE\uD862\uDCF5\u54CF\uD879\uDFC8\uD808\uDE24\u3EE4\uFD94\uD86D\uDCA1\uD87E\uDCC7\u791A\u9DF7\u897C\uB85D\uD801\uDC9C\uD84A\uDD48\uD801\uDF20\u7ABC\uD83D\uDC99\uA9F0\u2366\u8E95\uD857\uDF9E\u07DF\uD872\uDD6D\uD854\uDCEF\uD848\uDF2A\u6E7D\uB99D\uD843\uDD20\u9275\uD867\uDE47\uD857\uDCE8\uD820\uDDAB\uC7BD\uFB95\uD81A\uDE50\u37A9\uD82C\uDCC3\u7DE6\u4E23\uD844\uDE6D\uD85C\uDC58\uD802\uDDCE\u8454\uD843\uDDFD\uD840\uDD2F\uD854\uDF77\uD853\uDFA9\uD876\uDF77\u103B\uBC9E\uD82C\uDECD\uD861\uDE96\uD853\uDF48\uD864\uDEAF\u0C30\uD85F\uDC52\uA195\u580F\uD873\uDC63\uD849\uDF9F\u1F9E\uD878\uDF45\u585D\u12FE\uD800\uDD19\uD860\uDC32\uD82C\uDCBB\u33C0\u4D7A\uD84D\uDF4B\u250F\uD857\uDD21\u2CA9\uD81C\uDD5B\uD877\uDD53\u2BA0\uC921\uD85D\uDF79\uD847\uDFBD\u9CA7\uD83C\uDF5B\uD868\uDF75\u3872\uD85E\uDEF2\u6847\uB526\uD849\uDCDC\uD869\uDC1D\u2EBC\uD876\uDEF2\uD868\uDDED\uB5E8\uD848\uDCAC\uD860\uDCF8\uBC8E\u6CAA\u2236\uD869\uDDF7\u21AC\uD801\uDE4B\uBFA5\uD877\uDDA9\uD851\uDDEE\uD85F\uDF33\uD848\uDC8F\uD864\uDCD6\uFF64\uD85E\uDE87\uD856\uDE04\u484C\u3170\uD843\uDDE8\u5A8D\u7EB1\u3F78\uD80C\uDD00\uD801\uDD56\uD857\uDF76\uD874\uDD2E\uD852\uDF28\uD86B\uDFA2\uD808\uDDB5\uB323\uA0C2\uD847\uDD6C\u5988\uD85A\uDF89\uD858\uDC2F\u0732\uD86F\uDFB5\u2623\uD854\uDF73\uD870\uDC50\u6324\u18C8\u8124\uD84B\uDF63\uD83C\uDF36\uD81E\uDE9D\uD86A\uDD71\uFCFB\uD873\uDDDD\uD857\uDF41\uD876\uDC93\u84E5\uD81F\uDFCD\uD869\uDEB3\uD852\uDF7C\uD867\uDDC2\uB0B3\uD81E\uDD10\uD86C\uDE43\uBB58\uD805\uDF38\uD86E\uDEDB\uD866\uDDE4\uBFF4\uD81A\uDCA3\uD858\uDCA2\u26DB\uD865\uDFAB\uAC56\uD835\uDF87\uD86D\uDC20\u8BE5\u9FC5\uD854\uDF98\uCF52\u8911\uD86D\uDD46\uD856\uDDB7\uA576\u0FB2\uD868\uDE23\u191C\uD85A\uDDAD\uD86A\uDEAC\uD849\uDE8D\uD875\uDEE4\u35F0\uD842\uDC92\uD800\uDD33\u2CC0\u23CD\u14E8\uD835\uDD10\u96A8\uD860\uDD4C\uD86C\uDFBD\uD861\uDC52\uD85E\uDEB0\uD81F\uDEC6\uD807\uDC3F\uD858\uDD7E\u24F2\uD863\uDEF7\uD867\uDFDD\u9508\uD859\uDCEC\uD873\uDFED\u39C0\u3251\uFFE0\uD86F\uDE34\uD808\uDC7A\uD85F\uDFD5\uD855\uDDCF\uBF31\uD85D\uDEA7\uD83C\uDC0E\uC629\uD841\uDF6E\uA64F\u863E\uD862\uDF02\uD86D\uDCA2\uD857\uDD2E\u145A\uD805\uDF20\u2018\uD877\uDE36\uD86F\uDC3A\u8CF9\uD86A\uDD51\uD875\uDF8B\uD855\uDCE8\uD3D9\uD83D\uDDF5\uD871\uDC32\u5CF2\uD877\uDEDC\u6567\uD84C\uDD44\u1041\uD867\uDD70\uD844\uDF9B\uD858\uDC9B\u1C1D\uD873\uDFB2\uD862\uDFD7\u7B05\uD863\uDE0A\u47AA\uD80C\uDDEE\u59F9\uD856\uDCAA\uD82C\uDEF9\u3B43\u87EC\uD855\uDDDC\u80A5\uD850\uDC09\u21D5\uD85A\uDFB8\u6E9C\uD86B\uDE2C\uD807\uDD36\uD879\uDE51\u9FA0\uD86E\uDE5C\uD81C\uDEFC\uD847\uDD71\u6440\u2B19\u0084\uA244\uD874\uDF6D\uC75E\uCE78\uD856\uDC24\uA9BD\uD844\uDE5E\uD835\uDCF0\uD83E\uDD64\u54EC\u5614\uD868\uDD4D\uD85C\uDFEB\uD855\uDCF0\uB35D\u86CC\u6C1C\uA2CF\uD86D\uDC1F\u428A\uD803\uDCD0\u3922\u7B1F\uD822\uDDE2\uD860\uDD61\uD811\uDD12\u8A8E\uD801\uDD48\u42FE\uD85A\uDF51\u80CA\uD859\uDF21\uD853\uDE0D\uD86C\uDDE9\uD800\uDE8D\uD853\uDEE8\uD874\uDD49\u763C\uD873\uDDD7\u1B0E\uD85E\uDE78\uD857\uDE23\u4CEE\uD855\uDF16\uB1A0\uCF36\uD801\uDC22\u8151\u166A\uD809\uDCF9\uD85B\uDF00\uD85A\uDF05\uD862\uDF74\uD81E\uDDFA\uC410\u3787\uD863\uDEDC\uD81E\uDEBD\u8A19\u8710\u10A8\uB715\uD806\uDE6D\uD874\uDDEC\uD849\uDC19\uBB7A\uC9D2\uD879\uDFBE\uD865\uDE0C\uD840\uDD35\u3343\u1F9B\uBD98\u7E47\uD841\uDC24\uD864\uDCFD\uD853\uDF20\u4F81\u1A24\u3D41\u3552\uD851\uDC40\uD857\uDD89\u5E21\uD874\uDFCD\uD841\uDDD2\u12CE\u8487\uD875\uDC7A\uD801\uDC62\u73A1\u4D24\u5163\uD836\uDDF1\u5D38\u081E\u24A2\u9EE0\uCC12\uD864\uDC1F\u9BBE\uD878\uDCEF\uD86C\uDCC9\uD842\uDCFB\u814F\uD853\uDDFB\uD861\uDDF9\uD85C\uDDA3\u651F\uD845\uDF52\u1BA2\uD84E\uDDF3\u3244\uD81D\uDFB7\uD84C\uDD28\uD842\uDC3A\uD854\uDF75\u35D2\uD84B\uDC7B\uD855\uDE64\u5384\uD863\uDCDC\uD81A\uDCA5\uD844\uDD91\uDB40\uDC2F\uD86F\uDC45\uD85D\uDFB3\u1B88\uD805\uDF23\uD84B\uDE5F\uD84A\uDFEA\uD84A\uDF9A\uD842\uDCA7\uD862\uDFDF\uD85C\uDD57\u56B6\uD84A\uDE3D\uD87A\uDCE6\u478F\uD877\uDE38\u6C60\uD849\uDE4F\u1D9F\u055A\u3741\uD81F\uDFD3\u69FE\uD84D\uDD18\uDB40\uDC7E\u8AB8\uD81E\uDEF3\u4EDF\uD877\uDD66\uD849\uDDD4\u7C7C\uD854\uDFF3\uD843\uDE55\u6EF4\uD835\uDD09\uD866\uDCA4\u63E2\uD86D\uDF5E\uD81E\uDEC3\uAB09\u529B\uD81B\uDF59\uD84A\uDD19\uD855\uDD04\uD86C\uDF2F\uD863\uDFE6\u783D\u937A\uD84E\uDF0F\uD877\uDCDE\uD845\uDF98\u55C6\uD81A\uDD3F\uD820\uDCAB\uD872\uDC11\uAD96\u5C38\uD338\uD858\uDDF8\uBC6D\uD84D\uDEC9\uD85C\uDF11\uD867\uDCB8\uD834\uDCEE\u306C\u1646\uD842\uDE09\u513B\uD865\uDDCA\u2F1C\uA25F\uD790\uD875\uDF9D\u6791\uD846\uDF46\uD872\uDDF8\uD39C\uD84F\uDE5F\uD83C\uDF32\uD83D\uDF04\u04BB\uD870\uDD56\uD86B\uDE4F\uC028\u9C00\uD81C\uDFE3\uD834\uDD54\uD81E\uDED9\u3A7F\u6D65\u9353\uD793\uD84A\uDD3B\u45FF\uD86A\uDEAB\u141B\uD820\uDF4A\uD857\uDFBB\uD872\uDE41\uD85F\uDC35\u15C3\uACBD\uD81D\uDC2E\uD870\uDC31\uD875\uDE0E\uD861\uDC33\uD871\uDCCD\uD865\uDEEF\uD83E\uDC37\uD1C4\uD846\uDCC1\uD87A\uDDCA\uD821\uDCF7\uD83C\uDC1C\uD81D\uDC4A\uD861\uDCC5\uD869\uDF13\uD840\uDD0E\u23EB\uD851\uDD70\uD847\uDF0E\u4013\uB0EB\u656E\uD865\uDDFD\uD841\uDFF8\u7FB7\u14DE\uD86A\uDD94\uD834\uDCE6\",\"_entity_type\":\"indexNameType\"}", + JsonObject.class + ) + ) + ) ); + + return params; + } + + private GsonHttpEntity gsonEntity; + private String expectedPayloadString; + private int expectedContentLength; + + public void init(List payload) throws IOException { + Gson gson = GsonProvider.create( GsonBuilder::new, true ).getGson(); + this.gsonEntity = new GsonHttpEntity( gson, payload ); + StringBuilder builder = new StringBuilder(); + for ( JsonObject object : payload ) { + gson.toJson( object, builder ); + builder.append( "\n" ); + } + this.expectedPayloadString = builder.toString(); + this.expectedContentLength = StandardCharsets.UTF_8.encode( expectedPayloadString ).limit(); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("params") + void initialContentLength(String ignoredLabel, List payload) throws IOException { + init( payload ); + // The content length cannot be known from the start for large, multi-object payloads + assumeTrue( payload.size() <= 1 || expectedContentLength < 1024 ); + + assertThat( gsonEntity.getContentLength() ).isEqualTo( expectedContentLength ); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("params") + void contentType(String ignoredLabel, List payload) throws IOException { + init( payload ); + String contentType = gsonEntity.getContentType(); + assertThat( contentType ).isEqualTo( "application/json; charset=UTF-8" ); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("params") + void produceContent_noPushBack(String ignoredLabel, List payload) throws IOException { + init( payload ); + int pushBackPeriod = Integer.MAX_VALUE; + for ( int i = 0; i < 2; i++ ) { // Try several times: the result shouldn't change. + assertThat( doProduceContent( gsonEntity, pushBackPeriod ) ) + .isEqualTo( expectedPayloadString ); + assertThat( gsonEntity.getContentLength() ) + .isEqualTo( expectedContentLength ); + } + } + + @ParameterizedTest(name = "{0}") + @MethodSource("params") + void produceContent_pushBack_every5Bytes(String ignoredLabel, List payload) throws IOException { + init( payload ); + int pushBackPeriod = 5; + for ( int i = 0; i < 2; i++ ) { // Try several times: the result shouldn't change. + assertThat( doProduceContent( gsonEntity, pushBackPeriod ) ) + .isEqualTo( expectedPayloadString ); + assertThat( gsonEntity.getContentLength() ) + .isEqualTo( expectedContentLength ); + } + } + + @ParameterizedTest(name = "{0}") + @MethodSource("params") + void produceContent_pushBack_every100Bytes(String ignoredLabel, List payload) + throws IOException { + init( payload ); + int pushBackPeriod = 100; + for ( int i = 0; i < 2; i++ ) { // Try several times: the result shouldn't change. + assertThat( doProduceContent( gsonEntity, pushBackPeriod ) ) + .isEqualTo( expectedPayloadString ); + assertThat( gsonEntity.getContentLength() ) + .isEqualTo( expectedContentLength ); + } + } + + @ParameterizedTest(name = "{0}") + @MethodSource("params") + void produceContent_pushBack_every500Bytes(String ignoredLabel, List payload) + throws IOException { + init( payload ); + int pushBackPeriod = 500; + for ( int i = 0; i < 2; i++ ) { // Try several times: the result shouldn't change. + assertThat( doProduceContent( gsonEntity, pushBackPeriod ) ) + .isEqualTo( expectedPayloadString ); + assertThat( gsonEntity.getContentLength() ) + .isEqualTo( expectedContentLength ); + } + } + + @ParameterizedTest(name = "{0}") + @MethodSource("params") + void writeTo(String ignoredLabel, List payload) throws IOException { + init( payload ); + for ( int i = 0; i < 2; i++ ) { // Try several times: the result shouldn't change. + assertThat( doWriteTo( gsonEntity ) ) + .isEqualTo( expectedPayloadString ); + assertThat( gsonEntity.getContentLength() ) + .isEqualTo( expectedContentLength ); + } + } + + @ParameterizedTest(name = "{0}") + @MethodSource("params") + void getContent(String ignoredLabel, List payload) throws IOException { + init( payload ); + for ( int i = 0; i < 2; i++ ) { // Try several times: the result shouldn't change. + assertThat( doGetContent( gsonEntity ) ) + .isEqualTo( expectedPayloadString ); + assertThat( gsonEntity.getContentLength() ) + .isEqualTo( expectedContentLength ); + } + } + + private String doProduceContent(GsonHttpEntity entity, int pushBackPeriod) throws IOException { + try ( ByteArrayOutputStream outputStream = new ByteArrayOutputStream() ) { + OutputStreamContentEncoder contentEncoder = new OutputStreamContentEncoder( outputStream, pushBackPeriod ); + while ( !contentEncoder.isCompleted() ) { + entity.produce( contentEncoder ); + } + return outputStream.toString( StandardCharsets.UTF_8.name() ); + } + finally { + entity.close(); + } + } + + private String doWriteTo(GsonHttpEntity entity) throws IOException { + try ( ByteArrayOutputStream outputStream = new ByteArrayOutputStream() ) { + entity.writeTo( outputStream ); + return outputStream.toString( StandardCharsets.UTF_8.name() ); + } + } + + private String doGetContent(GsonHttpEntity entity) throws IOException { + try ( InputStream inputStream = entity.getContent(); + Reader reader = new InputStreamReader( inputStream, StandardCharsets.UTF_8 ); + BufferedReader bufferedReader = new BufferedReader( reader ) ) { + StringBuilder builder = new StringBuilder(); + int read; + while ( ( read = bufferedReader.read() ) >= 0 ) { + builder.appendCodePoint( read ); + } + return builder.toString(); + } + } + + private static class OutputStreamContentEncoder implements ContentEncoder, DataStreamChannel { + private boolean complete = false; + private final OutputStream outputStream; + private final int pushBackPeriod; + + private int written = 0; + private boolean pushedBack = false; + + private OutputStreamContentEncoder(OutputStream outputStream, int pushBackPeriod) { + this.outputStream = outputStream; + this.pushBackPeriod = pushBackPeriod; + } + + @Override + public void requestOutput() { + + } + + @Override + public int write(ByteBuffer src) throws IOException { + int toWrite = src.remaining(); + if ( !pushedBack && ( written % pushBackPeriod ) != ( ( written + toWrite ) % pushBackPeriod ) ) { + // push back + pushedBack = true; + return 0; + } + pushedBack = false; + outputStream.write( src.array(), src.arrayOffset(), toWrite ); + written += toWrite; + return toWrite; + } + + @Override + public void endStream() throws IOException { + complete = true; + } + + @Override + public void endStream(List trailers) throws IOException { + complete = true; + } + + @Override + public void complete(List trailers) { + this.complete = true; + } + + @Override + public boolean isCompleted() { + return this.complete; + } + } +} diff --git a/backend/elasticsearch/pom.xml b/backend/elasticsearch/pom.xml index 75a8c67ede9..126a024abb4 100644 --- a/backend/elasticsearch/pom.xml +++ b/backend/elasticsearch/pom.xml @@ -29,12 +29,13 @@ hibernate-search-engine - org.elasticsearch.client - elasticsearch-rest-client + org.hibernate.search + hibernate-search-backend-elasticsearch-client-common + - org.elasticsearch.client - elasticsearch-rest-client-sniffer + org.hibernate.search + hibernate-search-backend-elasticsearch-client-rest org.jboss.logging diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/analysis/impl/ElasticsearchAnalysisPerformer.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/analysis/impl/ElasticsearchAnalysisPerformer.java index 333c4df82ab..1a320795c47 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/analysis/impl/ElasticsearchAnalysisPerformer.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/analysis/impl/ElasticsearchAnalysisPerformer.java @@ -7,9 +7,9 @@ import java.util.List; import java.util.concurrent.CompletableFuture; +import org.hibernate.search.backend.elasticsearch.client.common.util.spi.URLEncodedString; import org.hibernate.search.backend.elasticsearch.document.model.impl.ElasticsearchIndexModel; import org.hibernate.search.backend.elasticsearch.orchestration.impl.ElasticsearchParallelWorkOrchestrator; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; import org.hibernate.search.backend.elasticsearch.work.factory.impl.ElasticsearchWorkFactory; import org.hibernate.search.backend.elasticsearch.work.impl.NonBulkableWork; import org.hibernate.search.engine.backend.analysis.AnalysisToken; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/cfg/ElasticsearchBackendSettings.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/cfg/ElasticsearchBackendSettings.java index 215f0b9e7aa..c59c185031c 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/cfg/ElasticsearchBackendSettings.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/cfg/ElasticsearchBackendSettings.java @@ -8,7 +8,6 @@ import java.util.List; import org.hibernate.search.backend.elasticsearch.ElasticsearchVersion; -import org.hibernate.search.backend.elasticsearch.client.ElasticsearchHttpClientConfigurer; import org.hibernate.search.backend.elasticsearch.index.layout.IndexLayoutStrategy; import org.hibernate.search.backend.elasticsearch.index.layout.impl.SimpleIndexLayoutStrategy; import org.hibernate.search.backend.elasticsearch.mapping.TypeNameMappingStrategyName; @@ -50,7 +49,10 @@ private ElasticsearchBackendSettings() { * Setting this property at the same time as {@link #URIS} will lead to an exception being thrown on startup. *

* Defaults to {@link Defaults#HOSTS}. + * + * @deprecated Use {@link org.hibernate.search.backend.elasticsearch.client.common.cfg.ElasticsearchBackendClientCommonSettings#HOSTS} instead. */ + @Deprecated(since = "8.2", forRemoval = true) public static final String HOSTS = "hosts"; /** @@ -61,7 +63,10 @@ private ElasticsearchBackendSettings() { * Setting this property at the same time as {@link #URIS} will lead to an exception being thrown on startup. *

* Defaults to {@link Defaults#PROTOCOL}. + * + * @deprecated Use {@link org.hibernate.search.backend.elasticsearch.client.common.cfg.ElasticsearchBackendClientCommonSettings#PROTOCOL} instead. */ + @Deprecated(since = "8.2", forRemoval = true) public static final String PROTOCOL = "protocol"; /** @@ -77,7 +82,10 @@ private ElasticsearchBackendSettings() { * Setting this property at the same time as {@link #HOSTS} or {@link #PROTOCOL} will lead to an exception being thrown on startup. *

* Defaults to {@code http://localhost:9200}, unless {@link #HOSTS} or {@link #PROTOCOL} are set, in which case they take precedence. + * + * @deprecated Use {@link org.hibernate.search.backend.elasticsearch.client.common.cfg.ElasticsearchBackendClientCommonSettings#URIS} instead. */ + @Deprecated(since = "8.2", forRemoval = true) public static final String URIS = "uris"; /** @@ -85,7 +93,10 @@ private ElasticsearchBackendSettings() { * Use the path prefix if your Elasticsearch instance is located at a specific context path. *

* Defaults to {@link Defaults#PATH_PREFIX}. + * + * @deprecated Use {@link org.hibernate.search.backend.elasticsearch.client.common.cfg.ElasticsearchBackendClientCommonSettings#PATH_PREFIX} instead. */ + @Deprecated(since = "8.2", forRemoval = true) public static final String PATH_PREFIX = "path_prefix"; /** @@ -118,7 +129,10 @@ private ElasticsearchBackendSettings() { * Expects a String. *

* Defaults to no username (anonymous access). + * + * @deprecated Use {@link org.hibernate.search.backend.elasticsearch.client.common.cfg.ElasticsearchBackendClientCommonSettings#USERNAME} instead. */ + @Deprecated(since = "8.2", forRemoval = true) public static final String USERNAME = "username"; /** @@ -127,7 +141,10 @@ private ElasticsearchBackendSettings() { * Expects a String. *

* Defaults to no username (anonymous access). + * + * @deprecated Use {@link org.hibernate.search.backend.elasticsearch.client.common.cfg.ElasticsearchBackendClientCommonSettings#PASSWORD} instead. */ + @Deprecated(since = "8.2", forRemoval = true) public static final String PASSWORD = "password"; /** @@ -139,7 +156,10 @@ private ElasticsearchBackendSettings() { * or a String that can be parsed into such Integer value. *

* Defaults to no request timeout. + * + * @deprecated Use {@link org.hibernate.search.backend.elasticsearch.client.rest.cfg.ClientRestElasticsearchBackendClientSettings#REQUEST_TIMEOUT} instead. */ + @Deprecated(since = "8.2", forRemoval = true) public static final String REQUEST_TIMEOUT = "request_timeout"; /** @@ -149,7 +169,10 @@ private ElasticsearchBackendSettings() { * or a String that can be parsed into such Integer value. *

* Defaults to {@link Defaults#READ_TIMEOUT}. + * + * @deprecated Use {@link org.hibernate.search.backend.elasticsearch.client.rest.cfg.ClientRestElasticsearchBackendClientSettings#READ_TIMEOUT} instead. */ + @Deprecated(since = "8.2", forRemoval = true) public static final String READ_TIMEOUT = "read_timeout"; /** @@ -159,7 +182,10 @@ private ElasticsearchBackendSettings() { * or a String that can be parsed into such Integer value. *

* Defaults to {@link Defaults#CONNECTION_TIMEOUT}. + * + * @deprecated Use {@link org.hibernate.search.backend.elasticsearch.client.rest.cfg.ClientRestElasticsearchBackendClientSettings#CONNECTION_TIMEOUT} instead. */ + @Deprecated(since = "8.2", forRemoval = true) public static final String CONNECTION_TIMEOUT = "connection_timeout"; /** @@ -170,7 +196,10 @@ private ElasticsearchBackendSettings() { * or a String that can be parsed into such Integer value. *

* Defaults to {@link Defaults#MAX_CONNECTIONS}. + * + * @deprecated Use {@link org.hibernate.search.backend.elasticsearch.client.rest.cfg.ClientRestElasticsearchBackendClientSettings#MAX_CONNECTIONS} instead. */ + @Deprecated(since = "8.2", forRemoval = true) public static final String MAX_CONNECTIONS = "max_connections"; /** @@ -180,7 +209,10 @@ private ElasticsearchBackendSettings() { * or a String that can be parsed into such Integer value. *

* Defaults to {@link Defaults#MAX_CONNECTIONS_PER_ROUTE}. + * + * @deprecated Use {@link org.hibernate.search.backend.elasticsearch.client.rest.cfg.ClientRestElasticsearchBackendClientSettings#MAX_CONNECTIONS_PER_ROUTE} instead. */ + @Deprecated(since = "8.2", forRemoval = true) public static final String MAX_CONNECTIONS_PER_ROUTE = "max_connections_per_route"; /** @@ -190,7 +222,10 @@ private ElasticsearchBackendSettings() { * or a string that can be parsed into a Boolean value. *

* Defaults to {@link Defaults#DISCOVERY_ENABLED}. + * + * @deprecated Use {@link org.hibernate.search.backend.elasticsearch.client.rest.cfg.ClientRestElasticsearchBackendClientSettings#DISCOVERY_ENABLED} instead. */ + @Deprecated(since = "8.2", forRemoval = true) public static final String DISCOVERY_ENABLED = "discovery.enabled"; /** @@ -200,19 +235,25 @@ private ElasticsearchBackendSettings() { * or a String that can be parsed into such Integer value. *

* Defaults to {@link Defaults#DISCOVERY_REFRESH_INTERVAL}. + * + * @deprecated Use {@link org.hibernate.search.backend.elasticsearch.client.rest.cfg.ClientRestElasticsearchBackendClientSettings#DISCOVERY_REFRESH_INTERVAL} instead. */ + @Deprecated(since = "8.2", forRemoval = true) public static final String DISCOVERY_REFRESH_INTERVAL = "discovery.refresh_interval"; /** - * A {@link ElasticsearchHttpClientConfigurer} that defines custom HTTP client configuration. + * A {@link org.hibernate.search.backend.elasticsearch.client.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 #USERNAME} or {@link #MAX_CONNECTIONS_PER_ROUTE}. *

- * Expects a reference to a bean of type {@link ElasticsearchHttpClientConfigurer}. + * Expects a reference to a bean of type {@link org.hibernate.search.backend.elasticsearch.client.ElasticsearchHttpClientConfigurer}. *

* Defaults to no value. + * + * @deprecated Use client specific configurers instead. */ + @Deprecated(since = "8.2", forRemoval = true) public static final String CLIENT_CONFIGURER = "client.configurer"; /** @@ -298,7 +339,10 @@ private ElasticsearchBackendSettings() { *

* If this property is not set, only the {@code Keep-Alive} header is considered, * and if it's absent, idle connections will be kept forever. + * + * @deprecated Use {@link org.hibernate.search.backend.elasticsearch.client.rest.cfg.ClientRestElasticsearchBackendClientSettings#MAX_KEEP_ALIVE} instead. */ + @Deprecated(since = "8.2", forRemoval = true) public static final String MAX_KEEP_ALIVE = "max_keep_alive"; /** @@ -326,14 +370,50 @@ public static final class Defaults { private Defaults() { } + /** + * @deprecated Use {@link org.hibernate.search.backend.elasticsearch.client.common.cfg.ElasticsearchBackendClientCommonSettings.Defaults#HOSTS} instead. + */ + @Deprecated(since = "8.2", forRemoval = true) public static final List HOSTS = Collections.singletonList( "localhost:9200" ); + /** + * @deprecated Use {@link org.hibernate.search.backend.elasticsearch.client.common.cfg.ElasticsearchBackendClientCommonSettings.Defaults#PROTOCOL} instead. + */ + @Deprecated(since = "8.2", forRemoval = true) public static final String PROTOCOL = "http"; + /** + * @deprecated Use {@link org.hibernate.search.backend.elasticsearch.client.common.cfg.ElasticsearchBackendClientCommonSettings.Defaults#PATH_PREFIX} instead. + */ + @Deprecated(since = "8.2", forRemoval = true) public static final String PATH_PREFIX = ""; + /** + * @deprecated Use {@link org.hibernate.search.backend.elasticsearch.client.rest.cfg.ClientRestElasticsearchBackendClientSettings.Defaults#READ_TIMEOUT} instead. + */ + @Deprecated(since = "8.2", forRemoval = true) public static final int READ_TIMEOUT = 30000; + /** + * @deprecated Use {@link org.hibernate.search.backend.elasticsearch.client.rest.cfg.ClientRestElasticsearchBackendClientSettings.Defaults#CONNECTION_TIMEOUT} instead. + */ + @Deprecated(since = "8.2", forRemoval = true) public static final int CONNECTION_TIMEOUT = 1000; + /** + * @deprecated Use {@link org.hibernate.search.backend.elasticsearch.client.rest.cfg.ClientRestElasticsearchBackendClientSettings.Defaults#MAX_CONNECTIONS} instead. + */ + @Deprecated(since = "8.2", forRemoval = true) public static final int MAX_CONNECTIONS = 40; + /** + * @deprecated Use {@link org.hibernate.search.backend.elasticsearch.client.rest.cfg.ClientRestElasticsearchBackendClientSettings.Defaults#MAX_CONNECTIONS_PER_ROUTE} instead. + */ + @Deprecated(since = "8.2", forRemoval = true) public static final int MAX_CONNECTIONS_PER_ROUTE = 20; + /** + * @deprecated Use {@link org.hibernate.search.backend.elasticsearch.client.rest.cfg.ClientRestElasticsearchBackendClientSettings.Defaults#DISCOVERY_ENABLED} instead. + */ + @Deprecated(since = "8.2", forRemoval = true) public static final boolean DISCOVERY_ENABLED = false; + /** + * @deprecated Use {@link org.hibernate.search.backend.elasticsearch.client.rest.cfg.ClientRestElasticsearchBackendClientSettings.Defaults#DISCOVERY_REFRESH_INTERVAL} instead. + */ + @Deprecated(since = "8.2", forRemoval = true) public static final int DISCOVERY_REFRESH_INTERVAL = 10; public static final boolean LOG_JSON_PRETTY_PRINTING = false; /** diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/cfg/impl/ElasticsearchBackendImplSettings.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/cfg/impl/ElasticsearchBackendImplSettings.java index cceec7567bb..7b6447b00cc 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/cfg/impl/ElasticsearchBackendImplSettings.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/cfg/impl/ElasticsearchBackendImplSettings.java @@ -7,6 +7,7 @@ /** * Implementation-related settings, used for testing only. */ +@Deprecated(since = "8.2", forRemoval = true) public final class ElasticsearchBackendImplSettings { private ElasticsearchBackendImplSettings() { diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/cfg/spi/ElasticsearchBackendSpiSettings.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/cfg/spi/ElasticsearchBackendSpiSettings.java index 2d44c16270c..6a6954cb1d5 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/cfg/spi/ElasticsearchBackendSpiSettings.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/cfg/spi/ElasticsearchBackendSpiSettings.java @@ -28,25 +28,6 @@ public final class ElasticsearchBackendSpiSettings { */ public static final String BACKEND_WORK_EXECUTOR_PROVIDER = PREFIX + Radicals.BACKEND_WORK_EXECUTOR_PROVIDER; - /** - * An external Elasticsearch 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 Elasticsearch, - * and all other client-related configuration properties - * (hosts/uris, authentication, discovery, timeouts, max connections, configurer, ...) - * will be ignored. - *

- * Expects a reference to a bean of type {@link org.elasticsearch.client.RestClient}. - *

- * 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 ElasticsearchBackendSpiSettings() { } diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/ElasticsearchHttpClientConfigurationContext.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/ElasticsearchHttpClientConfigurationContext.java index 0bf609052ac..4227af62867 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/ElasticsearchHttpClientConfigurationContext.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/ElasticsearchHttpClientConfigurationContext.java @@ -7,39 +7,31 @@ import java.util.Optional; import org.hibernate.search.backend.elasticsearch.ElasticsearchVersion; -import org.hibernate.search.engine.cfg.ConfigurationPropertySource; -import org.hibernate.search.engine.environment.bean.BeanResolver; - -import org.apache.http.impl.nio.client.HttpAsyncClientBuilder; +import org.hibernate.search.backend.elasticsearch.cfg.ElasticsearchBackendSettings; +import org.hibernate.search.engine.cfg.spi.ConfigurationProperty; /** - * The context passed to {@link ElasticsearchHttpClientConfigurer}. + * The context passed to {@link ElasticsearchHttpClientConfigurer} + * + * @deprecated Use the client specific configurers instead. */ -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 An Apache HTTP client builder, to set the configuration. - * @see the Apache HTTP Client documentation - */ - HttpAsyncClientBuilder clientBuilder(); +@Deprecated(since = "8.2", forRemoval = true) +public interface ElasticsearchHttpClientConfigurationContext + extends + org.hibernate.search.backend.elasticsearch.client.rest.ElasticsearchHttpClientConfigurationContext { /** * @return The version of Elasticsearch/OpenSearch configured on the backend. * May be empty if not configured explicitly (in which case it will only be known after the client is built). + * + * @deprecated Use the {@link #configurationPropertySource() property source} and inspect the corresponding properties + * (e.g. {@link ElasticsearchBackendSettings#VERSION}) instead. */ - Optional configuredVersion(); + @Deprecated(since = "8.2", forRemoval = true) + default Optional configuredVersion() { + return ConfigurationProperty.forKey( ElasticsearchBackendSettings.VERSION ) + .as( ElasticsearchVersion.class, ElasticsearchVersion::of ) + .build().get( configurationPropertySource() ); + } } diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/ElasticsearchHttpClientConfigurer.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/ElasticsearchHttpClientConfigurer.java index 0ac05f568b0..01f2fe17d9a 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/ElasticsearchHttpClientConfigurer.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/ElasticsearchHttpClientConfigurer.java @@ -4,6 +4,8 @@ */ package org.hibernate.search.backend.elasticsearch.client; +import org.hibernate.search.backend.elasticsearch.client.impl.ElasticsearchHttpClientConfigurationContextDelegate; + /** * An extension point allowing fine tuning of the Apache HTTP Client used by the Elasticsearch integration. *

@@ -15,8 +17,13 @@ *

* 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. + * + * @deprecated Use the client specific configurers instead. */ -public interface ElasticsearchHttpClientConfigurer { +@SuppressWarnings("removal") +@Deprecated(since = "8.2", forRemoval = true) +public interface ElasticsearchHttpClientConfigurer + extends org.hibernate.search.backend.elasticsearch.client.rest.ElasticsearchHttpClientConfigurer { /** * Configure the HTTP Client. @@ -34,4 +41,10 @@ public interface ElasticsearchHttpClientConfigurer { */ void configure(ElasticsearchHttpClientConfigurationContext context); + @Override + default void configure( + org.hibernate.search.backend.elasticsearch.client.rest.ElasticsearchHttpClientConfigurationContext context) { + configure( new ElasticsearchHttpClientConfigurationContextDelegate( context ) ); + } + } diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/ElasticsearchClientUtils.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/ElasticsearchClientUtils.java index 3a94867a019..e9801c17002 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/ElasticsearchClientUtils.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/ElasticsearchClientUtils.java @@ -4,25 +4,18 @@ */ package org.hibernate.search.backend.elasticsearch.client.impl; -import java.io.IOException; -import java.util.List; import org.hibernate.search.backend.elasticsearch.ElasticsearchDistributionName; import org.hibernate.search.backend.elasticsearch.ElasticsearchVersion; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchClient; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchRequest; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchResponse; +import org.hibernate.search.backend.elasticsearch.client.common.logging.spi.ElasticsearchClientLog; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchClient; +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.gson.impl.JsonAccessor; -import org.hibernate.search.backend.elasticsearch.logging.impl.ElasticsearchClientLog; import org.hibernate.search.util.common.AssertionFailure; import org.hibernate.search.util.common.impl.Futures; -import com.google.gson.Gson; -import com.google.gson.JsonObject; - -import org.apache.http.HttpEntity; - -public class ElasticsearchClientUtils { +public final class ElasticsearchClientUtils { private static final JsonAccessor DISTRIBUTION_ACCESSOR = JsonAccessor.root().property( "version" ).property( "distribution" ).asString(); @@ -37,14 +30,6 @@ public static boolean isSuccessCode(int code) { return 200 <= code && code < 300; } - public static HttpEntity toEntity(Gson gson, ElasticsearchRequest request) throws IOException { - final List bodyParts = request.bodyParts(); - if ( bodyParts.isEmpty() ) { - return null; - } - return new GsonHttpEntity( gson, bodyParts ); - } - public static ElasticsearchVersion tryGetElasticsearchVersion(ElasticsearchClient client) { ElasticsearchRequest request = ElasticsearchRequest.get().build(); ElasticsearchResponse response = null; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/ElasticsearchHttpClientConfigurationContextDelegate.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/ElasticsearchHttpClientConfigurationContextDelegate.java new file mode 100644 index 00000000000..74b19a63cce --- /dev/null +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/ElasticsearchHttpClientConfigurationContextDelegate.java @@ -0,0 +1,37 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.client.impl; + +import org.hibernate.search.backend.elasticsearch.client.ElasticsearchHttpClientConfigurationContext; +import org.hibernate.search.engine.cfg.ConfigurationPropertySource; +import org.hibernate.search.engine.environment.bean.BeanResolver; + +import org.apache.http.impl.nio.client.HttpAsyncClientBuilder; + +@SuppressWarnings("removal") +public class ElasticsearchHttpClientConfigurationContextDelegate implements ElasticsearchHttpClientConfigurationContext { + + private final org.hibernate.search.backend.elasticsearch.client.rest.ElasticsearchHttpClientConfigurationContext context; + + public ElasticsearchHttpClientConfigurationContextDelegate( + org.hibernate.search.backend.elasticsearch.client.rest.ElasticsearchHttpClientConfigurationContext context) { + this.context = context; + } + + @Override + public BeanResolver beanResolver() { + return context.beanResolver(); + } + + @Override + public ConfigurationPropertySource configurationPropertySource() { + return context.configurationPropertySource(); + } + + @Override + public HttpAsyncClientBuilder clientBuilder() { + return context.clientBuilder(); + } +} diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/Paths.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/Paths.java index f222f6998bb..980b6170c57 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/Paths.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/Paths.java @@ -4,7 +4,7 @@ */ package org.hibernate.search.backend.elasticsearch.client.impl; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; +import org.hibernate.search.backend.elasticsearch.client.common.util.spi.URLEncodedString; /** * Useful paths to compose Elasticsearch URLs. diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/dialect/protocol/impl/AmazonOpenSearchServerlessProtocolDialect.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/dialect/protocol/impl/AmazonOpenSearchServerlessProtocolDialect.java index 837e86507ec..3f03f0aeb74 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/dialect/protocol/impl/AmazonOpenSearchServerlessProtocolDialect.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/dialect/protocol/impl/AmazonOpenSearchServerlessProtocolDialect.java @@ -4,7 +4,7 @@ */ package org.hibernate.search.backend.elasticsearch.dialect.protocol.impl; -import org.hibernate.search.backend.elasticsearch.gson.spi.GsonProvider; +import org.hibernate.search.backend.elasticsearch.client.common.gson.spi.GsonProvider; import org.hibernate.search.backend.elasticsearch.work.factory.impl.AmazonOpenSearchServerlessWorkFactory; import org.hibernate.search.backend.elasticsearch.work.factory.impl.ElasticsearchWorkFactory; import org.hibernate.search.util.common.annotation.Incubating; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/dialect/protocol/impl/Elasticsearch70ProtocolDialect.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/dialect/protocol/impl/Elasticsearch70ProtocolDialect.java index be4fcf8a21e..43f080a6b77 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/dialect/protocol/impl/Elasticsearch70ProtocolDialect.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/dialect/protocol/impl/Elasticsearch70ProtocolDialect.java @@ -4,7 +4,7 @@ */ package org.hibernate.search.backend.elasticsearch.dialect.protocol.impl; -import org.hibernate.search.backend.elasticsearch.gson.spi.GsonProvider; +import org.hibernate.search.backend.elasticsearch.client.common.gson.spi.GsonProvider; import org.hibernate.search.backend.elasticsearch.lowlevel.syntax.metadata.impl.Elasticsearch7IndexMetadataSyntax; import org.hibernate.search.backend.elasticsearch.lowlevel.syntax.metadata.impl.ElasticsearchIndexMetadataSyntax; import org.hibernate.search.backend.elasticsearch.lowlevel.syntax.search.impl.Elasticsearch7SearchSyntax; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/dialect/protocol/impl/Elasticsearch80ProtocolDialect.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/dialect/protocol/impl/Elasticsearch80ProtocolDialect.java index e1e75cc5530..2c3779145e2 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/dialect/protocol/impl/Elasticsearch80ProtocolDialect.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/dialect/protocol/impl/Elasticsearch80ProtocolDialect.java @@ -4,7 +4,7 @@ */ package org.hibernate.search.backend.elasticsearch.dialect.protocol.impl; -import org.hibernate.search.backend.elasticsearch.gson.spi.GsonProvider; +import org.hibernate.search.backend.elasticsearch.client.common.gson.spi.GsonProvider; import org.hibernate.search.backend.elasticsearch.lowlevel.syntax.metadata.impl.Elasticsearch7IndexMetadataSyntax; import org.hibernate.search.backend.elasticsearch.lowlevel.syntax.metadata.impl.ElasticsearchIndexMetadataSyntax; import org.hibernate.search.backend.elasticsearch.lowlevel.syntax.search.impl.Elasticsearch7SearchSyntax; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/dialect/protocol/impl/Elasticsearch81ProtocolDialect.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/dialect/protocol/impl/Elasticsearch81ProtocolDialect.java index c76ba52b510..646dfffb53f 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/dialect/protocol/impl/Elasticsearch81ProtocolDialect.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/dialect/protocol/impl/Elasticsearch81ProtocolDialect.java @@ -4,7 +4,7 @@ */ package org.hibernate.search.backend.elasticsearch.dialect.protocol.impl; -import org.hibernate.search.backend.elasticsearch.gson.spi.GsonProvider; +import org.hibernate.search.backend.elasticsearch.client.common.gson.spi.GsonProvider; import org.hibernate.search.backend.elasticsearch.lowlevel.syntax.metadata.impl.Elasticsearch7IndexMetadataSyntax; import org.hibernate.search.backend.elasticsearch.lowlevel.syntax.metadata.impl.ElasticsearchIndexMetadataSyntax; import org.hibernate.search.backend.elasticsearch.lowlevel.syntax.search.impl.Elasticsearch81SearchSyntax; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/dialect/protocol/impl/ElasticsearchProtocolDialect.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/dialect/protocol/impl/ElasticsearchProtocolDialect.java index 62e98c311a9..2ea8676f8b5 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/dialect/protocol/impl/ElasticsearchProtocolDialect.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/dialect/protocol/impl/ElasticsearchProtocolDialect.java @@ -4,8 +4,8 @@ */ package org.hibernate.search.backend.elasticsearch.dialect.protocol.impl; +import org.hibernate.search.backend.elasticsearch.client.common.gson.spi.GsonProvider; import org.hibernate.search.backend.elasticsearch.dialect.model.impl.ElasticsearchModelDialect; -import org.hibernate.search.backend.elasticsearch.gson.spi.GsonProvider; import org.hibernate.search.backend.elasticsearch.lowlevel.syntax.metadata.impl.ElasticsearchIndexMetadataSyntax; import org.hibernate.search.backend.elasticsearch.lowlevel.syntax.search.impl.ElasticsearchSearchSyntax; import org.hibernate.search.backend.elasticsearch.search.query.impl.ElasticsearchSearchResultExtractorFactory; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/document/impl/ElasticsearchDocumentObjectBuilder.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/document/impl/ElasticsearchDocumentObjectBuilder.java index 09ea2e54350..e7f95bc7ee0 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/document/impl/ElasticsearchDocumentObjectBuilder.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/document/impl/ElasticsearchDocumentObjectBuilder.java @@ -6,13 +6,13 @@ import java.util.Objects; +import org.hibernate.search.backend.elasticsearch.client.common.logging.spi.ElasticsearchClientLog; import org.hibernate.search.backend.elasticsearch.document.model.impl.ElasticsearchIndexCompositeNode; import org.hibernate.search.backend.elasticsearch.document.model.impl.ElasticsearchIndexField; import org.hibernate.search.backend.elasticsearch.document.model.impl.ElasticsearchIndexModel; import org.hibernate.search.backend.elasticsearch.document.model.impl.ElasticsearchIndexObjectField; import org.hibernate.search.backend.elasticsearch.document.model.impl.ElasticsearchIndexValueField; import org.hibernate.search.backend.elasticsearch.gson.impl.GsonUtils; -import org.hibernate.search.backend.elasticsearch.logging.impl.ElasticsearchClientLog; import org.hibernate.search.backend.elasticsearch.logging.impl.IndexingLog; import org.hibernate.search.backend.elasticsearch.types.impl.ElasticsearchIndexValueFieldType; import org.hibernate.search.engine.backend.common.spi.FieldPaths; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/document/model/lowlevel/impl/LowLevelIndexMetadataBuilder.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/document/model/lowlevel/impl/LowLevelIndexMetadataBuilder.java index f889b78ed2d..9f426e40875 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/document/model/lowlevel/impl/LowLevelIndexMetadataBuilder.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/document/model/lowlevel/impl/LowLevelIndexMetadataBuilder.java @@ -8,8 +8,8 @@ import java.util.Map; import org.hibernate.search.backend.elasticsearch.analysis.model.impl.ElasticsearchAnalysisDefinitionRegistry; +import org.hibernate.search.backend.elasticsearch.client.common.gson.spi.GsonProvider; import org.hibernate.search.backend.elasticsearch.gson.impl.GsonUtils; -import org.hibernate.search.backend.elasticsearch.gson.spi.GsonProvider; import org.hibernate.search.backend.elasticsearch.index.layout.impl.IndexNames; import org.hibernate.search.backend.elasticsearch.lowlevel.index.aliases.impl.IndexAliasDefinition; import org.hibernate.search.backend.elasticsearch.lowlevel.index.impl.IndexMetadata; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/gson/spi/GsonProviderHelper.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/gson/spi/GsonProviderHelper.java new file mode 100644 index 00000000000..84f3ac8e9ab --- /dev/null +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/gson/spi/GsonProviderHelper.java @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.backend.elasticsearch.gson.spi; + +import java.util.Set; +import java.util.function.Supplier; + +import org.hibernate.search.backend.elasticsearch.client.common.gson.spi.GsonProvider; +import org.hibernate.search.backend.elasticsearch.lowlevel.index.mapping.impl.RootTypeMapping; +import org.hibernate.search.backend.elasticsearch.lowlevel.index.settings.impl.IndexSettings; +import org.hibernate.search.util.common.impl.CollectionHelper; + +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; + +/** + * Centralizes the configuration of the Gson objects. + */ +public final class GsonProviderHelper { + + private GsonProviderHelper() { + } + + /* + * See https://github.com/google/gson/issues/764. + * + * Only composite type adapters (referring to another type adapter) + * should be affected by this bug, so we'll list the corresponding types here. + * Maybe it's even narrower, and only type adapters that indirectly refer to themselves are affected, + * but I'm not entirely sure about that. + * + * Note we only need to list "root" types: + * all types they refer to will also have their type adapter initialized. + */ + private static final Set> TYPES_CAUSING_GSON_CONCURRENT_INITIALIZATION_BUG = + CollectionHelper.asImmutableSet( + TypeToken.get( IndexSettings.class ), + TypeToken.get( RootTypeMapping.class ) + ); + + public static GsonProvider create(Supplier builderBaseSupplier, boolean logPrettyPrinting) { + return GsonProvider.create( builderBaseSupplier, logPrettyPrinting, TYPES_CAUSING_GSON_CONCURRENT_INITIALIZATION_BUG ); + } + +} 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 8f0bd745390..55c4355cec9 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,17 +4,18 @@ */ package org.hibernate.search.backend.elasticsearch.impl; +import java.util.List; import java.util.Locale; import java.util.Optional; import org.hibernate.search.backend.elasticsearch.ElasticsearchVersion; import org.hibernate.search.backend.elasticsearch.cfg.ElasticsearchBackendSettings; -import org.hibernate.search.backend.elasticsearch.cfg.impl.ElasticsearchBackendImplSettings; -import org.hibernate.search.backend.elasticsearch.client.impl.ElasticsearchClientFactoryImpl; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchClientFactory; +import org.hibernate.search.backend.elasticsearch.client.common.cfg.spi.ElasticsearchBackendClientSpiSettings; +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.dialect.impl.ElasticsearchDialectFactory; import org.hibernate.search.backend.elasticsearch.dialect.model.impl.ElasticsearchModelDialect; -import org.hibernate.search.backend.elasticsearch.gson.spi.GsonProvider; +import org.hibernate.search.backend.elasticsearch.gson.spi.GsonProviderHelper; import org.hibernate.search.backend.elasticsearch.logging.impl.ConfigurationLog; import org.hibernate.search.backend.elasticsearch.mapping.TypeNameMappingStrategyName; import org.hibernate.search.backend.elasticsearch.mapping.impl.DiscriminatorTypeNameMapping; @@ -37,6 +38,7 @@ import org.hibernate.search.engine.environment.bean.BeanReference; import org.hibernate.search.engine.environment.bean.BeanResolver; import org.hibernate.search.util.common.AssertionFailure; +import org.hibernate.search.util.common.SearchException; import org.hibernate.search.util.common.impl.SuppressingCloser; import org.hibernate.search.util.common.reporting.EventContext; @@ -57,7 +59,7 @@ public class ElasticsearchBackendFactory implements BackendFactory { .build(); private static final OptionalConfigurationProperty> CLIENT_FACTORY = - ConfigurationProperty.forKey( ElasticsearchBackendImplSettings.CLIENT_FACTORY ) + ConfigurationProperty.forKey( ElasticsearchBackendClientSpiSettings.CLIENT_FACTORY ) .asBeanReference( ElasticsearchClientFactory.class ) .build(); @@ -76,7 +78,7 @@ public BackendImplementor create(EventContext eventContext, BackendBuildContext * vice-versa, it doesn't need a Gson instance that was specially * configured for a particular Elasticsearch version. */ - GsonProvider defaultGsonProvider = GsonProvider.create( GsonBuilder::new, logPrettyPrinting ); + GsonProvider defaultGsonProvider = GsonProviderHelper.create( GsonBuilder::new, logPrettyPrinting ); Optional configuredVersion = ElasticsearchLinkImpl.VERSION.get( propertySource ); @@ -87,15 +89,51 @@ public BackendImplementor create(EventContext eventContext, BackendBuildContext try { threads = new BackendThreads( eventContext.render() ); + // First, let's see if the factory was configured explicitly: Optional> customClientFactoryHolderOptional = CLIENT_FACTORY.getAndMap( propertySource, beanResolver::resolve ); if ( customClientFactoryHolderOptional.isPresent() ) { clientFactoryHolder = customClientFactoryHolderOptional.get(); - ConfigurationLog.INSTANCE.backendClientFactory( clientFactoryHolder, eventContext.render() ); } else { - clientFactoryHolder = BeanHolder.of( new ElasticsearchClientFactoryImpl() ); + // otherwise let's find all client factories and pick + List> clientFactoryReferences = + beanResolver.allConfiguredForRole( ElasticsearchClientFactory.class ); + if ( clientFactoryReferences.isEmpty() ) { + throw ConfigurationLog.INSTANCE.backendClientFactoryNotConfigured( eventContext.render() ); + } + // if there's just one -- use it: + 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 ) { + try { + 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 ); + } + else if ( second == defaultFactoryReference ) { + clientFactoryHolder = first.resolve( beanResolver ); + } + } + catch (SearchException e) { + // ignore and fail later; + } + } + if ( clientFactoryHolder == null ) { + throw ConfigurationLog.INSTANCE.backendClientFactoryMultipleConfigured( + clientFactoryReferences.stream().map( ref -> ref.resolve( beanResolver ) ) + .toList(), + eventContext.render() + ); + } } + ConfigurationLog.INSTANCE.backendClientFactory( clientFactoryHolder, eventContext.render() ); ElasticsearchDialectFactory dialectFactory = new ElasticsearchDialectFactory(); link = new ElasticsearchLinkImpl( diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/impl/ElasticsearchLinkImpl.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/impl/ElasticsearchLinkImpl.java index 0416aa01722..b0f1a4e871e 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/impl/ElasticsearchLinkImpl.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/impl/ElasticsearchLinkImpl.java @@ -8,13 +8,15 @@ import org.hibernate.search.backend.elasticsearch.ElasticsearchVersion; 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.ElasticsearchClient; +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.util.spi.URLEncodedString; import org.hibernate.search.backend.elasticsearch.client.impl.ElasticsearchClientUtils; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchClient; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchClientFactory; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchClientImplementor; import org.hibernate.search.backend.elasticsearch.dialect.impl.ElasticsearchDialectFactory; import org.hibernate.search.backend.elasticsearch.dialect.protocol.impl.ElasticsearchProtocolDialect; -import org.hibernate.search.backend.elasticsearch.gson.spi.GsonProvider; +import org.hibernate.search.backend.elasticsearch.gson.spi.GsonProviderHelper; import org.hibernate.search.backend.elasticsearch.index.layout.IndexLayoutStrategy; import org.hibernate.search.backend.elasticsearch.index.layout.impl.IndexNames; import org.hibernate.search.backend.elasticsearch.link.impl.ElasticsearchLink; @@ -28,7 +30,6 @@ import org.hibernate.search.backend.elasticsearch.search.projection.impl.ProjectionExtractionHelper; import org.hibernate.search.backend.elasticsearch.search.projection.impl.SearchProjectionBackendContext; import org.hibernate.search.backend.elasticsearch.search.query.impl.ElasticsearchSearchResultExtractorFactory; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; import org.hibernate.search.backend.elasticsearch.work.factory.impl.ElasticsearchWorkFactory; import org.hibernate.search.engine.cfg.ConfigurationPropertySource; import org.hibernate.search.engine.cfg.spi.ConfigurationProperty; @@ -207,15 +208,14 @@ void onStart(BeanResolver beanResolver, MultiTenancyStrategy multiTenancyStrateg if ( clientImplementor == null ) { clientImplementor = clientFactoryHolder.get().create( beanResolver, propertySource, threads.getThreadProvider(), threads.getPrefix(), - threads.getWorkExecutor(), defaultGsonProvider, - configuredVersionOnBackendCreationOptional + threads.getWorkExecutor(), defaultGsonProvider ); clientFactoryHolder.close(); // We won't need it anymore elasticsearchVersion = initVersion( propertySource ); ElasticsearchProtocolDialect protocolDialect = dialectFactory.createProtocolDialect( elasticsearchVersion ); - gsonProvider = GsonProvider.create( GsonBuilder::new, logPrettyPrinting ); + gsonProvider = GsonProviderHelper.create( GsonBuilder::new, logPrettyPrinting ); indexMetadataSyntax = protocolDialect.createIndexMetadataSyntax(); searchSyntax = protocolDialect.createSearchSyntax(); workFactory = protocolDialect.createWorkFactory( gsonProvider, QUERY_SHARD_FAILURE_IGNORE.get( propertySource ) ); diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/index/impl/ElasticsearchIndexManagerImpl.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/index/impl/ElasticsearchIndexManagerImpl.java index 4b493798707..afd73e71050 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/index/impl/ElasticsearchIndexManagerImpl.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/index/impl/ElasticsearchIndexManagerImpl.java @@ -11,6 +11,7 @@ import org.hibernate.search.backend.elasticsearch.ElasticsearchBackend; import org.hibernate.search.backend.elasticsearch.analysis.impl.ElasticsearchAnalysisPerformer; +import org.hibernate.search.backend.elasticsearch.client.common.util.spi.URLEncodedString; import org.hibernate.search.backend.elasticsearch.document.impl.DocumentMetadataContributor; import org.hibernate.search.backend.elasticsearch.document.impl.ElasticsearchDocumentObjectBuilder; import org.hibernate.search.backend.elasticsearch.document.model.impl.ElasticsearchIndexModel; @@ -21,7 +22,6 @@ import org.hibernate.search.backend.elasticsearch.metamodel.ElasticsearchIndexDescriptor; import org.hibernate.search.backend.elasticsearch.orchestration.impl.ElasticsearchBatchingWorkOrchestrator; import org.hibernate.search.backend.elasticsearch.schema.management.impl.ElasticsearchIndexSchemaManager; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; import org.hibernate.search.backend.elasticsearch.work.execution.impl.WorkExecutionIndexManagerContext; import org.hibernate.search.engine.backend.analysis.AnalysisToken; import org.hibernate.search.engine.backend.index.IndexManager; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/index/layout/impl/IndexNames.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/index/layout/impl/IndexNames.java index c4e1aaa62db..d1412245111 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/index/layout/impl/IndexNames.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/index/layout/impl/IndexNames.java @@ -6,8 +6,8 @@ import java.util.Locale; +import org.hibernate.search.backend.elasticsearch.client.common.util.spi.URLEncodedString; import org.hibernate.search.backend.elasticsearch.logging.impl.ElasticsearchMiscLog; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; public final class IndexNames { diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/index/layout/impl/SimpleIndexLayoutStrategy.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/index/layout/impl/SimpleIndexLayoutStrategy.java index 32679418190..ebbff9557ce 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/index/layout/impl/SimpleIndexLayoutStrategy.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/index/layout/impl/SimpleIndexLayoutStrategy.java @@ -7,8 +7,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.hibernate.search.backend.elasticsearch.client.common.logging.spi.ElasticsearchClientLog; import org.hibernate.search.backend.elasticsearch.index.layout.IndexLayoutStrategy; -import org.hibernate.search.backend.elasticsearch.logging.impl.ElasticsearchClientLog; /** * A simple layout strategy for indexes: diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/link/impl/ElasticsearchLink.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/link/impl/ElasticsearchLink.java index fc1b7e63f25..ef4d04d347c 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/link/impl/ElasticsearchLink.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/link/impl/ElasticsearchLink.java @@ -4,8 +4,8 @@ */ package org.hibernate.search.backend.elasticsearch.link.impl; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchClient; -import org.hibernate.search.backend.elasticsearch.gson.spi.GsonProvider; +import org.hibernate.search.backend.elasticsearch.client.common.gson.spi.GsonProvider; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchClient; import org.hibernate.search.backend.elasticsearch.index.layout.IndexLayoutStrategy; import org.hibernate.search.backend.elasticsearch.index.layout.impl.IndexNames; import org.hibernate.search.backend.elasticsearch.lowlevel.syntax.metadata.impl.ElasticsearchIndexMetadataSyntax; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/logging/impl/ConfigurationLog.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/logging/impl/ConfigurationLog.java index cba86fc9061..357e4a7e40b 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/logging/impl/ConfigurationLog.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/logging/impl/ConfigurationLog.java @@ -12,6 +12,7 @@ import java.util.List; import java.util.Set; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchClientFactory; import org.hibernate.search.engine.environment.bean.BeanHolder; import org.hibernate.search.util.common.SearchException; import org.hibernate.search.util.common.logging.CategorizedLogger; @@ -19,7 +20,6 @@ import org.hibernate.search.util.common.logging.impl.MessageConstants; import org.hibernate.search.util.common.reporting.EventContext; -import org.jboss.logging.annotations.Cause; import org.jboss.logging.annotations.LogMessage; import org.jboss.logging.annotations.Message; import org.jboss.logging.annotations.MessageLogger; @@ -67,11 +67,6 @@ public interface ConfigurationLog { + " Valid names are: %2$s.") SearchException invalidElasticsearchDistributionName(String invalidRepresentation, List validRepresentations); - @Message(id = ID_OFFSET + 89, value = "Invalid host/port: '%1$s'." - + " The host/port string must use the format 'host:port', for example 'mycompany.com:9200'" - + " The URI scheme ('http://', 'https://') must not be included.") - SearchException invalidHostAndPort(String hostAndPort, @Cause Exception e); - @Message(id = ID_OFFSET + 91, value = "Invalid name for the type-name mapping strategy: '%1$s'." + " Valid names are: %2$s.") SearchException invalidTypeNameMappingStrategyName(String invalidRepresentation, List validRepresentations); @@ -80,33 +75,6 @@ public interface ConfigurationLog { + " Valid values are: %2$s.") SearchException invalidDynamicType(String invalidRepresentation, List validRepresentations); - @Message(id = ID_OFFSET + 126, value = "Invalid target hosts configuration:" - + " both the 'uris' property and the 'protocol' property are set." - + " Uris: '%1$s'. Protocol: '%2$s'." - + " Either set the protocol and hosts simultaneously using the 'uris' property," - + " or set them separately using the 'protocol' property and the 'hosts' property.") - SearchException uriAndProtocol(List uris, String protocol); - - @Message(id = ID_OFFSET + 127, value = "Invalid target hosts configuration:" - + " both the 'uris' property and the 'hosts' property are set." - + " Uris: '%1$s'. Hosts: '%2$s'." - + " Either set the protocol and hosts simultaneously using the 'uris' property," - + " or set them separately using the 'protocol' property and the 'hosts' property.") - SearchException uriAndHosts(List uris, List hosts); - - @Message(id = ID_OFFSET + 128, - value = "Invalid target hosts configuration: the 'uris' use different protocols (http, https)." - + " All URIs must use the same protocol. Uris: '%1$s'.") - SearchException differentProtocolsOnUris(List uris); - - @Message(id = ID_OFFSET + 129, - value = "Invalid target hosts configuration: the list of hosts must not be empty.") - SearchException emptyListOfHosts(); - - @Message(id = ID_OFFSET + 130, - value = "Invalid target hosts configuration: the list of URIs must not be empty.") - SearchException emptyListOfUris(); - @Message(id = ID_OFFSET + 148, value = "Invalid backend configuration: mapping requires multi-tenancy" + " but no multi-tenancy strategy is set.") @@ -121,4 +89,14 @@ public interface ConfigurationLog { @Message(id = ID_OFFSET + 192, value = "Elasticsearch backend will use client factory '%s'. Context: %s") void backendClientFactory(BeanHolder clientFactoryHolder, String eventContext); + + @Message(id = ID_OFFSET + 194, + value = "Elasticsearch backend have found no client factories. Please make one of the client factory implementations available. Context: %s") + SearchException backendClientFactoryNotConfigured(String eventContext); + + @Message(id = ID_OFFSET + 195, + value = "Elasticsearch backend have found multiple client factories: %s. Please make just one of the client factory implementations available. Context: %s") + SearchException backendClientFactoryMultipleConfigured(List> factories, + String eventContext); + } diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/logging/impl/ElasticsearchLog.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/logging/impl/ElasticsearchLog.java index dd69d5000f7..4c7facd9ffb 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/logging/impl/ElasticsearchLog.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/logging/impl/ElasticsearchLog.java @@ -7,6 +7,7 @@ import static org.jboss.logging.Logger.Level.TRACE; +import org.hibernate.search.backend.elasticsearch.client.common.logging.spi.ElasticsearchRequestLog; import org.hibernate.search.util.common.logging.impl.MessageConstants; import org.jboss.logging.annotations.LogMessage; @@ -20,7 +21,7 @@ @ValidIdRange(min = MessageConstants.BACKEND_ES_ID_RANGE_MIN, max = MessageConstants.BACKEND_ES_ID_RANGE_MAX), }) public interface ElasticsearchLog - extends AnalysisLog, ConfigurationLog, DeprecationLog, ElasticsearchClientLog, ElasticsearchMiscLog, + extends AnalysisLog, ConfigurationLog, DeprecationLog, ElasticsearchMiscLog, ElasticsearchRequestLog, IndexingLog, MappingLog, QueryLog, VersionLog { // ----------------------------------- @@ -40,6 +41,6 @@ public interface ElasticsearchLog * here to the next value. */ @LogMessage(level = TRACE) - @Message(id = ID_OFFSET + 194, value = "") + @Message(id = ID_OFFSET + 196, value = "") void nextLoggerIdForConvenience(); } diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/logging/impl/ElasticsearchMiscLog.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/logging/impl/ElasticsearchMiscLog.java index 6bd4a1273a0..14901ed0245 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/logging/impl/ElasticsearchMiscLog.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/logging/impl/ElasticsearchMiscLog.java @@ -52,12 +52,6 @@ public interface ElasticsearchMiscLog { + " This extension can only be applied to components created by an Elasticsearch backend.") SearchException elasticsearchExtensionOnUnknownType(Object context); - @Message(id = ID_OFFSET + 18, - value = "Invalid requested type for client: '%1$s'." - + " The Elasticsearch low-level client can only be unwrapped to '%2$s'.") - SearchException clientUnwrappingWithUnknownType(@FormatWith(ClassFormatter.class) Class requestedClass, - @FormatWith(ClassFormatter.class) Class actualClass); - @Message(id = ID_OFFSET + 19, value = "Invalid requested type for this backend: '%1$s'." + " Elasticsearch backends can only be unwrapped to '%2$s'.") diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/logging/impl/MappingLog.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/logging/impl/MappingLog.java index 442c51c1310..61ab6f7bace 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/logging/impl/MappingLog.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/logging/impl/MappingLog.java @@ -9,7 +9,7 @@ import java.lang.invoke.MethodHandles; import java.util.Set; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; +import org.hibernate.search.backend.elasticsearch.client.common.util.spi.URLEncodedString; import org.hibernate.search.engine.backend.types.Highlightable; import org.hibernate.search.engine.backend.types.TermVector; import org.hibernate.search.engine.backend.types.VectorSimilarity; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/logging/impl/QueryLog.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/logging/impl/QueryLog.java index ac3d9a6cd50..3c108f22894 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/logging/impl/QueryLog.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/logging/impl/QueryLog.java @@ -12,8 +12,8 @@ import java.util.Map; import java.util.Set; +import org.hibernate.search.backend.elasticsearch.client.common.util.spi.URLEncodedString; import org.hibernate.search.backend.elasticsearch.index.ElasticsearchIndexManager; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; import org.hibernate.search.engine.backend.scope.spi.IndexScopeBuilder; import org.hibernate.search.engine.logging.spi.AggregationKeyFormatter; import org.hibernate.search.engine.search.aggregation.AggregationKey; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/mapping/impl/IndexNameTypeNameMapping.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/mapping/impl/IndexNameTypeNameMapping.java index c2262de452e..876268605b4 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/mapping/impl/IndexNameTypeNameMapping.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/mapping/impl/IndexNameTypeNameMapping.java @@ -8,12 +8,12 @@ import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import org.hibernate.search.backend.elasticsearch.client.common.logging.spi.ElasticsearchClientLog; import org.hibernate.search.backend.elasticsearch.document.impl.DocumentMetadataContributor; import org.hibernate.search.backend.elasticsearch.document.model.dsl.impl.IndexSchemaRootContributor; import org.hibernate.search.backend.elasticsearch.gson.impl.JsonAccessor; import org.hibernate.search.backend.elasticsearch.index.layout.IndexLayoutStrategy; import org.hibernate.search.backend.elasticsearch.index.layout.impl.IndexNames; -import org.hibernate.search.backend.elasticsearch.logging.impl.ElasticsearchClientLog; import org.hibernate.search.backend.elasticsearch.search.projection.impl.ProjectionExtractContext; import org.hibernate.search.backend.elasticsearch.search.projection.impl.ProjectionExtractionHelper; import org.hibernate.search.backend.elasticsearch.search.projection.impl.ProjectionRequestContext; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/multitenancy/impl/DiscriminatorMultiTenancyStrategy.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/multitenancy/impl/DiscriminatorMultiTenancyStrategy.java index aaa3f5cde06..26b1a7a3c81 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/multitenancy/impl/DiscriminatorMultiTenancyStrategy.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/multitenancy/impl/DiscriminatorMultiTenancyStrategy.java @@ -8,12 +8,12 @@ import java.util.Set; import java.util.regex.Pattern; +import org.hibernate.search.backend.elasticsearch.client.common.logging.spi.ElasticsearchClientLog; import org.hibernate.search.backend.elasticsearch.common.impl.DocumentIdHelper; import org.hibernate.search.backend.elasticsearch.document.impl.DocumentMetadataContributor; import org.hibernate.search.backend.elasticsearch.document.model.dsl.impl.IndexSchemaRootContributor; import org.hibernate.search.backend.elasticsearch.gson.impl.JsonAccessor; import org.hibernate.search.backend.elasticsearch.logging.impl.ConfigurationLog; -import org.hibernate.search.backend.elasticsearch.logging.impl.ElasticsearchClientLog; import org.hibernate.search.backend.elasticsearch.lowlevel.index.mapping.impl.DataTypes; import org.hibernate.search.backend.elasticsearch.lowlevel.index.mapping.impl.MetadataFields; import org.hibernate.search.backend.elasticsearch.lowlevel.index.mapping.impl.PropertyMapping; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/multitenancy/impl/NoMultiTenancyStrategy.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/multitenancy/impl/NoMultiTenancyStrategy.java index 59496d42611..2eea2fd48b3 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/multitenancy/impl/NoMultiTenancyStrategy.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/multitenancy/impl/NoMultiTenancyStrategy.java @@ -8,12 +8,12 @@ import java.util.Optional; import java.util.Set; +import org.hibernate.search.backend.elasticsearch.client.common.logging.spi.ElasticsearchClientLog; import org.hibernate.search.backend.elasticsearch.common.impl.DocumentIdHelper; import org.hibernate.search.backend.elasticsearch.document.impl.DocumentMetadataContributor; import org.hibernate.search.backend.elasticsearch.document.model.dsl.impl.IndexSchemaRootContributor; import org.hibernate.search.backend.elasticsearch.gson.impl.JsonAccessor; import org.hibernate.search.backend.elasticsearch.logging.impl.ConfigurationLog; -import org.hibernate.search.backend.elasticsearch.logging.impl.ElasticsearchClientLog; import org.hibernate.search.backend.elasticsearch.search.projection.impl.ProjectionExtractContext; import org.hibernate.search.backend.elasticsearch.search.projection.impl.ProjectionExtractionHelper; import org.hibernate.search.backend.elasticsearch.search.projection.impl.ProjectionRequestContext; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/orchestration/impl/ElasticsearchDefaultWorkSequenceBuilder.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/orchestration/impl/ElasticsearchDefaultWorkSequenceBuilder.java index 364d1bb3758..52d1a94175c 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/orchestration/impl/ElasticsearchDefaultWorkSequenceBuilder.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/orchestration/impl/ElasticsearchDefaultWorkSequenceBuilder.java @@ -7,7 +7,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; -import org.hibernate.search.backend.elasticsearch.logging.impl.ElasticsearchClientLog; +import org.hibernate.search.backend.elasticsearch.client.common.logging.spi.ElasticsearchClientLog; import org.hibernate.search.backend.elasticsearch.work.impl.BulkableWork; import org.hibernate.search.backend.elasticsearch.work.impl.ElasticsearchWork; import org.hibernate.search.backend.elasticsearch.work.impl.ElasticsearchWorkExecutionContext; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/orchestration/impl/ElasticsearchWorkExecutionContextImpl.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/orchestration/impl/ElasticsearchWorkExecutionContextImpl.java index bd3bf1cc309..897df2b453f 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/orchestration/impl/ElasticsearchWorkExecutionContextImpl.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/orchestration/impl/ElasticsearchWorkExecutionContextImpl.java @@ -4,8 +4,8 @@ */ package org.hibernate.search.backend.elasticsearch.orchestration.impl; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchClient; -import org.hibernate.search.backend.elasticsearch.gson.spi.GsonProvider; +import org.hibernate.search.backend.elasticsearch.client.common.gson.spi.GsonProvider; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchClient; import org.hibernate.search.backend.elasticsearch.work.impl.ElasticsearchWorkExecutionContext; /** diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/schema/management/impl/ElasticsearchIndexSchemaExportImpl.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/schema/management/impl/ElasticsearchIndexSchemaExportImpl.java index 35f5fe51efc..914178e7d7f 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/schema/management/impl/ElasticsearchIndexSchemaExportImpl.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/schema/management/impl/ElasticsearchIndexSchemaExportImpl.java @@ -14,7 +14,7 @@ import java.util.List; import java.util.Map; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchRequest; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchRequest; import org.hibernate.search.backend.elasticsearch.logging.impl.ElasticsearchMiscLog; import org.hibernate.search.backend.elasticsearch.schema.management.ElasticsearchIndexSchemaExport; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/schema/management/impl/ElasticsearchIndexSchemaManager.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/schema/management/impl/ElasticsearchIndexSchemaManager.java index 38d66e40d9e..fe696604873 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/schema/management/impl/ElasticsearchIndexSchemaManager.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/schema/management/impl/ElasticsearchIndexSchemaManager.java @@ -7,11 +7,11 @@ import java.util.Optional; import java.util.concurrent.CompletableFuture; +import org.hibernate.search.backend.elasticsearch.client.common.util.spi.URLEncodedString; import org.hibernate.search.backend.elasticsearch.index.layout.IndexLayoutStrategy; import org.hibernate.search.backend.elasticsearch.index.layout.impl.IndexNames; import org.hibernate.search.backend.elasticsearch.lowlevel.index.impl.IndexMetadata; import org.hibernate.search.backend.elasticsearch.orchestration.impl.ElasticsearchParallelWorkOrchestrator; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; import org.hibernate.search.backend.elasticsearch.validation.impl.ElasticsearchPropertyMappingValidatorProvider; import org.hibernate.search.backend.elasticsearch.work.factory.impl.ElasticsearchWorkFactory; import org.hibernate.search.engine.backend.schema.management.spi.IndexSchemaCollector; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/schema/management/impl/ElasticsearchSchemaAccessor.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/schema/management/impl/ElasticsearchSchemaAccessor.java index 8d644829196..e28f7f587e1 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/schema/management/impl/ElasticsearchSchemaAccessor.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/schema/management/impl/ElasticsearchSchemaAccessor.java @@ -9,14 +9,14 @@ import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; +import org.hibernate.search.backend.elasticsearch.client.common.logging.spi.ElasticsearchClientLog; +import org.hibernate.search.backend.elasticsearch.client.common.util.spi.URLEncodedString; import org.hibernate.search.backend.elasticsearch.index.IndexStatus; import org.hibernate.search.backend.elasticsearch.index.layout.impl.IndexNames; -import org.hibernate.search.backend.elasticsearch.logging.impl.ElasticsearchClientLog; import org.hibernate.search.backend.elasticsearch.lowlevel.index.aliases.impl.IndexAliasDefinition; import org.hibernate.search.backend.elasticsearch.lowlevel.index.mapping.impl.RootTypeMapping; import org.hibernate.search.backend.elasticsearch.lowlevel.index.settings.impl.IndexSettings; import org.hibernate.search.backend.elasticsearch.orchestration.impl.ElasticsearchParallelWorkOrchestrator; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; import org.hibernate.search.backend.elasticsearch.work.factory.impl.ElasticsearchWorkFactory; import org.hibernate.search.backend.elasticsearch.work.impl.NonBulkableWork; import org.hibernate.search.backend.elasticsearch.work.result.impl.CreateIndexResult; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/schema/management/impl/ElasticsearchSchemaCreator.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/schema/management/impl/ElasticsearchSchemaCreator.java index d33acdbd832..ad2a13d59ac 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/schema/management/impl/ElasticsearchSchemaCreator.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/schema/management/impl/ElasticsearchSchemaCreator.java @@ -6,10 +6,10 @@ import java.util.concurrent.CompletableFuture; +import org.hibernate.search.backend.elasticsearch.client.common.util.spi.URLEncodedString; import org.hibernate.search.backend.elasticsearch.index.layout.IndexLayoutStrategy; import org.hibernate.search.backend.elasticsearch.index.layout.impl.IndexNames; import org.hibernate.search.backend.elasticsearch.lowlevel.index.impl.IndexMetadata; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; import org.hibernate.search.backend.elasticsearch.work.result.impl.ExistingIndexMetadata; import org.hibernate.search.engine.backend.work.execution.OperationSubmitter; import org.hibernate.search.util.common.SearchException; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/schema/management/impl/ElasticsearchSchemaDropper.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/schema/management/impl/ElasticsearchSchemaDropper.java index 973d01213f0..3167fea04c5 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/schema/management/impl/ElasticsearchSchemaDropper.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/schema/management/impl/ElasticsearchSchemaDropper.java @@ -6,8 +6,8 @@ import java.util.concurrent.CompletableFuture; +import org.hibernate.search.backend.elasticsearch.client.common.util.spi.URLEncodedString; import org.hibernate.search.backend.elasticsearch.index.layout.impl.IndexNames; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; import org.hibernate.search.engine.backend.work.execution.OperationSubmitter; import org.hibernate.search.util.common.SearchException; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/schema/management/impl/ElasticsearchSchemaExporter.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/schema/management/impl/ElasticsearchSchemaExporter.java index bf824a4b7f0..0782656af0c 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/schema/management/impl/ElasticsearchSchemaExporter.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/schema/management/impl/ElasticsearchSchemaExporter.java @@ -4,12 +4,12 @@ */ package org.hibernate.search.backend.elasticsearch.schema.management.impl; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchRequest; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchRequest; +import org.hibernate.search.backend.elasticsearch.client.common.util.spi.URLEncodedString; import org.hibernate.search.backend.elasticsearch.index.layout.IndexLayoutStrategy; import org.hibernate.search.backend.elasticsearch.index.layout.impl.IndexNames; import org.hibernate.search.backend.elasticsearch.lowlevel.index.impl.IndexMetadata; import org.hibernate.search.backend.elasticsearch.schema.management.ElasticsearchIndexSchemaExport; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; import org.hibernate.search.backend.elasticsearch.work.factory.impl.ElasticsearchWorkFactory; import com.google.gson.Gson; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/schema/management/impl/ElasticsearchSchemaMigrator.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/schema/management/impl/ElasticsearchSchemaMigrator.java index c7264ca71b9..b3b71b4ef80 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/schema/management/impl/ElasticsearchSchemaMigrator.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/schema/management/impl/ElasticsearchSchemaMigrator.java @@ -7,12 +7,12 @@ import java.util.Map; import java.util.concurrent.CompletableFuture; -import org.hibernate.search.backend.elasticsearch.logging.impl.ElasticsearchClientLog; +import org.hibernate.search.backend.elasticsearch.client.common.logging.spi.ElasticsearchClientLog; +import org.hibernate.search.backend.elasticsearch.client.common.util.spi.URLEncodedString; import org.hibernate.search.backend.elasticsearch.lowlevel.index.aliases.impl.IndexAliasDefinition; import org.hibernate.search.backend.elasticsearch.lowlevel.index.impl.IndexMetadata; import org.hibernate.search.backend.elasticsearch.lowlevel.index.mapping.impl.RootTypeMapping; import org.hibernate.search.backend.elasticsearch.lowlevel.index.settings.impl.IndexSettings; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; import org.hibernate.search.engine.backend.work.execution.OperationSubmitter; import org.hibernate.search.util.common.SearchException; import org.hibernate.search.util.common.impl.Futures; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/aggregation/impl/AbstractElasticsearchNestableAggregation.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/aggregation/impl/AbstractElasticsearchNestableAggregation.java index 4ca7c409591..29f8c4c35bf 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/aggregation/impl/AbstractElasticsearchNestableAggregation.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/aggregation/impl/AbstractElasticsearchNestableAggregation.java @@ -6,9 +6,9 @@ import java.util.List; +import org.hibernate.search.backend.elasticsearch.client.common.logging.spi.ElasticsearchClientLog; import org.hibernate.search.backend.elasticsearch.gson.impl.JsonAccessor; import org.hibernate.search.backend.elasticsearch.gson.impl.JsonObjectAccessor; -import org.hibernate.search.backend.elasticsearch.logging.impl.ElasticsearchClientLog; import org.hibernate.search.backend.elasticsearch.logging.impl.QueryLog; import org.hibernate.search.backend.elasticsearch.search.common.impl.ElasticsearchSearchIndexScope; import org.hibernate.search.backend.elasticsearch.search.common.impl.ElasticsearchSearchIndexValueFieldContext; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/aggregation/impl/ElasticsearchCompositeAggregation.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/aggregation/impl/ElasticsearchCompositeAggregation.java index 8071567a9db..ab89443ce49 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/aggregation/impl/ElasticsearchCompositeAggregation.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/aggregation/impl/ElasticsearchCompositeAggregation.java @@ -5,8 +5,8 @@ package org.hibernate.search.backend.elasticsearch.search.aggregation.impl; +import org.hibernate.search.backend.elasticsearch.client.common.logging.spi.ElasticsearchClientLog; import org.hibernate.search.backend.elasticsearch.gson.impl.JsonAccessor; -import org.hibernate.search.backend.elasticsearch.logging.impl.ElasticsearchClientLog; import org.hibernate.search.backend.elasticsearch.search.common.impl.ElasticsearchSearchIndexScope; import org.hibernate.search.engine.search.aggregation.AggregationKey; import org.hibernate.search.engine.search.aggregation.SearchAggregation; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/aggregation/impl/ElasticsearchCountDocumentAggregation.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/aggregation/impl/ElasticsearchCountDocumentAggregation.java index 08f62de442e..22fd03a628d 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/aggregation/impl/ElasticsearchCountDocumentAggregation.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/aggregation/impl/ElasticsearchCountDocumentAggregation.java @@ -4,8 +4,8 @@ */ package org.hibernate.search.backend.elasticsearch.search.aggregation.impl; +import org.hibernate.search.backend.elasticsearch.client.common.logging.spi.ElasticsearchClientLog; import org.hibernate.search.backend.elasticsearch.gson.impl.JsonAccessor; -import org.hibernate.search.backend.elasticsearch.logging.impl.ElasticsearchClientLog; import org.hibernate.search.backend.elasticsearch.logging.impl.QueryLog; import org.hibernate.search.backend.elasticsearch.search.common.impl.ElasticsearchSearchIndexNodeContext; import org.hibernate.search.backend.elasticsearch.search.common.impl.ElasticsearchSearchIndexScope; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/impl/ElasticsearchSearchQueryImpl.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/impl/ElasticsearchSearchQueryImpl.java index 008817c918f..03d42c9f3d9 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/impl/ElasticsearchSearchQueryImpl.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/impl/ElasticsearchSearchQueryImpl.java @@ -10,6 +10,7 @@ import java.util.Set; import java.util.concurrent.TimeUnit; +import org.hibernate.search.backend.elasticsearch.client.common.util.spi.URLEncodedString; import org.hibernate.search.backend.elasticsearch.gson.impl.JsonAccessor; import org.hibernate.search.backend.elasticsearch.logging.impl.QueryLog; import org.hibernate.search.backend.elasticsearch.orchestration.impl.ElasticsearchParallelWorkOrchestrator; @@ -19,7 +20,6 @@ import org.hibernate.search.backend.elasticsearch.search.query.ElasticsearchSearchRequestTransformer; import org.hibernate.search.backend.elasticsearch.search.query.ElasticsearchSearchResult; import org.hibernate.search.backend.elasticsearch.search.query.ElasticsearchSearchScroll; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; import org.hibernate.search.backend.elasticsearch.work.factory.impl.ElasticsearchWorkFactory; import org.hibernate.search.backend.elasticsearch.work.impl.CountWork; import org.hibernate.search.backend.elasticsearch.work.impl.ElasticsearchSearchResultExtractor; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/impl/ElasticsearchSearchRequestTransformerContextImpl.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/impl/ElasticsearchSearchRequestTransformerContextImpl.java index 50bb1787f59..f0cc0115eaa 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/impl/ElasticsearchSearchRequestTransformerContextImpl.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/impl/ElasticsearchSearchRequestTransformerContextImpl.java @@ -9,7 +9,7 @@ import java.util.Map; import java.util.function.Function; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchRequest; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchRequest; import org.hibernate.search.backend.elasticsearch.search.query.ElasticsearchSearchRequestTransformer; import org.hibernate.search.backend.elasticsearch.search.query.ElasticsearchSearchRequestTransformerContext; import org.hibernate.search.util.common.AssertionFailure; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/types/codec/impl/ElasticsearchGeoPointFieldCodec.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/types/codec/impl/ElasticsearchGeoPointFieldCodec.java index a5d7977a7b6..201d62ea0f3 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/types/codec/impl/ElasticsearchGeoPointFieldCodec.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/types/codec/impl/ElasticsearchGeoPointFieldCodec.java @@ -4,9 +4,9 @@ */ package org.hibernate.search.backend.elasticsearch.types.codec.impl; +import org.hibernate.search.backend.elasticsearch.client.common.logging.spi.ElasticsearchClientLog; import org.hibernate.search.backend.elasticsearch.gson.impl.JsonAccessor; import org.hibernate.search.backend.elasticsearch.gson.impl.JsonElementTypes; -import org.hibernate.search.backend.elasticsearch.logging.impl.ElasticsearchClientLog; import org.hibernate.search.engine.spatial.GeoPoint; import com.google.gson.Gson; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/execution/impl/ElasticsearchIndexWorkspace.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/execution/impl/ElasticsearchIndexWorkspace.java index 994396791e5..79440e5b558 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/execution/impl/ElasticsearchIndexWorkspace.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/execution/impl/ElasticsearchIndexWorkspace.java @@ -7,10 +7,10 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; +import org.hibernate.search.backend.elasticsearch.client.common.util.spi.URLEncodedString; import org.hibernate.search.backend.elasticsearch.lowlevel.query.impl.Queries; import org.hibernate.search.backend.elasticsearch.multitenancy.impl.MultiTenancyStrategy; import org.hibernate.search.backend.elasticsearch.orchestration.impl.ElasticsearchParallelWorkOrchestrator; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; import org.hibernate.search.backend.elasticsearch.work.factory.impl.ElasticsearchWorkFactory; import org.hibernate.search.engine.backend.work.execution.OperationSubmitter; import org.hibernate.search.engine.backend.work.execution.spi.IndexWorkspace; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/execution/impl/WorkExecutionIndexManagerContext.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/execution/impl/WorkExecutionIndexManagerContext.java index a98a31eac72..ee6b5e8a69a 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/execution/impl/WorkExecutionIndexManagerContext.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/execution/impl/WorkExecutionIndexManagerContext.java @@ -4,7 +4,7 @@ */ package org.hibernate.search.backend.elasticsearch.work.execution.impl; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; +import org.hibernate.search.backend.elasticsearch.client.common.util.spi.URLEncodedString; import org.hibernate.search.engine.backend.work.execution.spi.DocumentContributor; import com.google.gson.JsonObject; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/factory/impl/AmazonOpenSearchServerlessWorkFactory.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/factory/impl/AmazonOpenSearchServerlessWorkFactory.java index 47f41c6f5bb..55455b60f53 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/factory/impl/AmazonOpenSearchServerlessWorkFactory.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/factory/impl/AmazonOpenSearchServerlessWorkFactory.java @@ -4,10 +4,10 @@ */ package org.hibernate.search.backend.elasticsearch.work.factory.impl; -import org.hibernate.search.backend.elasticsearch.gson.spi.GsonProvider; +import org.hibernate.search.backend.elasticsearch.client.common.gson.spi.GsonProvider; +import org.hibernate.search.backend.elasticsearch.client.common.util.spi.URLEncodedString; import org.hibernate.search.backend.elasticsearch.index.IndexStatus; import org.hibernate.search.backend.elasticsearch.logging.impl.ElasticsearchMiscLog; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; import org.hibernate.search.backend.elasticsearch.work.impl.CloseIndexWork; import org.hibernate.search.backend.elasticsearch.work.impl.DeleteByQueryWork; import org.hibernate.search.backend.elasticsearch.work.impl.FlushWork; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/factory/impl/Elasticsearch7WorkFactory.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/factory/impl/Elasticsearch7WorkFactory.java index 76c44564db0..ba89c1fcbbc 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/factory/impl/Elasticsearch7WorkFactory.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/factory/impl/Elasticsearch7WorkFactory.java @@ -7,12 +7,12 @@ import java.util.List; import java.util.Map; -import org.hibernate.search.backend.elasticsearch.gson.spi.GsonProvider; +import org.hibernate.search.backend.elasticsearch.client.common.gson.spi.GsonProvider; +import org.hibernate.search.backend.elasticsearch.client.common.util.spi.URLEncodedString; import org.hibernate.search.backend.elasticsearch.index.IndexStatus; import org.hibernate.search.backend.elasticsearch.lowlevel.index.aliases.impl.IndexAliasDefinition; import org.hibernate.search.backend.elasticsearch.lowlevel.index.mapping.impl.RootTypeMapping; import org.hibernate.search.backend.elasticsearch.lowlevel.index.settings.impl.IndexSettings; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; import org.hibernate.search.backend.elasticsearch.work.impl.AnalyzeWork; import org.hibernate.search.backend.elasticsearch.work.impl.BulkWork; import org.hibernate.search.backend.elasticsearch.work.impl.BulkableWork; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/factory/impl/ElasticsearchWorkFactory.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/factory/impl/ElasticsearchWorkFactory.java index 189d4912401..8b42214b8e5 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/factory/impl/ElasticsearchWorkFactory.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/factory/impl/ElasticsearchWorkFactory.java @@ -7,11 +7,11 @@ import java.util.List; import java.util.Map; +import org.hibernate.search.backend.elasticsearch.client.common.util.spi.URLEncodedString; import org.hibernate.search.backend.elasticsearch.index.IndexStatus; import org.hibernate.search.backend.elasticsearch.lowlevel.index.aliases.impl.IndexAliasDefinition; import org.hibernate.search.backend.elasticsearch.lowlevel.index.mapping.impl.RootTypeMapping; import org.hibernate.search.backend.elasticsearch.lowlevel.index.settings.impl.IndexSettings; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; import org.hibernate.search.backend.elasticsearch.work.impl.AnalyzeWork; import org.hibernate.search.backend.elasticsearch.work.impl.BulkWork; import org.hibernate.search.backend.elasticsearch.work.impl.BulkableWork; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/AbstractNonBulkableWork.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/AbstractNonBulkableWork.java index 01291da6ac5..7bdc3b2d18e 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/AbstractNonBulkableWork.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/AbstractNonBulkableWork.java @@ -7,9 +7,9 @@ import java.util.concurrent.CompletableFuture; import java.util.function.Function; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchRequest; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchResponse; -import org.hibernate.search.backend.elasticsearch.logging.impl.ElasticsearchClientLog; +import org.hibernate.search.backend.elasticsearch.client.common.logging.spi.ElasticsearchClientLog; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchRequest; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchResponse; import org.hibernate.search.util.common.SearchException; import org.hibernate.search.util.common.impl.Futures; import org.hibernate.search.util.common.impl.Throwables; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/AbstractSingleDocumentIndexingWork.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/AbstractSingleDocumentIndexingWork.java index 7b89ae82085..9cea71ac0c7 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/AbstractSingleDocumentIndexingWork.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/AbstractSingleDocumentIndexingWork.java @@ -4,7 +4,7 @@ */ package org.hibernate.search.backend.elasticsearch.work.impl; -import org.hibernate.search.backend.elasticsearch.logging.impl.ElasticsearchClientLog; +import org.hibernate.search.backend.elasticsearch.client.common.logging.spi.ElasticsearchClientLog; import org.hibernate.search.engine.backend.work.execution.DocumentRefreshStrategy; import com.google.gson.JsonObject; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/AnalyzeWork.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/AnalyzeWork.java index 42d8fc43694..2b20be5510f 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/AnalyzeWork.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/AnalyzeWork.java @@ -7,12 +7,12 @@ import java.util.ArrayList; import java.util.List; +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.URLEncodedString; import org.hibernate.search.backend.elasticsearch.client.impl.Paths; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchRequest; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchResponse; import org.hibernate.search.backend.elasticsearch.gson.impl.JsonAccessor; import org.hibernate.search.backend.elasticsearch.gson.impl.JsonArrayAccessor; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; import org.hibernate.search.engine.backend.analysis.AnalysisToken; import org.hibernate.search.util.common.AssertionFailure; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/BulkWork.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/BulkWork.java index fbfae005969..3378207250a 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/BulkWork.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/BulkWork.java @@ -6,9 +6,9 @@ import java.util.List; +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.impl.Paths; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchRequest; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchResponse; import org.hibernate.search.backend.elasticsearch.gson.impl.JsonAccessor; import org.hibernate.search.backend.elasticsearch.work.result.impl.BulkResult; import org.hibernate.search.engine.backend.work.execution.DocumentRefreshStrategy; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/ClearScrollWork.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/ClearScrollWork.java index e3cfb0029ef..26fd87935aa 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/ClearScrollWork.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/ClearScrollWork.java @@ -4,9 +4,9 @@ */ package org.hibernate.search.backend.elasticsearch.work.impl; +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.impl.Paths; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchRequest; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchResponse; import com.google.gson.JsonObject; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/CloseIndexWork.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/CloseIndexWork.java index 292414a5b6f..dbbb787c995 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/CloseIndexWork.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/CloseIndexWork.java @@ -4,10 +4,10 @@ */ package org.hibernate.search.backend.elasticsearch.work.impl; +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.URLEncodedString; import org.hibernate.search.backend.elasticsearch.client.impl.Paths; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchRequest; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchResponse; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; public class CloseIndexWork extends AbstractNonBulkableWork { diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/CountWork.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/CountWork.java index 88854631bf5..a6f5082c47f 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/CountWork.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/CountWork.java @@ -8,11 +8,11 @@ import java.util.List; import java.util.Set; +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.URLEncodedString; import org.hibernate.search.backend.elasticsearch.client.impl.Paths; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchRequest; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchResponse; import org.hibernate.search.backend.elasticsearch.gson.impl.JsonAccessor; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; import org.hibernate.search.engine.common.timing.Deadline; import com.google.gson.JsonObject; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/CreateIndexWork.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/CreateIndexWork.java index e9c5e81bb33..aed4b3dd8a4 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/CreateIndexWork.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/CreateIndexWork.java @@ -6,14 +6,14 @@ import java.util.Map; +import org.hibernate.search.backend.elasticsearch.client.common.gson.spi.GsonProvider; +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.URLEncodedString; import org.hibernate.search.backend.elasticsearch.client.impl.ElasticsearchClientUtils; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchRequest; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchResponse; -import org.hibernate.search.backend.elasticsearch.gson.spi.GsonProvider; import org.hibernate.search.backend.elasticsearch.lowlevel.index.aliases.impl.IndexAliasDefinition; import org.hibernate.search.backend.elasticsearch.lowlevel.index.mapping.impl.RootTypeMapping; import org.hibernate.search.backend.elasticsearch.lowlevel.index.settings.impl.IndexSettings; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; import org.hibernate.search.backend.elasticsearch.work.result.impl.CreateIndexResult; import com.google.gson.Gson; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/DeleteByQueryWork.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/DeleteByQueryWork.java index 6897d88da16..9f1881fdf4e 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/DeleteByQueryWork.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/DeleteByQueryWork.java @@ -9,10 +9,10 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; +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.URLEncodedString; import org.hibernate.search.backend.elasticsearch.client.impl.Paths; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchRequest; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchResponse; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; import org.hibernate.search.backend.elasticsearch.work.factory.impl.ElasticsearchWorkFactory; import com.google.gson.JsonObject; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/DeleteWork.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/DeleteWork.java index f748beb35d1..e3e4014e67f 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/DeleteWork.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/DeleteWork.java @@ -4,7 +4,7 @@ */ package org.hibernate.search.backend.elasticsearch.work.impl; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; +import org.hibernate.search.backend.elasticsearch.client.common.util.spi.URLEncodedString; import com.google.gson.JsonObject; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/DropIndexWork.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/DropIndexWork.java index 833fc2374a3..88cf867e17e 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/DropIndexWork.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/DropIndexWork.java @@ -4,9 +4,9 @@ */ package org.hibernate.search.backend.elasticsearch.work.impl; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchRequest; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchResponse; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; +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.URLEncodedString; public class DropIndexWork extends AbstractNonBulkableWork { diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/ElasticsearchRequestSuccessAssessor.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/ElasticsearchRequestSuccessAssessor.java index 3e7e5a3751c..42eb78f7327 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/ElasticsearchRequestSuccessAssessor.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/ElasticsearchRequestSuccessAssessor.java @@ -11,10 +11,10 @@ import java.util.Optional; import java.util.Set; +import org.hibernate.search.backend.elasticsearch.client.common.logging.spi.ElasticsearchClientLog; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchResponse; import org.hibernate.search.backend.elasticsearch.client.impl.ElasticsearchClientUtils; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchResponse; import org.hibernate.search.backend.elasticsearch.gson.impl.JsonAccessor; -import org.hibernate.search.backend.elasticsearch.logging.impl.ElasticsearchClientLog; import org.hibernate.search.util.common.SearchException; import com.google.gson.JsonObject; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/ElasticsearchWorkExecutionContext.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/ElasticsearchWorkExecutionContext.java index 6fc184bc80d..59223f04d46 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/ElasticsearchWorkExecutionContext.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/ElasticsearchWorkExecutionContext.java @@ -4,8 +4,8 @@ */ package org.hibernate.search.backend.elasticsearch.work.impl; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchClient; -import org.hibernate.search.backend.elasticsearch.gson.spi.GsonProvider; +import org.hibernate.search.backend.elasticsearch.client.common.gson.spi.GsonProvider; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchClient; public interface ElasticsearchWorkExecutionContext { diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/ExplainWork.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/ExplainWork.java index 9416ea96bdf..6dde52a40d8 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/ExplainWork.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/ExplainWork.java @@ -6,11 +6,11 @@ import java.util.Set; +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.URLEncodedString; import org.hibernate.search.backend.elasticsearch.client.impl.Paths; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchRequest; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchResponse; import org.hibernate.search.backend.elasticsearch.logging.impl.QueryLog; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; import org.hibernate.search.backend.elasticsearch.work.result.impl.ExplainResult; import com.google.gson.JsonObject; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/FlushWork.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/FlushWork.java index 8ded465fa81..e77f2b90a53 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/FlushWork.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/FlushWork.java @@ -7,10 +7,10 @@ import java.util.HashSet; import java.util.Set; +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.URLEncodedString; import org.hibernate.search.backend.elasticsearch.client.impl.Paths; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchRequest; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchResponse; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; /** * A flush work for ES5, using the Flush API then the Refresh API. diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/ForceMergeWork.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/ForceMergeWork.java index 1eb5661d4bf..8e4e42e3074 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/ForceMergeWork.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/ForceMergeWork.java @@ -7,10 +7,10 @@ import java.util.ArrayList; import java.util.List; +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.URLEncodedString; import org.hibernate.search.backend.elasticsearch.client.impl.Paths; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchRequest; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchResponse; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; /** * A force-merge work for ES5+. diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/GetIndexMetadataWork.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/GetIndexMetadataWork.java index 13cdfd10ca8..68f684002f8 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/GetIndexMetadataWork.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/GetIndexMetadataWork.java @@ -11,14 +11,14 @@ import java.util.Map; import java.util.Set; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchRequest; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchResponse; -import org.hibernate.search.backend.elasticsearch.gson.spi.GsonProvider; +import org.hibernate.search.backend.elasticsearch.client.common.gson.spi.GsonProvider; +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.URLEncodedString; import org.hibernate.search.backend.elasticsearch.lowlevel.index.aliases.impl.IndexAliasDefinition; import org.hibernate.search.backend.elasticsearch.lowlevel.index.impl.IndexMetadata; import org.hibernate.search.backend.elasticsearch.lowlevel.index.mapping.impl.RootTypeMapping; import org.hibernate.search.backend.elasticsearch.lowlevel.index.settings.impl.IndexSettings; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; import org.hibernate.search.backend.elasticsearch.work.result.impl.ExistingIndexMetadata; import org.hibernate.search.util.common.AssertionFailure; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/IndexWork.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/IndexWork.java index 3e160d68b46..31f7e7250a1 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/IndexWork.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/IndexWork.java @@ -4,7 +4,7 @@ */ package org.hibernate.search.backend.elasticsearch.work.impl; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; +import org.hibernate.search.backend.elasticsearch.client.common.util.spi.URLEncodedString; import com.google.gson.JsonObject; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/OpenIndexWork.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/OpenIndexWork.java index 2d19ebf3f6d..e364f19e0e2 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/OpenIndexWork.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/OpenIndexWork.java @@ -4,10 +4,10 @@ */ package org.hibernate.search.backend.elasticsearch.work.impl; +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.URLEncodedString; import org.hibernate.search.backend.elasticsearch.client.impl.Paths; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchRequest; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchResponse; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; public class OpenIndexWork extends AbstractNonBulkableWork { diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/PutIndexAliasesWork.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/PutIndexAliasesWork.java index 109e8df0521..233b77743ae 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/PutIndexAliasesWork.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/PutIndexAliasesWork.java @@ -6,12 +6,12 @@ import java.util.Map; +import org.hibernate.search.backend.elasticsearch.client.common.gson.spi.GsonProvider; +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.URLEncodedString; import org.hibernate.search.backend.elasticsearch.client.impl.Paths; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchRequest; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchResponse; -import org.hibernate.search.backend.elasticsearch.gson.spi.GsonProvider; import org.hibernate.search.backend.elasticsearch.lowlevel.index.aliases.impl.IndexAliasDefinition; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; import com.google.gson.Gson; import com.google.gson.JsonArray; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/PutIndexMappingWork.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/PutIndexMappingWork.java index 50ee9305961..eadbb62ee7e 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/PutIndexMappingWork.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/PutIndexMappingWork.java @@ -4,12 +4,12 @@ */ package org.hibernate.search.backend.elasticsearch.work.impl; +import org.hibernate.search.backend.elasticsearch.client.common.gson.spi.GsonProvider; +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.URLEncodedString; import org.hibernate.search.backend.elasticsearch.client.impl.Paths; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchRequest; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchResponse; -import org.hibernate.search.backend.elasticsearch.gson.spi.GsonProvider; import org.hibernate.search.backend.elasticsearch.lowlevel.index.mapping.impl.RootTypeMapping; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; import com.google.gson.Gson; import com.google.gson.JsonObject; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/PutIndexSettingsWork.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/PutIndexSettingsWork.java index d6a820d320d..b70cdcb7474 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/PutIndexSettingsWork.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/PutIndexSettingsWork.java @@ -4,12 +4,12 @@ */ package org.hibernate.search.backend.elasticsearch.work.impl; +import org.hibernate.search.backend.elasticsearch.client.common.gson.spi.GsonProvider; +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.URLEncodedString; import org.hibernate.search.backend.elasticsearch.client.impl.Paths; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchRequest; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchResponse; -import org.hibernate.search.backend.elasticsearch.gson.spi.GsonProvider; import org.hibernate.search.backend.elasticsearch.lowlevel.index.settings.impl.IndexSettings; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; import com.google.gson.Gson; import com.google.gson.JsonObject; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/RefreshWork.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/RefreshWork.java index 82fe00c8387..ad4f5f6aa83 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/RefreshWork.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/RefreshWork.java @@ -7,10 +7,10 @@ import java.util.ArrayList; import java.util.List; +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.URLEncodedString; import org.hibernate.search.backend.elasticsearch.client.impl.Paths; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchRequest; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchResponse; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; public class RefreshWork extends AbstractNonBulkableWork { diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/ScrollWork.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/ScrollWork.java index 017654dfccb..45b3e217d47 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/ScrollWork.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/ScrollWork.java @@ -4,9 +4,9 @@ */ package org.hibernate.search.backend.elasticsearch.work.impl; +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.impl.Paths; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchRequest; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchResponse; import org.hibernate.search.engine.common.timing.Deadline; import com.google.gson.JsonObject; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/SearchWork.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/SearchWork.java index d3ba94ed786..0e446565f1d 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/SearchWork.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/SearchWork.java @@ -8,11 +8,11 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; +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.URLEncodedString; import org.hibernate.search.backend.elasticsearch.client.impl.Paths; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchRequest; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchResponse; import org.hibernate.search.backend.elasticsearch.logging.impl.QueryLog; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; import org.hibernate.search.engine.common.timing.Deadline; import com.google.gson.JsonObject; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/WaitForIndexStatusWork.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/WaitForIndexStatusWork.java index ebbb05de08a..d5ee44709ed 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/WaitForIndexStatusWork.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/work/impl/WaitForIndexStatusWork.java @@ -4,11 +4,11 @@ */ package org.hibernate.search.backend.elasticsearch.work.impl; +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.URLEncodedString; import org.hibernate.search.backend.elasticsearch.client.impl.Paths; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchRequest; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchResponse; import org.hibernate.search.backend.elasticsearch.index.IndexStatus; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; import org.hibernate.search.engine.common.timing.spi.StaticDeadline; public class WaitForIndexStatusWork extends AbstractNonBulkableWork { diff --git a/backend/elasticsearch/src/test/java/org/hibernate/search/backend/elasticsearch/client/impl/ElasticsearchClientUtilsTryGetElasticsearchVersionTest.java b/backend/elasticsearch/src/test/java/org/hibernate/search/backend/elasticsearch/client/impl/ElasticsearchClientUtilsTryGetElasticsearchVersionTest.java index 8990f4afc86..f05d0e99114 100644 --- a/backend/elasticsearch/src/test/java/org/hibernate/search/backend/elasticsearch/client/impl/ElasticsearchClientUtilsTryGetElasticsearchVersionTest.java +++ b/backend/elasticsearch/src/test/java/org/hibernate/search/backend/elasticsearch/client/impl/ElasticsearchClientUtilsTryGetElasticsearchVersionTest.java @@ -16,8 +16,8 @@ import org.hibernate.search.backend.elasticsearch.ElasticsearchDistributionName; import org.hibernate.search.backend.elasticsearch.ElasticsearchVersion; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchClient; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchResponse; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchClient; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchResponse; import org.hibernate.search.util.common.SearchException; import org.junit.jupiter.params.ParameterizedTest; @@ -26,7 +26,6 @@ import com.google.gson.JsonObject; -import org.apache.http.HttpHost; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; @@ -116,7 +115,7 @@ void testHttpStatus404(String distributionString, String versionString, // which doesn't allow retrieving the version. when( clientMock.submit( any() ) ) .thenReturn( CompletableFuture.completedFuture( new ElasticsearchResponse( - new HttpHost( "mockHost:9200" ), 404, "", null ) ) ); + "mockHost:9200", 404, "", null ) ) ); ElasticsearchVersion version = ElasticsearchClientUtils.tryGetElasticsearchVersion( clientMock ); assertThat( version ).isNull(); } @@ -131,6 +130,6 @@ private void doMock(String theDistributionString, String theVersionString) { responseBody.add( "version", versionObject ); when( clientMock.submit( any() ) ) .thenReturn( CompletableFuture.completedFuture( new ElasticsearchResponse( - new HttpHost( "mockHost:9200" ), 200, "", responseBody ) ) ); + "mockHost:9200", 200, "", responseBody ) ) ); } } diff --git a/backend/elasticsearch/src/test/java/org/hibernate/search/backend/elasticsearch/gson/spi/GsonParsingTest.java b/backend/elasticsearch/src/test/java/org/hibernate/search/backend/elasticsearch/gson/spi/GsonParsingTest.java index 8cae788b601..f59983dadc7 100644 --- a/backend/elasticsearch/src/test/java/org/hibernate/search/backend/elasticsearch/gson/spi/GsonParsingTest.java +++ b/backend/elasticsearch/src/test/java/org/hibernate/search/backend/elasticsearch/gson/spi/GsonParsingTest.java @@ -9,6 +9,7 @@ import java.util.Arrays; import java.util.List; +import org.hibernate.search.backend.elasticsearch.client.common.gson.spi.GsonProvider; import org.hibernate.search.backend.elasticsearch.lowlevel.index.mapping.impl.RootTypeMapping; import org.hibernate.search.util.impl.test.JsonHelper; import org.hibernate.search.util.impl.test.annotation.TestForIssue; diff --git a/backend/elasticsearch/src/test/java/org/hibernate/search/backend/elasticsearch/work/impl/BulkWorkTest.java b/backend/elasticsearch/src/test/java/org/hibernate/search/backend/elasticsearch/work/impl/BulkWorkTest.java index 0f0f6cbc9b0..73947e821cf 100644 --- a/backend/elasticsearch/src/test/java/org/hibernate/search/backend/elasticsearch/work/impl/BulkWorkTest.java +++ b/backend/elasticsearch/src/test/java/org/hibernate/search/backend/elasticsearch/work/impl/BulkWorkTest.java @@ -18,9 +18,9 @@ import java.util.List; import java.util.concurrent.CompletableFuture; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchClient; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchRequest; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchResponse; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchClient; +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.work.result.impl.BulkResult; import org.hibernate.search.util.common.SearchException; import org.hibernate.search.util.impl.test.annotation.TestForIssue; @@ -31,7 +31,6 @@ import com.google.gson.JsonArray; import com.google.gson.JsonObject; -import org.apache.http.HttpHost; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoSettings; @@ -76,7 +75,7 @@ void execute_success() { responseBody.add( "items", items ); items.add( new JsonObject() ); items.add( new JsonObject() ); - ElasticsearchResponse response = new ElasticsearchResponse( new HttpHost( "mockHost:9200" ), + ElasticsearchResponse response = new ElasticsearchResponse( "mockHost:9200", 200, "OK", responseBody ); futureFromClient.complete( response ); verifyNoOtherClientInteractionsAndReset(); @@ -120,7 +119,7 @@ void execute_http500() { JsonObject responseBody = new JsonObject(); responseBody.addProperty( "someProperty", "someValue" ); - ElasticsearchResponse response = new ElasticsearchResponse( new HttpHost( "mockHost:9200" ), + ElasticsearchResponse response = new ElasticsearchResponse( "mockHost:9200", 500, "SomeStatus", responseBody ); futureFromClient.complete( response ); verifyNoOtherClientInteractionsAndReset(); diff --git a/bom/platform-common/pom.xml b/bom/platform-common/pom.xml index 37158268321..6a92ceefa7a 100644 --- a/bom/platform-common/pom.xml +++ b/bom/platform-common/pom.xml @@ -35,6 +35,11 @@ 2.0.1 4.0.2 3.1.0.Final + 5.5 + 5.3.5 + 4.5.14 + 4.4.16 + 4.1.5 @@ -63,6 +68,26 @@ hibernate-search-backend-elasticsearch-aws ${project.version} + + org.hibernate.search + hibernate-search-backend-elasticsearch-client-common + ${project.version} + + + org.hibernate.search + hibernate-search-backend-elasticsearch-client-rest + ${project.version} + + + org.hibernate.search + hibernate-search-backend-elasticsearch-client-java + ${project.version} + + + org.hibernate.search + hibernate-search-backend-elasticsearch-client-opensearch + ${project.version} + org.hibernate.search hibernate-search-mapper-pojo-base @@ -600,6 +625,119 @@ third-party ${version.bom.software.amazon.awssdk} + + + + org.apache.httpcomponents.client5 + httpclient5-cache + ${version.bom.org.apache.httpcomponents.client5} + + + org.apache.httpcomponents.client5 + httpclient5-fluent + ${version.bom.org.apache.httpcomponents.client5} + + + org.apache.httpcomponents.client5 + httpclient5 + ${version.bom.org.apache.httpcomponents.client5} + + + org.apache.httpcomponents.core5 + httpcore5-h2 + ${version.bom.org.apache.httpcomponents.core5} + + + org.apache.httpcomponents.core5 + httpcore5-reactive + ${version.bom.org.apache.httpcomponents.core5} + + + org.apache.httpcomponents.core5 + httpcore5 + ${version.bom.org.apache.httpcomponents.core5} + + + + org.apache.httpcomponents + fluent-hc + ${version.bom.org.apache.httpcomponents.httpclient} + + + org.apache.httpcomponents + httpasyncclient-cache + ${version.bom.org.apache.httpcomponents.httpasyncclient} + + + org.apache.httpcomponents + httpasyncclient-osgi + ${version.bom.org.apache.httpcomponents.httpasyncclient} + + + org.apache.httpcomponents + httpasyncclient + ${version.bom.org.apache.httpcomponents.httpasyncclient} + + + org.apache.httpcomponents + httpclient-cache + ${version.bom.org.apache.httpcomponents.httpclient} + + + org.apache.httpcomponents + httpclient-osgi + ${version.bom.org.apache.httpcomponents.httpclient} + + + org.apache.httpcomponents + httpclient-win + ${version.bom.org.apache.httpcomponents.httpclient} + + + org.apache.httpcomponents + httpclient + ${version.bom.org.apache.httpcomponents.httpclient} + + + org.apache.httpcomponents + httpcomponents-asyncclient + ${version.bom.org.apache.httpcomponents.httpasyncclient} + + + org.apache.httpcomponents + httpcomponents-client + ${version.bom.org.apache.httpcomponents.httpclient} + + + org.apache.httpcomponents + httpcomponents-core + ${version.bom.org.apache.httpcomponents.httpcore} + + + org.apache.httpcomponents + httpcore-ab + ${version.bom.org.apache.httpcomponents.httpcore} + + + org.apache.httpcomponents + httpcore-nio + ${version.bom.org.apache.httpcomponents.httpcore} + + + org.apache.httpcomponents + httpcore-osgi + ${version.bom.org.apache.httpcomponents.httpcore} + + + org.apache.httpcomponents + httpcore + ${version.bom.org.apache.httpcomponents.httpcore} + + + org.apache.httpcomponents + httpmime + ${version.bom.org.apache.httpcomponents.httpclient} + @@ -875,6 +1013,26 @@ avro-codegen-test ${version.bom.org.apache.avro} + + org.apache.httpcomponents.core5 + httpcore5-parent + ${version.bom.org.apache.httpcomponents.core5} + + + org.apache.httpcomponents.core5 + httpcore5-testing + ${version.bom.org.apache.httpcomponents.core5} + + + org.apache.httpcomponents.client5 + httpclient5-parent + ${version.bom.org.apache.httpcomponents.client5} + + + org.apache.httpcomponents.client5 + httpclient5-testing + ${version.bom.org.apache.httpcomponents.client5} + diff --git a/bom/public/pom.xml b/bom/public/pom.xml index ecc2c406d56..b8e3c417ce3 100644 --- a/bom/public/pom.xml +++ b/bom/public/pom.xml @@ -42,6 +42,26 @@ hibernate-search-backend-elasticsearch-aws ${project.version} + + org.hibernate.search + hibernate-search-backend-elasticsearch-client-common + ${project.version} + + + org.hibernate.search + hibernate-search-backend-elasticsearch-client-rest + ${project.version} + + + org.hibernate.search + hibernate-search-backend-elasticsearch-client-java + ${project.version} + + + org.hibernate.search + hibernate-search-backend-elasticsearch-client-opensearch + ${project.version} + org.hibernate.search hibernate-search-backend-lucene diff --git a/build/config/src/main/java/org/hibernate/search/build/report/loggers/LoggerCategoriesProcessor.java b/build/config/src/main/java/org/hibernate/search/build/report/loggers/LoggerCategoriesProcessor.java index c2fec0f0f38..ea89326798d 100644 --- a/build/config/src/main/java/org/hibernate/search/build/report/loggers/LoggerCategoriesProcessor.java +++ b/build/config/src/main/java/org/hibernate/search/build/report/loggers/LoggerCategoriesProcessor.java @@ -78,7 +78,7 @@ public boolean process(Set annotations, RoundEnvironment if ( description != null && !description.isBlank() ) { if ( categories.put( category, description ) != null ) { messager.printMessage( Diagnostic.Kind.ERROR, - "Logging category %sis already defined in this module. Failed on logger: %s" + "Logging category %s is already defined in this module. Failed on logger: %s" .formatted( category, logger ) ); } } diff --git a/build/jqassistant/rules/rules.xml b/build/jqassistant/rules/rules.xml index 9057dfe7ebd..88c30ce6b5f 100644 --- a/build/jqassistant/rules/rules.xml +++ b/build/jqassistant/rules/rules.xml @@ -282,6 +282,10 @@ WHEN 'hibernate-search-mapper-orm-outbox-polling' THEN 'OutboxPolling' WHEN 'hibernate-search-mapper-orm-jakarta-batch-jberet' THEN 'JBeret' WHEN 'hibernate-search-processor' THEN 'Processor' + WHEN 'hibernate-search-backend-elasticsearch-client-common' THEN 'ClientCommon' + WHEN 'hibernate-search-backend-elasticsearch-client-rest' THEN 'ClientRest' + WHEN 'hibernate-search-backend-elasticsearch-client-java' THEN 'ClientJava' + WHEN 'hibernate-search-backend-elasticsearch-client-opensearch' THEN 'ClientOpenSearch' ELSE 'UNKNOWN-MODULE-SPECIFIC-KEYWORD-PLEASE-UPDATE-JQASSISTANT-RULES' END RETURN @@ -292,11 +296,14 @@ + API/SPI types must not expose internal types. (superType:Type:Impl) + WHERE + NOT type:SuppressJQAssistant RETURN type AS ExposingSite, superType.fqn AS ExposedType @@ -306,6 +313,7 @@ (type:Type:Public)-[:DECLARES]->(method)-[:RETURNS]->(returnType:Type:Impl) WHERE (method.visibility="public" OR method.visibility="protected") + AND NOT type:SuppressJQAssistant RETURN ( method.signature + " (in " + type.fqn + ")" ) AS ExposingSite, returnType.fqn AS ExposedType @@ -315,6 +323,7 @@ (type:Type:Public)-[:DECLARES]->(method)-[:HAS]->(parameter)-[:OF_TYPE]->(parameterType:Type:Impl) WHERE (method.visibility="public" OR method.visibility="protected") + AND NOT type:SuppressJQAssistant RETURN ( method.signature + " (in " + type.fqn + ")" ) AS ExposingSite, parameterType.fqn AS ExposedType @@ -324,6 +333,7 @@ (type:Type:Public)-[:DECLARES]->(field)-[:OF_TYPE]->(fieldType:Type:Impl) WHERE (field.visibility="public" OR field.visibility="protected") + AND NOT type:SuppressJQAssistant RETURN ( field.signature + " (in " + type.fqn + ")" ) AS ExposingSite, fieldType.fqn AS ExposedType ]]> @@ -346,6 +356,7 @@ - or the name of the declaring type ends with 'ConfigurationPropertySource'. - or the name of the declaring type ends with: 'ScrollableResultsAdapter'. - or the name of the declaring type ends with: '_$bundle'. + - or the name of the declaring type ends with: '_$logger'. 9.1.3 + 9.1.3 + 3.2.0 + + 5.5 + 5.3.5 + 4.5.14 + 4.4.16 + 4.1.5 + ${version.org.elasticsearch.latest} https://www.elastic.co/guide/en/elasticsearch/reference/${parsed-version.org.elasticsearch.compatible.main.majorVersion}.${parsed-version.org.elasticsearch.compatible.main.minorVersion} @@ -145,6 +154,7 @@ 3.13.1 3.6.1 1.19.0 + 1.3.5 @@ -470,6 +547,12 @@ commons-codec ${version.commons-codec} + + + commons-logging + commons-logging + ${version.commons-logging} + diff --git a/build/reports/pom.xml b/build/reports/pom.xml index 2517ed776a5..7303852e5fa 100644 --- a/build/reports/pom.xml +++ b/build/reports/pom.xml @@ -50,6 +50,22 @@ org.hibernate.search hibernate-search-backend-elasticsearch-aws + + org.hibernate.search + hibernate-search-backend-elasticsearch-client-common + + + org.hibernate.search + hibernate-search-backend-elasticsearch-client-rest + + + org.hibernate.search + hibernate-search-backend-elasticsearch-client-java + + + org.hibernate.search + hibernate-search-backend-elasticsearch-client-opensearch + org.hibernate.search hibernate-search-mapper-pojo-base diff --git a/distribution/pom.xml b/distribution/pom.xml index 514a1b42b99..d90b5c39021 100644 --- a/distribution/pom.xml +++ b/distribution/pom.xml @@ -90,6 +90,26 @@ hibernate-search-backend-elasticsearch-aws compile + + org.hibernate.search + hibernate-search-backend-elasticsearch-client-common + compile + + + org.hibernate.search + hibernate-search-backend-elasticsearch-client-rest + compile + + + org.hibernate.search + hibernate-search-backend-elasticsearch-client-java + compile + + + org.hibernate.search + hibernate-search-backend-elasticsearch-client-opensearch + compile + org.hibernate.search hibernate-search-v5migrationhelper-engine @@ -256,6 +276,10 @@ ${basedir}/../mapper/orm/src/main/java; ${basedir}/../mapper/pojo-standalone/src/main/java; ${basedir}/../backend/elasticsearch/src/main/java; + ${basedir}/../backend/elasticsearch-client/common/src/main/java; + ${basedir}/../backend/elasticsearch-client/elasticsearch-rest-client/src/main/java; + ${basedir}/../backend/elasticsearch-client/elasticsearch-java-client/src/main/java; + ${basedir}/../backend/elasticsearch-client/opensearch-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; @@ -268,6 +292,10 @@ ${basedir}/../mapper/orm/target/generated-sources/annotations; ${basedir}/../mapper/pojo-standalone/target/generated-sources/annotations; ${basedir}/../backend/elasticsearch/target/generated-sources/annotations; + ${basedir}/../backend/elasticsearch-client/common/target/generated-sources/annotations; + ${basedir}/../backend/elasticsearch-client/elasticsearch-rest-client/target/generated-sources/annotations; + ${basedir}/../backend/elasticsearch-client/opensearch-rest-client/target/generated-sources/annotations; + ${basedir}/../backend/elasticsearch-client/elasticsearch-java-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/src/test/java/org/hibernate/search/documentation/backend/elasticsearch/client/HttpClientConfigurer.java b/documentation/src/test/java/org/hibernate/search/documentation/backend/elasticsearch/client/HttpClientConfigurer.java index db0534b4368..342afde327e 100644 --- a/documentation/src/test/java/org/hibernate/search/documentation/backend/elasticsearch/client/HttpClientConfigurer.java +++ b/documentation/src/test/java/org/hibernate/search/documentation/backend/elasticsearch/client/HttpClientConfigurer.java @@ -4,8 +4,8 @@ */ package org.hibernate.search.documentation.backend.elasticsearch.client; -import org.hibernate.search.backend.elasticsearch.client.ElasticsearchHttpClientConfigurationContext; -import org.hibernate.search.backend.elasticsearch.client.ElasticsearchHttpClientConfigurer; +import org.hibernate.search.backend.elasticsearch.client.rest.ElasticsearchHttpClientConfigurationContext; +import org.hibernate.search.backend.elasticsearch.client.rest.ElasticsearchHttpClientConfigurer; import org.hibernate.search.util.impl.test.extension.StaticCounters; import org.apache.http.HttpResponseInterceptor; diff --git a/documentation/src/test/java/org/hibernate/search/documentation/backend/elasticsearch/layout/ElasticsearchCustomLayoutStrategyIT.java b/documentation/src/test/java/org/hibernate/search/documentation/backend/elasticsearch/layout/ElasticsearchCustomLayoutStrategyIT.java index f43f3a13962..7ddda6922ca 100644 --- a/documentation/src/test/java/org/hibernate/search/documentation/backend/elasticsearch/layout/ElasticsearchCustomLayoutStrategyIT.java +++ b/documentation/src/test/java/org/hibernate/search/documentation/backend/elasticsearch/layout/ElasticsearchCustomLayoutStrategyIT.java @@ -14,7 +14,7 @@ import org.hibernate.search.backend.elasticsearch.ElasticsearchExtension; import org.hibernate.search.backend.elasticsearch.cfg.ElasticsearchBackendSettings; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; +import org.hibernate.search.backend.elasticsearch.client.common.util.spi.URLEncodedString; import org.hibernate.search.documentation.testsupport.BackendConfigurations; import org.hibernate.search.documentation.testsupport.DocumentationSetupHelper; import org.hibernate.search.mapper.orm.Search; diff --git a/documentation/src/test/java/org/hibernate/search/documentation/configuration/ElasticsearchConfigurationIT.java b/documentation/src/test/java/org/hibernate/search/documentation/configuration/ElasticsearchConfigurationIT.java index c2c463dd343..f874d97295c 100644 --- a/documentation/src/test/java/org/hibernate/search/documentation/configuration/ElasticsearchConfigurationIT.java +++ b/documentation/src/test/java/org/hibernate/search/documentation/configuration/ElasticsearchConfigurationIT.java @@ -9,8 +9,8 @@ import java.util.Properties; -import org.hibernate.search.backend.elasticsearch.cfg.ElasticsearchBackendSettings; import org.hibernate.search.backend.elasticsearch.cfg.ElasticsearchIndexSettings; +import org.hibernate.search.backend.elasticsearch.client.common.cfg.ElasticsearchBackendClientCommonSettings; import org.hibernate.search.engine.cfg.BackendSettings; import org.hibernate.search.engine.cfg.EngineSettings; import org.hibernate.search.engine.cfg.IndexSettings; @@ -24,8 +24,8 @@ class ElasticsearchConfigurationIT { private Properties buildHibernateConfiguration() { Properties config = new Properties(); // backend configuration - config.put( BackendSettings.backendKey( ElasticsearchBackendSettings.HOSTS ), "127.0.0.1:9200" ); - config.put( BackendSettings.backendKey( ElasticsearchBackendSettings.PROTOCOL ), "http" ); + config.put( BackendSettings.backendKey( ElasticsearchBackendClientCommonSettings.HOSTS ), "127.0.0.1:9200" ); + config.put( BackendSettings.backendKey( ElasticsearchBackendClientCommonSettings.PROTOCOL ), "http" ); // index configuration config.put( IndexSettings.indexKey( "myIndex", ElasticsearchIndexSettings.INDEXING_MAX_BULK_SIZE ), diff --git a/integrationtest/backend/elasticsearch/pom.xml b/integrationtest/backend/elasticsearch/pom.xml index 32138616241..3ae2960982d 100644 --- a/integrationtest/backend/elasticsearch/pom.xml +++ b/integrationtest/backend/elasticsearch/pom.xml @@ -14,6 +14,18 @@ elasticsearch + + ${project.build.directory}/failsafe-reports/opensearch-rest + ${failsafe.client.elasticsearch.reportsDirectory}/failsafe-summary.xml + + + ${project.build.directory}/failsafe-reports/elasticsearch-rest + ${failsafe.client.opensearch.reportsDirectory}/failsafe-summary.xml + + + ${project.build.directory}/failsafe-reports/elasticsearch-java + ${failsafe.client.elasticsearch.java.reportsDirectory}/failsafe-summary.xml + @@ -22,6 +34,14 @@ hibernate-search-backend-elasticsearch test + + org.hibernate.search + hibernate-search-backend-elasticsearch-client-opensearch + + + org.hibernate.search + hibernate-search-backend-elasticsearch-client-java + org.hibernate.search @@ -66,10 +86,73 @@ ${test.elasticsearch.skip} + ${surefire.executionIdentifier}-elasticsearch-rest + ${failsafe.client.elasticsearch.reportsDirectory} + ${failsafe.client.elasticsearch.summaryFile} + ${failsafe.jvm.args.no-jacoco} @{failsafe.jvm.args.jacoco.client.elasticsearch} ${project.groupId}:hibernate-search-integrationtest-backend-tck + + org.hibernate.search:hibernate-search-backend-elasticsearch-client-opensearch + org.hibernate.search:hibernate-search-backend-elasticsearch-client-java + + + org.hibernate.search.integrationtest.backend.elasticsearch.client.ClientJavaElasticsearchClientFactoryIT + org.hibernate.search.integrationtest.backend.elasticsearch.client.ClientOpenSearchElasticsearchClientFactoryIT + org.hibernate.search.integrationtest.backend.elasticsearch.ElasticsearchExtensionElasticsearchJavaIT + org.hibernate.search.integrationtest.backend.elasticsearch.ElasticsearchExtensionOpenSearchLowLevelIT + + + + + it-opensearch-client + + integration-test + verify + + + ${test.elasticsearch.skip} + ${surefire.executionIdentifier}-opensearch-rest + ${failsafe.client.opensearch.reportsDirectory} + ${failsafe.client.opensearch.summaryFile} + ${failsafe.jvm.args.no-jacoco} @{failsafe.jvm.args.jacoco.client.opensearch} + + + + org.hibernate.search:hibernate-search-backend-elasticsearch-client-java + + + org.hibernate.search.integrationtest.backend.elasticsearch.client.ClientJavaElasticsearchClientFactoryIT + org.hibernate.search.integrationtest.backend.elasticsearch.client.ClientRestElasticsearchClientFactoryIT + org.hibernate.search.integrationtest.backend.elasticsearch.ElasticsearchExtensionLowLevelIT + org.hibernate.search.integrationtest.backend.elasticsearch.ElasticsearchExtensionElasticsearchJavaIT + + + + + it-elasticsearch-java-client + + integration-test + verify + + + ${test.elasticsearch.skip} + ${surefire.executionIdentifier}-elasticsearch-java + ${failsafe.client.elasticsearch.java.reportsDirectory} + ${failsafe.client.elasticsearch.java.summaryFile} + ${failsafe.jvm.args.no-jacoco} @{failsafe.jvm.args.jacoco.client.elasticsearch.java} + + org.hibernate.search:hibernate-search-backend-elasticsearch-client-rest + org.hibernate.search:hibernate-search-backend-elasticsearch-client-opensearch + + + org.hibernate.search.integrationtest.backend.elasticsearch.client.ClientRestElasticsearchClientFactoryIT + org.hibernate.search.integrationtest.backend.elasticsearch.client.ClientOpenSearchElasticsearchClientFactoryIT + org.hibernate.search.integrationtest.backend.elasticsearch.ElasticsearchExtensionLowLevelIT + org.hibernate.search.integrationtest.backend.elasticsearch.ElasticsearchExtensionOpenSearchLowLevelIT + @@ -94,6 +177,60 @@ + + coverage + + + + + org.jacoco + jacoco-maven-plugin + + + jacoco-prepare-agent-integration + + true + + + + jacoco-prepare-agent-integration-elasticsearch + initialize + + prepare-agent-integration + + + failsafe.jvm.args.jacoco.client.elasticsearch + ${project.build.directory}/${jacoco.environment.sub-directory}/elasticsearch/jacoco.exec + + + + jacoco-prepare-agent-integration-elasticsearch-java + initialize + + prepare-agent-integration + + + failsafe.jvm.args.jacoco.client.elasticsearch.java + ${project.build.directory}/${jacoco.environment.sub-directory}/elasticsearch-java/jacoco.exec + + + + jacoco-prepare-agent-integration-opensearch + initialize + + prepare-agent-integration + + + failsafe.jvm.args.jacoco.client.opensearch + ${project.build.directory}/${jacoco.environment.sub-directory}/opensearch/jacoco.exec + + + + + + + diff --git a/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/ElasticsearchExtensionElasticsearchJavaIT.java b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/ElasticsearchExtensionElasticsearchJavaIT.java new file mode 100644 index 00000000000..6dd8f024073 --- /dev/null +++ b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/ElasticsearchExtensionElasticsearchJavaIT.java @@ -0,0 +1,77 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.integrationtest.backend.elasticsearch; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.hibernate.search.backend.elasticsearch.ElasticsearchBackend; +import org.hibernate.search.engine.backend.Backend; +import org.hibernate.search.engine.backend.document.IndexFieldReference; +import org.hibernate.search.engine.backend.document.model.dsl.IndexSchemaElement; +import org.hibernate.search.engine.backend.types.Projectable; +import org.hibernate.search.engine.common.spi.SearchIntegration; +import org.hibernate.search.integrationtest.backend.tck.testsupport.util.extension.SearchSetupHelper; +import org.hibernate.search.util.common.SearchException; +import org.hibernate.search.util.impl.integrationtest.mapper.stub.SimpleMappedIndex; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import co.elastic.clients.transport.rest5_client.low_level.Request; +import co.elastic.clients.transport.rest5_client.low_level.Response; +import co.elastic.clients.transport.rest5_client.low_level.Rest5Client; +import org.apache.hc.client5.http.async.HttpAsyncClient; + +class ElasticsearchExtensionElasticsearchJavaIT { + + @RegisterExtension + public final SearchSetupHelper setupHelper = SearchSetupHelper.create(); + + private final SimpleMappedIndex mainIndex = SimpleMappedIndex.of( IndexBinding::new ).name( "main" ); + + + private SearchIntegration integration; + + @BeforeEach + void setup() { + this.integration = setupHelper.start().withIndexes( mainIndex ).setup().integration(); + } + + @Test + void backend_getClient() throws Exception { + Backend backend = integration.backend(); + ElasticsearchBackend elasticsearchBackend = backend.unwrap( ElasticsearchBackend.class ); + Rest5Client restClient = elasticsearchBackend.client( Rest5Client.class ); + + // Test that the client actually works + Response response = restClient.performRequest( new Request( "GET", "/" ) ); + assertThat( response.getStatusCode() ).isEqualTo( 200 ); + } + + @Test + void backend_getClient_error_invalidClass() { + Backend backend = integration.backend(); + ElasticsearchBackend elasticsearchBackend = backend.unwrap( ElasticsearchBackend.class ); + + assertThatThrownBy( () -> elasticsearchBackend.client( HttpAsyncClient.class ) ) + .isInstanceOf( SearchException.class ) + .hasMessageContainingAll( + "Invalid requested type for client", + HttpAsyncClient.class.getName(), + "The Elasticsearch low-level client can only be unwrapped to", + Rest5Client.class.getName() + ); + } + + private static class IndexBinding { + final IndexFieldReference string; + + IndexBinding(IndexSchemaElement root) { + string = root.field( "string", f -> f.asString().projectable( Projectable.YES ) ).toReference(); + } + } +} diff --git a/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/ElasticsearchExtensionIT.java b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/ElasticsearchExtensionIT.java index 5b075fb169b..124ca2e5c5a 100644 --- a/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/ElasticsearchExtensionIT.java +++ b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/ElasticsearchExtensionIT.java @@ -68,11 +68,7 @@ import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; -import org.apache.http.nio.client.HttpAsyncClient; import org.assertj.core.api.InstanceOfAssertFactories; -import org.elasticsearch.client.Request; -import org.elasticsearch.client.Response; -import org.elasticsearch.client.RestClient; import org.json.JSONException; import org.skyscreamer.jsonassert.JSONCompareMode; @@ -1057,32 +1053,6 @@ void backend_unwrap_error_unknownType() { ); } - @Test - void backend_getClient() throws Exception { - Backend backend = integration.backend(); - ElasticsearchBackend elasticsearchBackend = backend.unwrap( ElasticsearchBackend.class ); - RestClient restClient = elasticsearchBackend.client( RestClient.class ); - - // Test that the client actually works - Response response = restClient.performRequest( new Request( "GET", "/" ) ); - assertThat( response.getStatusLine().getStatusCode() ).isEqualTo( 200 ); - } - - @Test - void backend_getClient_error_invalidClass() { - Backend backend = integration.backend(); - ElasticsearchBackend elasticsearchBackend = backend.unwrap( ElasticsearchBackend.class ); - - assertThatThrownBy( () -> elasticsearchBackend.client( HttpAsyncClient.class ) ) - .isInstanceOf( SearchException.class ) - .hasMessageContainingAll( - "Invalid requested type for client", - HttpAsyncClient.class.getName(), - "The Elasticsearch low-level client can only be unwrapped to", - RestClient.class.getName() - ); - } - @Test void mainIndex_unwrap() { IndexManager mainIndexFromIntegration = integration.indexManager( mainIndex.name() ); diff --git a/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/ElasticsearchExtensionLowLevelIT.java b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/ElasticsearchExtensionLowLevelIT.java new file mode 100644 index 00000000000..22fa13c316b --- /dev/null +++ b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/ElasticsearchExtensionLowLevelIT.java @@ -0,0 +1,76 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.integrationtest.backend.elasticsearch; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.hibernate.search.backend.elasticsearch.ElasticsearchBackend; +import org.hibernate.search.engine.backend.Backend; +import org.hibernate.search.engine.backend.document.IndexFieldReference; +import org.hibernate.search.engine.backend.document.model.dsl.IndexSchemaElement; +import org.hibernate.search.engine.backend.types.Projectable; +import org.hibernate.search.engine.common.spi.SearchIntegration; +import org.hibernate.search.integrationtest.backend.tck.testsupport.util.extension.SearchSetupHelper; +import org.hibernate.search.util.common.SearchException; +import org.hibernate.search.util.impl.integrationtest.mapper.stub.SimpleMappedIndex; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import org.apache.http.nio.client.HttpAsyncClient; +import org.elasticsearch.client.Request; +import org.elasticsearch.client.Response; +import org.elasticsearch.client.RestClient; + +class ElasticsearchExtensionLowLevelIT { + + @RegisterExtension + public final SearchSetupHelper setupHelper = SearchSetupHelper.create(); + + private final SimpleMappedIndex mainIndex = SimpleMappedIndex.of( IndexBinding::new ).name( "main" ); + + private SearchIntegration integration; + + @BeforeEach + void setup() { + this.integration = setupHelper.start().withIndexes( mainIndex ).setup().integration(); + } + + @Test + void backend_getClient() throws Exception { + Backend backend = integration.backend(); + ElasticsearchBackend elasticsearchBackend = backend.unwrap( ElasticsearchBackend.class ); + RestClient restClient = elasticsearchBackend.client( RestClient.class ); + + // Test that the client actually works + Response response = restClient.performRequest( new Request( "GET", "/" ) ); + assertThat( response.getStatusLine().getStatusCode() ).isEqualTo( 200 ); + } + + @Test + void backend_getClient_error_invalidClass() { + Backend backend = integration.backend(); + ElasticsearchBackend elasticsearchBackend = backend.unwrap( ElasticsearchBackend.class ); + + assertThatThrownBy( () -> elasticsearchBackend.client( HttpAsyncClient.class ) ) + .isInstanceOf( SearchException.class ) + .hasMessageContainingAll( + "Invalid requested type for client", + HttpAsyncClient.class.getName(), + "The Elasticsearch low-level client can only be unwrapped to", + RestClient.class.getName() + ); + } + + private static class IndexBinding { + final IndexFieldReference string; + + IndexBinding(IndexSchemaElement root) { + string = root.field( "string", f -> f.asString().projectable( Projectable.YES ) ).toReference(); + } + } +} diff --git a/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/ElasticsearchExtensionOpenSearchLowLevelIT.java b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/ElasticsearchExtensionOpenSearchLowLevelIT.java new file mode 100644 index 00000000000..4db7cd2a266 --- /dev/null +++ b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/ElasticsearchExtensionOpenSearchLowLevelIT.java @@ -0,0 +1,77 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.integrationtest.backend.elasticsearch; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.hibernate.search.backend.elasticsearch.ElasticsearchBackend; +import org.hibernate.search.engine.backend.Backend; +import org.hibernate.search.engine.backend.document.IndexFieldReference; +import org.hibernate.search.engine.backend.document.model.dsl.IndexSchemaElement; +import org.hibernate.search.engine.backend.types.Projectable; +import org.hibernate.search.engine.common.spi.SearchIntegration; +import org.hibernate.search.integrationtest.backend.tck.testsupport.util.extension.SearchSetupHelper; +import org.hibernate.search.util.common.SearchException; +import org.hibernate.search.util.impl.integrationtest.mapper.stub.SimpleMappedIndex; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import org.apache.hc.client5.http.async.HttpAsyncClient; +import org.opensearch.client.Request; +import org.opensearch.client.Response; +import org.opensearch.client.RestClient; + +class ElasticsearchExtensionOpenSearchLowLevelIT { + + @RegisterExtension + public final SearchSetupHelper setupHelper = SearchSetupHelper.create(); + + private final SimpleMappedIndex mainIndex = SimpleMappedIndex.of( IndexBinding::new ).name( "main" ); + + + private SearchIntegration integration; + + @BeforeEach + void setup() { + this.integration = setupHelper.start().withIndexes( mainIndex ).setup().integration(); + } + + @Test + void backend_getClient() throws Exception { + Backend backend = integration.backend(); + ElasticsearchBackend elasticsearchBackend = backend.unwrap( ElasticsearchBackend.class ); + RestClient restClient = elasticsearchBackend.client( RestClient.class ); + + // Test that the client actually works + Response response = restClient.performRequest( new Request( "GET", "/" ) ); + assertThat( response.getStatusLine().getStatusCode() ).isEqualTo( 200 ); + } + + @Test + void backend_getClient_error_invalidClass() { + Backend backend = integration.backend(); + ElasticsearchBackend elasticsearchBackend = backend.unwrap( ElasticsearchBackend.class ); + + assertThatThrownBy( () -> elasticsearchBackend.client( HttpAsyncClient.class ) ) + .isInstanceOf( SearchException.class ) + .hasMessageContainingAll( + "Invalid requested type for client", + HttpAsyncClient.class.getName(), + "The Elasticsearch low-level client can only be unwrapped to", + RestClient.class.getName() + ); + } + + private static class IndexBinding { + final IndexFieldReference string; + + IndexBinding(IndexSchemaElement root) { + string = root.field( "string", f -> f.asString().projectable( Projectable.YES ) ).toReference(); + } + } +} 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..3bb5544b7a3 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 @@ -7,7 +7,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assumptions.assumeTrue; -import org.hibernate.search.backend.elasticsearch.cfg.ElasticsearchBackendSettings; +import org.hibernate.search.backend.elasticsearch.client.common.cfg.ElasticsearchBackendClientCommonSettings; import org.hibernate.search.integrationtest.backend.elasticsearch.testsupport.util.ElasticsearchClientSpy; import org.hibernate.search.integrationtest.backend.elasticsearch.testsupport.util.ElasticsearchTckBackendFeatures; import org.hibernate.search.integrationtest.backend.tck.testsupport.util.extension.SearchSetupHelper; @@ -42,7 +42,7 @@ void cannotConnect() { assertThatThrownBy( () -> setupHelper.start() .withBackendProperty( - ElasticsearchBackendSettings.URIS, + ElasticsearchBackendClientCommonSettings.URIS, // We just need a closed port, hopefully this one will generally be closed "http://localhost:9199" ) 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 8e3ad154413..2c2c5a3db02 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 @@ -17,8 +17,8 @@ import org.hibernate.search.backend.elasticsearch.ElasticsearchVersion; import org.hibernate.search.backend.elasticsearch.cfg.ElasticsearchBackendSettings; import org.hibernate.search.backend.elasticsearch.cfg.ElasticsearchIndexSettings; -import org.hibernate.search.backend.elasticsearch.cfg.impl.ElasticsearchBackendImplSettings; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchRequest; +import org.hibernate.search.backend.elasticsearch.client.common.cfg.spi.ElasticsearchBackendClientSpiSettings; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchRequest; import org.hibernate.search.engine.backend.work.execution.OperationSubmitter; import org.hibernate.search.engine.cfg.BackendSettings; import org.hibernate.search.engine.cfg.spi.AllAwareConfigurationPropertySource; @@ -62,7 +62,7 @@ void explicitModelDialect() { ElasticsearchBackendSettings.VERSION, ElasticsearchTestDialect.getActualVersion().toString() ) .withBackendProperty( - ElasticsearchBackendImplSettings.CLIENT_FACTORY, + ElasticsearchBackendClientSpiSettings.CLIENT_FACTORY, elasticsearchClientSpy.factoryReference() ) .withSchemaManagement( StubMappingSchemaManagementStrategy.DROP_ON_SHUTDOWN_ONLY ) @@ -104,7 +104,7 @@ void noVersionCheck_missingVersion() { ElasticsearchBackendSettings.VERSION_CHECK_ENABLED, false ) .withBackendProperty( - ElasticsearchBackendImplSettings.CLIENT_FACTORY, + ElasticsearchBackendClientSpiSettings.CLIENT_FACTORY, elasticsearchClientSpy.factoryReference() ) .withSchemaManagement( StubMappingSchemaManagementStrategy.DROP_ON_SHUTDOWN_ONLY ) @@ -146,7 +146,7 @@ void noVersionCheck_incompleteVersion() { ElasticsearchBackendSettings.VERSION, versionWithMajorOnly ) .withBackendProperty( - ElasticsearchBackendImplSettings.CLIENT_FACTORY, + ElasticsearchBackendClientSpiSettings.CLIENT_FACTORY, elasticsearchClientSpy.factoryReference() ) .withSchemaManagement( StubMappingSchemaManagementStrategy.DROP_ON_SHUTDOWN_ONLY ) @@ -192,7 +192,7 @@ void noVersionCheck_completeVersion() { ElasticsearchBackendSettings.VERSION, configuredVersion ) .withBackendProperty( - ElasticsearchBackendImplSettings.CLIENT_FACTORY, + ElasticsearchBackendClientSpiSettings.CLIENT_FACTORY, elasticsearchClientSpy.factoryReference() ) .withSchemaManagement( StubMappingSchemaManagementStrategy.DROP_ON_SHUTDOWN_ONLY ) @@ -243,7 +243,7 @@ void noVersionCheck_versionOverrideOnStart_incompatibleVersion() { ElasticsearchBackendSettings.VERSION, configuredVersionOnBackendCreation ) .withBackendProperty( - ElasticsearchBackendImplSettings.CLIENT_FACTORY, + ElasticsearchBackendClientSpiSettings.CLIENT_FACTORY, elasticsearchClientSpy.factoryReference() ) .withSchemaManagement( StubMappingSchemaManagementStrategy.DROP_ON_SHUTDOWN_ONLY ) @@ -296,7 +296,7 @@ void noVersionCheck_versionOverrideOnStart_compatibleVersion() { ElasticsearchBackendSettings.VERSION, versionWithMajorOnly ) .withBackendProperty( - ElasticsearchBackendImplSettings.CLIENT_FACTORY, + ElasticsearchBackendClientSpiSettings.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( ElasticsearchBackendImplSettings.CLIENT_FACTORY, + .withBackendProperty( ElasticsearchBackendClientSpiSettings.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/client/ClientJavaElasticsearchClientFactoryIT.java b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/client/ClientJavaElasticsearchClientFactoryIT.java new file mode 100644 index 00000000000..aef9ce965c1 --- /dev/null +++ b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/client/ClientJavaElasticsearchClientFactoryIT.java @@ -0,0 +1,1384 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.integrationtest.backend.elasticsearch.client; + +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Fail.fail; +import static org.awaitility.Awaitility.await; +import static org.hibernate.search.util.impl.test.JsonHelper.assertJsonEquals; +import static org.junit.jupiter.api.Assumptions.assumeFalse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletionException; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import javax.net.ssl.SSLContext; + +import org.hibernate.search.backend.elasticsearch.client.common.cfg.ElasticsearchBackendClientCommonSettings; +import org.hibernate.search.backend.elasticsearch.client.common.gson.spi.GsonProvider; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchClient; +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.URLEncodedString; +import org.hibernate.search.backend.elasticsearch.client.java.ElasticsearchHttpClientConfigurationContext; +import org.hibernate.search.backend.elasticsearch.client.java.ElasticsearchHttpClientConfigurer; +import org.hibernate.search.backend.elasticsearch.client.java.cfg.ClientJavaElasticsearchBackendClientSettings; +import org.hibernate.search.backend.elasticsearch.client.java.cfg.spi.ClientJavaElasticsearchBackendClientSpiSettings; +import org.hibernate.search.backend.elasticsearch.client.java.impl.ClientJavaElasticsearchClientFactory; +import org.hibernate.search.engine.cfg.ConfigurationPropertySource; +import org.hibernate.search.engine.cfg.spi.AllAwareConfigurationPropertySource; +import org.hibernate.search.engine.cfg.spi.ConfigurationProperty; +import org.hibernate.search.engine.cfg.spi.EngineSpiSettings; +import org.hibernate.search.engine.common.execution.spi.DelegatingSimpleScheduledExecutor; +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.bean.spi.BeanConfigurer; +import org.hibernate.search.engine.environment.thread.impl.EmbeddedThreadProvider; +import org.hibernate.search.engine.environment.thread.impl.ThreadPoolProviderImpl; +import org.hibernate.search.integrationtest.backend.elasticsearch.testsupport.util.ElasticsearchTckBackendHelper; +import org.hibernate.search.util.common.AssertionFailure; +import org.hibernate.search.util.common.SearchException; +import org.hibernate.search.util.impl.integrationtest.backend.elasticsearch.ElasticsearchTestHostConnectionConfiguration; +import org.hibernate.search.util.impl.integrationtest.backend.elasticsearch.dialect.ElasticsearchTestDialect; +import org.hibernate.search.util.impl.integrationtest.common.TestConfigurationProvider; +import org.hibernate.search.util.impl.test.annotation.PortedFromSearch5; +import org.hibernate.search.util.impl.test.annotation.TestForIssue; +import org.hibernate.search.util.impl.test.extension.ExpectedLog4jLog; +import org.hibernate.search.util.impl.test.extension.RetryExtension; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.extension.RegisterExtension; + +import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder; +import com.github.tomakehurst.wiremock.extension.Parameters; +import com.github.tomakehurst.wiremock.http.Fault; +import com.github.tomakehurst.wiremock.http.Request; +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; +import com.github.tomakehurst.wiremock.matching.MatchResult; +import com.github.tomakehurst.wiremock.matching.RequestMatcherExtension; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; + +import co.elastic.clients.transport.rest5_client.low_level.Rest5Client; +import org.apache.hc.client5.http.HttpRoute; +import org.apache.hc.client5.http.config.ConnectionConfig; +import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager; +import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder; +import org.apache.hc.client5.http.nio.AsyncClientConnectionManager; +import org.apache.hc.client5.http.nio.AsyncConnectionEndpoint; +import org.apache.hc.client5.http.ssl.ClientTlsStrategyBuilder; +import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; +import org.apache.hc.client5.http.ssl.TrustAllStrategy; +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpResponseInterceptor; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.io.CloseMode; +import org.apache.hc.core5.pool.PoolEntry; +import org.apache.hc.core5.reactor.ConnectionInitiator; +import org.apache.hc.core5.ssl.SSLContexts; +import org.apache.hc.core5.util.TimeValue; +import org.apache.hc.core5.util.Timeout; +import org.apache.logging.log4j.Level; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +@MockitoSettings(strictness = Strictness.STRICT_STUBS) +@PortedFromSearch5(original = "org.hibernate.search.elasticsearch.test.DefaultElasticsearchClientFactoryTest") +class ClientJavaElasticsearchClientFactoryIT { + + // Some tests in here are flaky, for some reason once in a while wiremock takes a very long time to answer + // even though no delay was configured. + // The exact reason is unknown though, so just try multiple times... + + @RegisterExtension + private final ExpectedLog4jLog logged = ExpectedLog4jLog.create(); + + @RegisterExtension + private final WireMockExtension wireMockRule1 = WireMockExtension.newInstance() + .options( wireMockConfig().dynamicPort().dynamicHttpsPort() ) + .build(); + + @RegisterExtension + private final WireMockExtension wireMockRule2 = WireMockExtension.newInstance() + .options( wireMockConfig().dynamicPort().dynamicHttpsPort() ) + .build(); + + @RegisterExtension + private final TestConfigurationProvider testConfigurationProvider = new TestConfigurationProvider(); + + private final ThreadPoolProviderImpl threadPoolProvider = new ThreadPoolProviderImpl( + BeanHolder.of( + new EmbeddedThreadProvider( ClientJavaElasticsearchClientFactoryIT.class.getName() + ": " ) ) ); + + private final ScheduledExecutorService timeoutExecutorService = + threadPoolProvider.newScheduledExecutor( 1, "Timeout - " ); + + @AfterEach + void cleanup() { + timeoutExecutorService.shutdownNow(); + threadPoolProvider.close(); + + // Avoid side-effects from one test to another + // Ideally WiremockRule should do that by itself, but it doesn't... + wireMockRule1.resetAll(); + wireMockRule2.resetAll(); + } + + @RetryExtension.TestWithRetry + @TestForIssue(jiraKey = "HSEARCH-2274") + void simple_http() { + String payload = "{ \"foo\": \"bar\" }"; + String statusMessage = "OK"; + String responseBody = "{ \"foo\": \"bar\" }"; + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .andMatching( httpProtocol() ) + .willReturn( elasticsearchResponse().withStatus( 200 ) + .withStatusMessage( statusMessage ) + .withBody( responseBody ) ) ); + + try ( ElasticsearchClientImplementor client = createClient() ) { + ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + assertThat( result.statusMessage() ).as( "status message" ).isEqualTo( statusMessage ); + assertJsonEquals( responseBody, result.body().toString() ); + + wireMockRule1.verify( + postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) + .andMatching( httpProtocol() ) + ); + } + } + + @RetryExtension.TestWithRetry + void simple_httpClientConfigurer() throws Exception { + String payload = "{ \"foo\": \"bar\" }"; + String statusMessage = "OK"; + String responseBody = "{ \"foo\": \"bar\" }"; + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .andMatching( httpProtocol() ) + .willReturn( elasticsearchResponse().withStatus( 200 ) + .withStatusMessage( statusMessage ) + .withBody( responseBody ) ) ); + + HttpResponseInterceptor responseInterceptor = spy( HttpResponseInterceptor.class ); + + try ( ElasticsearchClientImplementor client = createClient( properties -> properties.accept( + ClientJavaElasticsearchBackendClientSettings.CLIENT_CONFIGURER, + (ElasticsearchHttpClientConfigurer) context -> context + .clientBuilder() + .addResponseInterceptorFirst( responseInterceptor ) + ) ) ) { + ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + assertThat( result.statusMessage() ).as( "status message" ).isEqualTo( statusMessage ); + assertJsonEquals( responseBody, result.body().toString() ); + + wireMockRule1.verify( + postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) + .andMatching( httpProtocol() ) + ); + } + + verify( responseInterceptor, times( 1 ) ).process( any(), any(), any() ); + } + + @RetryExtension.TestWithRetry + @TestForIssue(jiraKey = "HSEARCH-4099") + void uris_http() { + String payload = "{ \"foo\": \"bar\" }"; + String statusMessage = "OK"; + String responseBody = "{ \"foo\": \"bar\" }"; + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .andMatching( httpProtocol() ) + .willReturn( elasticsearchResponse().withStatus( 200 ) + .withStatusMessage( statusMessage ) + .withBody( responseBody ) ) ); + + try ( ElasticsearchClientImplementor client = createClient( + properties -> { + properties.accept( ElasticsearchBackendClientCommonSettings.URIS, httpUrisFor( wireMockRule1 ) ); + } + ) ) { + ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + assertThat( result.statusMessage() ).as( "status message" ).isEqualTo( statusMessage ); + assertJsonEquals( responseBody, result.body().toString() ); + + wireMockRule1.verify( + postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) + .andMatching( httpProtocol() ) + ); + } + } + + @RetryExtension.TestWithRetry + @TestForIssue(jiraKey = "HSEARCH-4051") + void pathPrefix_http() { + String payload = "{ \"foo\": \"bar\" }"; + String statusMessage = "OK"; + String responseBody = "{ \"foo\": \"bar\" }"; + wireMockRule1.stubFor( post( urlPathMatching( "/bla/bla/bla/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .andMatching( httpProtocol() ) + .willReturn( elasticsearchResponse().withStatus( 200 ) + .withStatusMessage( statusMessage ) + .withBody( responseBody ) ) ); + + try ( ElasticsearchClientImplementor client = createClient( + properties -> { + properties.accept( ElasticsearchBackendClientCommonSettings.PATH_PREFIX, "bla/bla/bla" ); + } + ) ) { + ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + assertThat( result.statusMessage() ).as( "status message" ).isEqualTo( statusMessage ); + assertJsonEquals( responseBody, result.body().toString() ); + + wireMockRule1.verify( + postRequestedFor( urlPathMatching( "/bla/bla/bla/myIndex/myType" ) ) + .andMatching( httpProtocol() ) + ); + } + } + + @RetryExtension.TestWithRetry + @TestForIssue(jiraKey = "HSEARCH-4099") + void pathPrefix_uris() { + String payload = "{ \"foo\": \"bar\" }"; + String statusMessage = "OK"; + String responseBody = "{ \"foo\": \"bar\" }"; + wireMockRule1.stubFor( post( urlPathMatching( "/bla/bla/bla/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .andMatching( httpProtocol() ) + .willReturn( elasticsearchResponse().withStatus( 200 ) + .withStatusMessage( statusMessage ) + .withBody( responseBody ) ) ); + + try ( ElasticsearchClientImplementor client = createClient( + properties -> { + properties.accept( ElasticsearchBackendClientCommonSettings.PATH_PREFIX, "bla/bla/bla" ); + properties.accept( ElasticsearchBackendClientCommonSettings.URIS, httpUrisFor( wireMockRule1 ) ); + } + ) ) { + ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + assertThat( result.statusMessage() ).as( "status message" ).isEqualTo( statusMessage ); + assertJsonEquals( responseBody, result.body().toString() ); + + wireMockRule1.verify( + postRequestedFor( urlPathMatching( "/bla/bla/bla/myIndex/myType" ) ) + .andMatching( httpProtocol() ) + ); + } + } + + @RetryExtension.TestWithRetry + @TestForIssue(jiraKey = "HSEARCH-2274") + void simple_https() { + String payload = "{ \"foo\": \"bar\" }"; + String statusMessage = "OK"; + String responseBody = "{ \"foo\": \"bar\" }"; + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .andMatching( httpsProtocol() ) + .willReturn( elasticsearchResponse().withStatus( 200 ) + .withStatusMessage( statusMessage ) + .withBody( responseBody ) ) ); + + try ( ElasticsearchClientImplementor client = createClient( + properties -> { + properties.accept( ElasticsearchBackendClientCommonSettings.HOSTS, httpsHostAndPortFor( wireMockRule1 ) ); + properties.accept( ElasticsearchBackendClientCommonSettings.PROTOCOL, "https" ); + } + ) ) { + ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + assertThat( result.statusMessage() ).as( "status message" ).isEqualTo( statusMessage ); + assertJsonEquals( responseBody, result.body().toString() ); + + wireMockRule1.verify( + postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) + .andMatching( httpsProtocol() ) + ); + } + } + + @RetryExtension.TestWithRetry + @TestForIssue(jiraKey = "HSEARCH-2274") + void uris_https() { + String payload = "{ \"foo\": \"bar\" }"; + String statusMessage = "OK"; + String responseBody = "{ \"foo\": \"bar\" }"; + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .andMatching( httpsProtocol() ) + .willReturn( elasticsearchResponse().withStatus( 200 ) + .withStatusMessage( statusMessage ) + .withBody( responseBody ) ) ); + + try ( ElasticsearchClientImplementor client = createClient( + properties -> { + properties.accept( ElasticsearchBackendClientCommonSettings.URIS, httpsUrisFor( wireMockRule1 ) ); + } + ) ) { + ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + assertThat( result.statusMessage() ).as( "status message" ).isEqualTo( statusMessage ); + assertJsonEquals( responseBody, result.body().toString() ); + + wireMockRule1.verify( + postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) + .andMatching( httpsProtocol() ) + ); + } + } + + @RetryExtension.TestWithRetry + void error() { + String payload = "{ \"foo\": \"bar\" }"; + String responseBody = "{ \"error\": \"ErrorMessageExplainingTheError\" }"; + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .willReturn( + elasticsearchResponse().withStatus( 500 ) + .withBody( responseBody ) + ) ); + + try ( ElasticsearchClientImplementor client = createClient() ) { + ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 500 ); + assertJsonEquals( responseBody, result.body().toString() ); + } + } + + @RetryExtension.TestWithRetry + void unparseable() { + String payload = "{ \"foo\": \"bar\" }"; + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .willReturn( + elasticsearchResponse() + .withBody( "'unparseable" ) + ) ); + + assertThatThrownBy( () -> { + try ( ElasticsearchClientImplementor client = createClient() ) { + doPost( client, "/myIndex/myType", payload ); + } + } ) + .isInstanceOf( AssertionFailure.class ) + .extracting( Throwable::getCause, InstanceOfAssertFactories.THROWABLE ) + .isInstanceOf( CompletionException.class ) + .extracting( Throwable::getCause, InstanceOfAssertFactories.THROWABLE ) + .isInstanceOf( SearchException.class ) + .hasMessageContaining( "HSEARCH400089" ) + .extracting( Throwable::getCause, InstanceOfAssertFactories.THROWABLE ) + .isInstanceOf( JsonSyntaxException.class ); + } + + @RetryExtension.TestWithRetry + void timeout_read() { + String payload = "{ \"foo\": \"bar\" }"; + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .willReturn( + elasticsearchResponse() + .withFixedDelay( 2000 ) + ) ); + + assertThatThrownBy( () -> { + try ( ElasticsearchClientImplementor client = createClient( + properties -> { + properties.accept( ClientJavaElasticsearchBackendClientSettings.READ_TIMEOUT, "1000" ); + properties.accept( ClientJavaElasticsearchBackendClientSettings.REQUEST_TIMEOUT, "99999" ); + } + ) ) { + doPost( client, "/myIndex/myType", payload ); + } + } ) + .isInstanceOf( AssertionFailure.class ) + .extracting( Throwable::getCause, InstanceOfAssertFactories.THROWABLE ) + .isInstanceOf( CompletionException.class ) + .extracting( Throwable::getCause, InstanceOfAssertFactories.THROWABLE ) + .isInstanceOf( IOException.class ); + } + + @RetryExtension.TestWithRetry + void timeout_request() { + String payload = "{ \"foo\": \"bar\" }"; + + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .willReturn( + elasticsearchResponse() + .withFixedDelay( 2000 ) + ) ); + + assertThatThrownBy( () -> { + try ( ElasticsearchClientImplementor client = createClient( + properties -> { + properties.accept( ClientJavaElasticsearchBackendClientSettings.READ_TIMEOUT, "99999" ); + properties.accept( ClientJavaElasticsearchBackendClientSettings.REQUEST_TIMEOUT, "1000" ); + } + ) ) { + doPost( client, "/myIndex/myType", payload ); + } + } ) + .isInstanceOf( AssertionFailure.class ) + .extracting( Throwable::getCause, InstanceOfAssertFactories.THROWABLE ) + .isInstanceOf( CompletionException.class ) + .extracting( Throwable::getCause, InstanceOfAssertFactories.THROWABLE ) + .isInstanceOf( SearchException.class ) + .hasMessageContainingAll( "Request execution exceeded the timeout of 1s, 0ms and 0ns", + "Request was POST /myIndex/myType with parameters {}" ); + } + + /** + * Verify that by default, even when the client is clogged (many pending requests), + * we don't trigger timeouts just because requests spend a long time waiting; + * timeouts are only related to how long the *server* takes to answer. + */ + @RetryExtension.TestWithRetry + @TestForIssue(jiraKey = "HSEARCH-2836") + void cloggedClient_noTimeout_read() { + String payload = "{ \"foo\": \"bar\" }"; + wireMockRule1.stubFor( post( urlPathMatching( "/long" ) ) + .willReturn( elasticsearchResponse() + .withFixedDelay( 300 /* 300ms => should not time out, but will still clog up the client */ ) ) ); + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .willReturn( elasticsearchResponse().withStatus( 200 ) + .withFixedDelay( 100 /* 100ms => should not time out */ ) ) ); + + try ( ElasticsearchClientImplementor client = createClient( + properties -> { + properties.accept( ElasticsearchBackendClientCommonSettings.HOSTS, httpHostAndPortFor( wireMockRule1 ) ); + properties.accept( ClientJavaElasticsearchBackendClientSettings.MAX_CONNECTIONS_PER_ROUTE, "1" ); + properties.accept( ClientJavaElasticsearchBackendClientSettings.READ_TIMEOUT, "1000" /* 1s */ ); + } + ) ) { + // Clog up the client: put many requests in the queue, to be executed asynchronously, + // so that we're sure the next request will have to wait in the queue + // for more that the configured timeout before it ends up being executed. + for ( int i = 0; i < 10; ++i ) { + client.submit( buildRequest( ElasticsearchRequest.post(), "/long", payload ) ); + } + + ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + + wireMockRule1.verify( 1, postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) ); + } + } + + /** + * Verify that when a request timeout is set, and when the client is clogged (many pending requests), + * we do trigger timeouts just because requests spend a long time waiting. + */ + @RetryExtension.TestWithRetry + @TestForIssue(jiraKey = "HSEARCH-2836") + void cloggedClient_timeout_request() { + String payload = "{ \"foo\": \"bar\" }"; + wireMockRule1.stubFor( post( urlPathMatching( "/long" ) ) + .willReturn( elasticsearchResponse() + .withFixedDelay( 300 /* 300ms => should not time out, but will still clog up the client */ ) ) ); + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .willReturn( elasticsearchResponse().withStatus( 200 ) + .withFixedDelay( 100 /* 100ms => should not time out */ ) ) ); + + try ( ElasticsearchClientImplementor client = createClient( + properties -> { + properties.accept( ElasticsearchBackendClientCommonSettings.HOSTS, httpHostAndPortFor( wireMockRule1 ) ); + properties.accept( ClientJavaElasticsearchBackendClientSettings.MAX_CONNECTIONS_PER_ROUTE, "1" ); + properties.accept( ClientJavaElasticsearchBackendClientSettings.REQUEST_TIMEOUT, "1000" /* 1s */ ); + } + ) ) { + // Clog up the client: put many requests in the queue, to be executed asynchronously, + // so that we're sure the next request will have to wait in the queue + // for more that the configured timeout before it ends up being executed. + for ( int i = 0; i < 10; ++i ) { + client.submit( buildRequest( ElasticsearchRequest.post(), "/long", payload ) ); + } + + assertThatThrownBy( () -> { + doPost( client, "/myIndex/myType", payload ); + } ) + .isInstanceOf( AssertionFailure.class ) + .extracting( Throwable::getCause, InstanceOfAssertFactories.THROWABLE ) + .isInstanceOf( CompletionException.class ) + .extracting( Throwable::getCause, InstanceOfAssertFactories.THROWABLE ) + .isInstanceOf( SearchException.class ) + .hasMessageContainingAll( "Request execution exceeded the timeout of 1s, 0ms and 0ns", + "Request was POST /myIndex/myType with parameters {}" ); + } + } + + @RetryExtension.TestWithRetry + @TestForIssue(jiraKey = "HSEARCH-2235") + void multipleHosts() { + String payload = "{ \"foo\": \"bar\" }"; + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .willReturn( elasticsearchResponse().withStatus( 200 ) ) ); + wireMockRule2.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .willReturn( elasticsearchResponse().withStatus( 200 ) ) ); + + try ( ElasticsearchClientImplementor client = createClient( + properties -> { + properties.accept( ElasticsearchBackendClientCommonSettings.HOSTS, + httpHostAndPortFor( wireMockRule1, wireMockRule2 ) ); + } + ) ) { + ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + + wireMockRule1.verify( postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) ); + wireMockRule2.verify( postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) ); + } + } + + @RetryExtension.TestWithRetry + void multipleURIs() { + String payload = "{ \"foo\": \"bar\" }"; + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .willReturn( elasticsearchResponse().withStatus( 200 ) ) ); + wireMockRule2.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .willReturn( elasticsearchResponse().withStatus( 200 ) ) ); + + try ( ElasticsearchClientImplementor client = createClient( + properties -> { + properties.accept( ElasticsearchBackendClientCommonSettings.URIS, + httpsUrisFor( wireMockRule1, wireMockRule2 ) ); + } + ) ) { + ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + + wireMockRule1.verify( postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) ); + wireMockRule2.verify( postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) ); + } + } + + @RetryExtension.TestWithRetry + @TestForIssue(jiraKey = "HSEARCH-2469") + void multipleHosts_failover_serverError() { + String payload = "{ \"foo\": \"bar\" }"; + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .willReturn( elasticsearchResponse().withStatus( 200 ) ) ); + wireMockRule2.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .willReturn( elasticsearchResponse().withStatus( 503 ) ) ); + + try ( ElasticsearchClientImplementor client = createClient( + properties -> { + properties.accept( ElasticsearchBackendClientCommonSettings.HOSTS, + httpHostAndPortFor( wireMockRule1, wireMockRule2 ) ); + } + ) ) { + ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + + wireMockRule1.verify( 2, postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) ); + wireMockRule2.verify( 2, postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) ); + + wireMockRule1.resetRequests(); + wireMockRule2.resetRequests(); + + result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + + // Must not use the failing node anymore + wireMockRule1.verify( 2, postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) ); + wireMockRule2.verify( 0, postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) ); + } + } + + @RetryExtension.TestWithRetry + @TestForIssue(jiraKey = "HSEARCH-2469") + void multipleHosts_failover_timeout() { + String payload = "{ \"foo\": \"bar\" }"; + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .willReturn( elasticsearchResponse().withStatus( 200 ) ) ); + wireMockRule2.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .willReturn( elasticsearchResponse().withStatus( 200 ).withFixedDelay( 5_000 /* 5s => will time out */ ) ) ); + + try ( ElasticsearchClientImplementor client = createClient( + properties -> { + properties.accept( ElasticsearchBackendClientCommonSettings.HOSTS, + httpHostAndPortFor( wireMockRule1, wireMockRule2 ) ); + // Use a timeout much higher than 1s, because wiremock can be really slow... + properties.accept( ClientJavaElasticsearchBackendClientSettings.READ_TIMEOUT, "1000" /* 1s */ ); + } + ) ) { + ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + + wireMockRule1.verify( 2, postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) ); + wireMockRule2.verify( 1, postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) ); + + wireMockRule1.resetRequests(); + wireMockRule2.resetRequests(); + + /* + * Remove the failure in the previously failing node, + * so that we can detect if requests are sent to this node. + */ + wireMockRule2.resetMappings(); + wireMockRule2.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .willReturn( elasticsearchResponse().withStatus( 200 ) ) ); + + result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + + // Must not use the failing node anymore + wireMockRule1.verify( 2, postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) ); + wireMockRule2.verify( 0, postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) ); + } + } + + @RetryExtension.TestWithRetry + @TestForIssue(jiraKey = "HSEARCH-2469") + void multipleHosts_failover_fault() { + String payload = "{ \"foo\": \"bar\" }"; + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .willReturn( elasticsearchResponse().withStatus( 200 ) ) ); + wireMockRule2.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .willReturn( elasticsearchResponse().withStatus( 200 ).withFault( Fault.MALFORMED_RESPONSE_CHUNK ) ) ); + + try ( ElasticsearchClientImplementor client = createClient( + properties -> { + properties.accept( ElasticsearchBackendClientCommonSettings.HOSTS, + httpHostAndPortFor( wireMockRule1, wireMockRule2 ) ); + } + ) ) { + ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + + wireMockRule1.verify( 2, postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) ); + wireMockRule2.verify( 1, postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) ); + + wireMockRule1.resetRequests(); + wireMockRule2.resetRequests(); + + result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + + // Must not use the failing node anymore + wireMockRule1.verify( 2, postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) ); + wireMockRule2.verify( 0, postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) ); + } + } + + @RetryExtension.TestWithRetry + @TestForIssue(jiraKey = "HSEARCH-2449") + void discovery_http() { + String nodesInfoResult = dummyNodeInfoResponse( wireMockRule1.getPort(), wireMockRule2.getPort() ); + + wireMockRule1.stubFor( get( urlPathMatching( "/_nodes.*" ) ) + .andMatching( httpProtocol() ) + .willReturn( elasticsearchResponse().withStatus( 200 ).withBody( nodesInfoResult ) ) ); + wireMockRule2.stubFor( get( urlPathMatching( "/_nodes.*" ) ) + .andMatching( httpProtocol() ) + .willReturn( elasticsearchResponse().withStatus( 200 ).withBody( nodesInfoResult ) ) ); + + String payload = "{ \"foo\": \"bar\" }"; + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .andMatching( httpProtocol() ) + .willReturn( elasticsearchResponse().withStatus( 200 ) ) ); + wireMockRule2.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .andMatching( httpProtocol() ) + .willReturn( elasticsearchResponse().withStatus( 200 ) ) ); + + try ( ElasticsearchClientImplementor client = createClient( + properties -> { + properties.accept( ClientJavaElasticsearchBackendClientSettings.DISCOVERY_ENABLED, "true" ); + properties.accept( ClientJavaElasticsearchBackendClientSettings.DISCOVERY_REFRESH_INTERVAL, "1" ); + } + ) ) { + ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + + /* + * Send requests repeatedly until both hosts have been targeted. + * This should happen pretty early (as soon as we sent two requests, actually), + * but there is always the risk that the sniffer would send a request + * between our own requests, effectively making our own requests target the same host + * (since the hosts are each targeted in turn). + */ + await().untilAsserted( () -> { + ElasticsearchResponse newResult = doPost( client, "/myIndex/myType", payload ); + assertThat( newResult.statusCode() ).as( "status code" ).isEqualTo( 200 ); + + wireMockRule2.verify( + postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) + .andMatching( httpProtocol() ) + ); + wireMockRule2.verify( + postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) + .andMatching( httpProtocol() ) + ); + } ); + } + } + + @RetryExtension.TestWithRetry + @TestForIssue(jiraKey = "HSEARCH-2736") + void discovery_https() { + String nodesInfoResult = dummyNodeInfoResponse( wireMockRule1.getHttpsPort(), wireMockRule2.getHttpsPort() ); + + wireMockRule1.stubFor( get( urlPathMatching( "/_nodes.*" ) ) + .andMatching( httpsProtocol() ) + .willReturn( elasticsearchResponse().withStatus( 200 ).withBody( nodesInfoResult ) ) ); + wireMockRule2.stubFor( get( urlPathMatching( "/_nodes.*" ) ) + .andMatching( httpsProtocol() ) + .willReturn( elasticsearchResponse().withStatus( 200 ).withBody( nodesInfoResult ) ) ); + + String payload = "{ \"foo\": \"bar\" }"; + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .andMatching( httpsProtocol() ) + .willReturn( elasticsearchResponse().withStatus( 200 ) ) ); + wireMockRule2.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .andMatching( httpsProtocol() ) + .willReturn( elasticsearchResponse().withStatus( 200 ) ) ); + + try ( ElasticsearchClientImplementor client = createClient( + properties -> { + properties.accept( ElasticsearchBackendClientCommonSettings.HOSTS, httpsHostAndPortFor( wireMockRule1 ) ); + properties.accept( ElasticsearchBackendClientCommonSettings.PROTOCOL, "https" ); + properties.accept( ClientJavaElasticsearchBackendClientSettings.DISCOVERY_ENABLED, "true" ); + properties.accept( ClientJavaElasticsearchBackendClientSettings.DISCOVERY_REFRESH_INTERVAL, "1" ); + } + ) ) { + /* + * Send requests repeatedly until both hosts have been targeted. + * This should happen pretty early (as soon as we sent two requests, actually), + * but there is always the risk that the sniffer would send a request + * between our own requests, effectively making our own requests target the same host + * (since the hosts are each targeted in turn). + */ + await().untilAsserted( () -> { + ElasticsearchResponse newResult = doPost( client, "/myIndex/myType", payload ); + assertThat( newResult.statusCode() ).as( "status code" ).isEqualTo( 200 ); + + wireMockRule2.verify( + postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) + .andMatching( httpsProtocol() ) + ); + wireMockRule2.verify( + postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) + .andMatching( httpsProtocol() ) + ); + } ); + } + } + + private static RequestMatcherExtension httpProtocol() { + return protocol( "http" ); + } + + private static RequestMatcherExtension httpsProtocol() { + return protocol( "https" ); + } + + private static RequestMatcherExtension protocol(String protocol) { + return new RequestMatcherExtension() { + @Override + public MatchResult match(Request request, Parameters parameters) { + return MatchResult.of( protocol.equals( request.getScheme() ) ); + } + + @Override + public String getName() { + return "expected protocol: " + protocol; + } + }; + } + + @RetryExtension.TestWithRetry + @TestForIssue(jiraKey = "HSEARCH-2453") + void authentication() { + assumeFalse( + ElasticsearchTestHostConnectionConfiguration.get().isAws(), + "This test only is only relevant if Elasticsearch request are *NOT* automatically" + + " augmented with an \"Authentication:\" header." + + " \"Authentication:\" headers are added by the AWS integration in particular." + ); + String username = "ironman"; + String password = "j@rV1s"; + + String payload = "{ \"foo\": \"bar\" }"; + + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType/_search" ) ) + .withRequestBody( equalToJson( payload ) ) + .willReturn( elasticsearchResponse().withStatus( 401 ) + .withHeader( "www-authenticate", "Basic realm=\"IT Realm\"" ) ) ); + + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType/_search" ) ) + .withBasicAuth( username, password ) + .withRequestBody( equalToJson( payload ) ) + .willReturn( elasticsearchResponse().withStatus( 200 ) ) ); + + try ( ElasticsearchClientImplementor client = createClient( + properties -> { + properties.accept( ElasticsearchBackendClientCommonSettings.USERNAME, username ); + properties.accept( ElasticsearchBackendClientCommonSettings.PASSWORD, password ); + } + ) ) { + ElasticsearchResponse result = doPost( client, "/myIndex/myType/_search", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + } + } + + @RetryExtension.TestWithRetry + @TestForIssue(jiraKey = "HSEARCH-2453") + void authentication_error() { + String payload = "{ \"foo\": \"bar\" }"; + String statusMessage = "Unauthorized"; + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType/_search" ) ) + .withRequestBody( equalToJson( payload ) ) + .willReturn( + elasticsearchResponse().withStatus( 401 /* Unauthorized */ ) + .withStatusMessage( statusMessage ) + ) ); + + try ( ElasticsearchClientImplementor client = createClient() ) { + ElasticsearchResponse result = doPost( client, "/myIndex/myType/_search", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 401 ); + assertThat( result.statusMessage() ).as( "status message" ).isEqualTo( statusMessage ); + } + } + + @RetryExtension.TestWithRetry + @TestForIssue(jiraKey = "HSEARCH-2453") + void authentication_http_password() { + String username = "ironman"; + String password = "j@rV1s"; + + logged.expectEvent( Level.WARN, "The password will be sent in clear text over the network" ); + + try ( ElasticsearchClientImplementor client = createClient( + properties -> { + properties.accept( ElasticsearchBackendClientCommonSettings.USERNAME, username ); + properties.accept( ElasticsearchBackendClientCommonSettings.PASSWORD, password ); + } + ) ) { + // Nothing to do here + } + } + + @RetryExtension.TestWithRetry + void uriAndProtocol() { + Consumer> additionalProperties = properties -> { + properties.accept( ElasticsearchBackendClientCommonSettings.URIS, "http://is-not-called:12345" ); + properties.accept( ElasticsearchBackendClientCommonSettings.PROTOCOL, "http" ); + }; + + assertThatThrownBy( () -> createClient( additionalProperties ) ) + .isInstanceOf( SearchException.class ) + .hasMessageContainingAll( + "Invalid target hosts configuration", + "both the 'uris' property and the 'protocol' property are set", + "Uris: '[http://is-not-called:12345]'", "Protocol: 'http'", + "Either set the protocol and hosts simultaneously using the 'uris' property", + "or set them separately using the 'protocol' property and the 'hosts' property" + ); + } + + @RetryExtension.TestWithRetry + void uriAndHosts() { + Consumer> additionalProperties = properties -> { + properties.accept( ElasticsearchBackendClientCommonSettings.URIS, "http://is-not-called:12345" ); + properties.accept( ElasticsearchBackendClientCommonSettings.HOSTS, "not-called-either:234" ); + }; + + assertThatThrownBy( () -> createClient( additionalProperties ) ) + .isInstanceOf( SearchException.class ) + .hasMessageContainingAll( + "Invalid target hosts configuration", + "both the 'uris' property and the 'hosts' property are set", + "Uris: '[http://is-not-called:12345]'", "Hosts: '[", // host and port are dynamic + "Either set the protocol and hosts simultaneously using the 'uris' property", + "or set them separately using the 'protocol' property and the 'hosts' property" + ); + } + + @RetryExtension.TestWithRetry + void differentProtocolsOnUris() { + Consumer> additionalProperties = properties -> { + properties.accept( ElasticsearchBackendClientCommonSettings.URIS, + "http://is-not-called:12345, https://neather-is:12345" ); + }; + + assertThatThrownBy( () -> createClient( additionalProperties ) ) + .isInstanceOf( SearchException.class ) + .hasMessageContainingAll( + "Invalid target hosts configuration: the 'uris' use different protocols (http, https)", + "All URIs must use the same protocol", + "Uris: '[http://is-not-called:12345, https://neather-is:12345]'" + ); + } + + @RetryExtension.TestWithRetry + void emptyListOfUris() { + Consumer> additionalProperties = properties -> { + properties.accept( ElasticsearchBackendClientCommonSettings.URIS, Collections.emptyList() ); + }; + + assertThatThrownBy( () -> createClient( additionalProperties ) ) + .isInstanceOf( SearchException.class ) + .hasMessageContaining( + "Invalid target hosts configuration: the list of URIs must not be empty" + ); + } + + @RetryExtension.TestWithRetry + void emptyListOfHosts() { + Consumer> additionalProperties = properties -> { + properties.accept( ElasticsearchBackendClientCommonSettings.HOSTS, Collections.emptyList() ); + }; + + assertThatThrownBy( () -> createClient( additionalProperties ) ) + .isInstanceOf( SearchException.class ) + .hasMessageContaining( + "Invalid target hosts configuration: the list of hosts must not be empty" + ); + } + + @RetryExtension.TestWithRetry + void clientInstance() throws IOException, URISyntaxException { + try ( Rest5Client myRestClient = Rest5Client.builder( HttpHost.create( httpUrisFor( wireMockRule1 ) ) ).build() ) { + String payload = "{ \"foo\": \"bar\" }"; + String statusMessage = "OK"; + String responseBody = "{ \"foo\": \"bar\" }"; + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .andMatching( httpProtocol() ) + .willReturn( elasticsearchResponse().withStatus( 200 ) + .withStatusMessage( statusMessage ) + .withBody( responseBody ) ) ); + + try ( ElasticsearchClientImplementor client = createClient( properties -> { + properties.accept( ClientJavaElasticsearchBackendClientSpiSettings.CLIENT_INSTANCE, + BeanReference.ofInstance( myRestClient ) ); + } ) ) { + ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + assertThat( result.statusMessage() ).as( "status message" ).isEqualTo( statusMessage ); + assertJsonEquals( responseBody, result.body().toString() ); + + wireMockRule1.verify( + postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) + .andMatching( httpProtocol() ) + ); + } + + // Hibernate Search must not close provided client instances + assertThat( myRestClient ).returns( true, Rest5Client::isRunning ); + } + } + + @RetryExtension.TestWithRetry + void maxKeepAliveNegativeValue() { + assertThatThrownBy( () -> createClient( + properties -> { + properties.accept( ClientJavaElasticsearchBackendClientSettings.MAX_KEEP_ALIVE, -1 ); + } + ) ).isInstanceOf( SearchException.class ) + .hasMessageContainingAll( + "Invalid value for configuration property", + "must be positive or zero" + ); + } + + @RetryExtension.TestWithRetry + void maxKeepAliveConnectionIsNotClosed() throws InterruptedException { + maxKeepAliveConnection( 100000, 1 ); + } + + @RetryExtension.TestWithRetry + void maxKeepAliveConnectionIsClosed() throws InterruptedException { + maxKeepAliveConnection( 10, 2 ); + } + + void maxKeepAliveConnection(long time, int connections) throws InterruptedException { + String payload = "{ \"foo\": \"bar\" }"; + String statusMessage = "OK"; + String responseBody = "{ \"foo\": \"bar\" }"; + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .andMatching( httpProtocol() ) + .willReturn( elasticsearchResponse().withStatus( 200 ) + .withStatusMessage( statusMessage ) + .withBody( responseBody ) ) ); + + Set usedConnections = Collections.synchronizedSet( new HashSet<>() ); + try ( ElasticsearchClientImplementor client = createClient( properties -> { + properties.accept( ClientJavaElasticsearchBackendClientSettings.MAX_KEEP_ALIVE, time ); + properties.accept( + ClientJavaElasticsearchBackendClientSettings.CLIENT_CONFIGURER, + (ElasticsearchHttpClientConfigurer) context -> { + context.clientBuilder() + .setConnectionManager( createPoolManager( usedConnections, connections, connections ) ); + } + ); + } ) ) { + + ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + assertThat( result.statusMessage() ).as( "status message" ).isEqualTo( statusMessage ); + assertJsonEquals( responseBody, result.body().toString() ); + + wireMockRule1.verify( + postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) + .andMatching( httpProtocol() ) + ); + + TimeUnit.SECONDS.sleep( 2 ); + + result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + assertThat( result.statusMessage() ).as( "status message" ).isEqualTo( statusMessage ); + assertJsonEquals( responseBody, result.body().toString() ); + + wireMockRule1.verify( + postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) + .andMatching( httpProtocol() ) + ); + + assertThat( usedConnections ).hasSize( connections ); + } + } + + private AsyncClientConnectionManager createPoolManager(Set connections, int maxConnTotal, int maxConnPerRoute) { + PoolingAsyncClientConnectionManager delegate = PoolingAsyncClientConnectionManagerBuilder.create() + .setMaxConnTotal( maxConnTotal ) + .setMaxConnPerRoute( maxConnPerRoute ) + .setTlsStrategy( + ClientTlsStrategyBuilder.create() + .setSslContext( buildAllowAnythingSSLContext() ) + .setHostnameVerifier( org.apache.http.conn.ssl.NoopHostnameVerifier.INSTANCE ) + .buildAsync() + ) + .build(); + + return new AsyncClientConnectionManager() { + private Field poolEntryRef; + + @Override + public void close() throws IOException { + delegate.close(); + } + + @Override + public void close(CloseMode closeMode) { + delegate.close( closeMode ); + } + + @Override + public Future lease(String id, HttpRoute route, Object state, Timeout requestTimeout, + FutureCallback callback) { + return delegate.lease( id, route, state, requestTimeout, callback ); + } + + @SuppressWarnings("unchecked") + @Override + public void release(AsyncConnectionEndpoint endpoint, Object state, TimeValue keepAlive) { + try { + if ( poolEntryRef == null ) { + poolEntryRef = endpoint.getClass().getDeclaredField( "poolEntryRef" ); + poolEntryRef.setAccessible( true ); + } + AtomicReference> ref = (AtomicReference>) poolEntryRef.get( endpoint ); + connections.add( ref.get().getConnection().toString() ); + + } + catch (NoSuchFieldException | IllegalAccessException e) { + fail( e.getMessage(), e ); + } + delegate.release( endpoint, state, keepAlive ); + } + + @Override + public Future connect(AsyncConnectionEndpoint endpoint, + ConnectionInitiator connectionInitiator, Timeout connectTimeout, Object attachment, HttpContext context, + FutureCallback callback) { + return delegate.connect( endpoint, connectionInitiator, connectTimeout, attachment, context, callback ); + } + + @Override + public void upgrade(AsyncConnectionEndpoint endpoint, Object attachment, HttpContext context) { + delegate.upgrade( endpoint, attachment, context ); + } + }; + } + + private ElasticsearchClientImplementor createClient() { + return createClient( ignored -> {} ); + } + + private ElasticsearchClientImplementor createClient(Consumer> additionalProperties) { + Map defaultBackendProperties = + new ElasticsearchTckBackendHelper().createDefaultBackendSetupStrategy() + .createBackendConfigurationProperties( testConfigurationProvider ); + + Map clientProperties = new HashMap<>( defaultBackendProperties ); + + // We won't target the provided ES instance, but Wiremock + clientProperties.remove( ElasticsearchBackendClientCommonSettings.HOSTS ); + clientProperties.remove( ElasticsearchBackendClientCommonSettings.PROTOCOL ); + clientProperties.remove( ElasticsearchBackendClientCommonSettings.URIS ); + + // Per-test overrides + additionalProperties.accept( clientProperties::put ); + + // By default, target the Wiremock server 1 using HTTP + if ( !clientProperties.containsKey( ElasticsearchBackendClientCommonSettings.HOSTS ) + && !clientProperties.containsKey( ElasticsearchBackendClientCommonSettings.URIS ) ) { + clientProperties.put( ElasticsearchBackendClientCommonSettings.URIS, httpUrisFor( wireMockRule1 ) ); + } + + ConfigurationPropertySource clientPropertySource = AllAwareConfigurationPropertySource.fromMap( clientProperties ); + + Map beanResolverConfiguration = new HashMap<>(); + // Accept Wiremock's self-signed SSL certificates + beanResolverConfiguration.put( + EngineSpiSettings.BEAN_CONFIGURERS, + Collections.singletonList( elasticsearchSslBeanConfigurer( clientPropertySource ) ) + ); + + BeanResolver beanResolver = testConfigurationProvider.createBeanResolverForTest( + AllAwareConfigurationPropertySource.fromMap( beanResolverConfiguration ) + + ); + return new ClientJavaElasticsearchClientFactory().create( beanResolver, clientPropertySource, + threadPoolProvider.threadProvider(), "Client", + new DelegatingSimpleScheduledExecutor( timeoutExecutorService, true ), + GsonProvider.create( GsonBuilder::new, true ) + ); + } + + private ElasticsearchResponse doPost(ElasticsearchClient client, String path, String payload) { + try { + return client.submit( buildRequest( ElasticsearchRequest.post(), path, payload ) ).join(); + } + catch (RuntimeException e) { + throw new AssertionFailure( "Unexpected exception during POST: " + e.getMessage(), e ); + } + } + + private ElasticsearchRequest buildRequest(ElasticsearchRequest.Builder builder, String path, String payload) { + for ( String pathComponent : path.split( "/" ) ) { + if ( !pathComponent.isEmpty() ) { + URLEncodedString fromString = URLEncodedString.fromString( pathComponent ); + builder = builder.pathComponent( fromString ); + } + } + if ( payload != null ) { + builder = builder.body( JsonParser.parseString( payload ).getAsJsonObject() ); + } + return builder.build(); + } + + private static String httpHostAndPortFor(WireMockExtension... extensions) { + return Arrays.stream( extensions ) + .map( extension -> "localhost:" + extension.getPort() ) + .collect( Collectors.joining( "," ) ); + } + + private static String httpsHostAndPortFor(WireMockExtension... extensions) { + return Arrays.stream( extensions ) + .map( extension -> "localhost:" + extension.getHttpsPort() ) + .collect( Collectors.joining( "," ) ); + } + + private static String httpUrisFor(WireMockExtension... extensions) { + return Arrays.stream( extensions ) + .map( extension -> "http://localhost:" + extension.getPort() ) + .collect( Collectors.joining( "," ) ); + } + + private static String httpsUrisFor(WireMockExtension... extensions) { + return Arrays.stream( extensions ) + .map( extension -> "https://localhost:" + extension.getHttpsPort() ) + .collect( Collectors.joining( "," ) ); + } + + private static ResponseDefinitionBuilder elasticsearchResponse() { + return ResponseDefinitionBuilder.okForEmptyJson(); + } + + private String dummyNodeInfoResponse(int... ports) { + JsonObject body = new JsonObject(); + body.addProperty( "cluster_name", "foo-cluster.local" ); + + JsonObject nodes = new JsonObject(); + body.add( "nodes", nodes ); + int index = 1; + for ( int port : ports ) { + nodes.add( "hJLXmY_NTrCytiIMbX4_" + index + "g", dummyNodeInfo( port ) ); + ++index; + } + + return body.toString(); + } + + private JsonObject dummyNodeInfo(int port) { + JsonObject node = new JsonObject(); + node.addProperty( "name", "nodeForPort" + port ); + node.addProperty( "version", ElasticsearchTestDialect.getActualVersion().versionString() ); + + JsonObject http = new JsonObject(); + node.add( "http", http ); + http.addProperty( "publish_address", "127.0.0.1:" + port ); + JsonArray boundAddresses = new JsonArray(); + http.add( "bound_address", boundAddresses ); + boundAddresses.add( "[::]:" + port ); + boundAddresses.add( "127.0.0.1:" + port ); + + JsonArray roles = new JsonArray(); + node.add( "roles", roles ); + roles.add( "ingest" ); + roles.add( "master" ); + roles.add( "data" ); + roles.add( "ml" ); + + node.add( "plugins", new JsonObject() ); + + return node; + } + + private static final ConfigurationProperty MAX_TOTAL_CONNECTION = + ConfigurationProperty.forKey( ClientJavaElasticsearchBackendClientSettings.MAX_CONNECTIONS ) + .asIntegerStrictlyPositive() + .withDefault( ClientJavaElasticsearchBackendClientSettings.Defaults.MAX_CONNECTIONS ) + .build(); + + private static final ConfigurationProperty MAX_TOTAL_CONNECTION_PER_ROUTE = + ConfigurationProperty.forKey( ClientJavaElasticsearchBackendClientSettings.MAX_CONNECTIONS_PER_ROUTE ) + .asIntegerStrictlyPositive() + .withDefault( ClientJavaElasticsearchBackendClientSettings.Defaults.MAX_CONNECTIONS_PER_ROUTE ) + .build(); + + private static final ConfigurationProperty READ_TIMEOUT = + ConfigurationProperty.forKey( ClientJavaElasticsearchBackendClientSettings.READ_TIMEOUT ) + .asIntegerPositiveOrZeroOrNegative() + .withDefault( ClientJavaElasticsearchBackendClientSettings.Defaults.READ_TIMEOUT ) + .build(); + + private static final ConfigurationProperty CONNECTION_TIMEOUT = + ConfigurationProperty.forKey( ClientJavaElasticsearchBackendClientSettings.CONNECTION_TIMEOUT ) + .asIntegerPositiveOrZeroOrNegative() + .withDefault( ClientJavaElasticsearchBackendClientSettings.Defaults.CONNECTION_TIMEOUT ) + .build(); + + private static BeanConfigurer elasticsearchSslBeanConfigurer(ConfigurationPropertySource propertySource) { + return context -> { + context.define( + ElasticsearchHttpClientConfigurer.class, + BeanReference.ofInstance( new ElasticsearchHttpClientConfigurer() { + @Override + public void configure(ElasticsearchHttpClientConfigurationContext context) { + context.clientBuilder() + .setConnectionManager( + PoolingAsyncClientConnectionManagerBuilder.create() + .setMaxConnTotal( MAX_TOTAL_CONNECTION.get( propertySource ) ) + .setMaxConnPerRoute( MAX_TOTAL_CONNECTION_PER_ROUTE.get( propertySource ) ) + .setDefaultConnectionConfig( + ConnectionConfig.copy( ConnectionConfig.DEFAULT ) + .setConnectTimeout( + CONNECTION_TIMEOUT.get( propertySource ), + TimeUnit.MILLISECONDS ) + .setSocketTimeout( READ_TIMEOUT.get( propertySource ), + TimeUnit.MILLISECONDS ) + .build() + ) + .setTlsStrategy( + ClientTlsStrategyBuilder.create() + .setSslContext( buildAllowAnythingSSLContext() ) + .setHostnameVerifier( NoopHostnameVerifier.INSTANCE ) + .buildAsync() + ) + .build() + ); + } + } ) + ); + }; + } + + private static SSLContext buildAllowAnythingSSLContext() { + try { + return SSLContexts.custom().loadTrustMaterial( null, new TrustAllStrategy() ).build(); + } + catch (Exception e) { + throw new AssertionFailure( "Unexpected exception", e ); + } + } +} diff --git a/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/client/ClientOpenSearchElasticsearchClientFactoryIT.java b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/client/ClientOpenSearchElasticsearchClientFactoryIT.java new file mode 100644 index 00000000000..c54d9a97fdb --- /dev/null +++ b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/client/ClientOpenSearchElasticsearchClientFactoryIT.java @@ -0,0 +1,1379 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.search.integrationtest.backend.elasticsearch.client; + +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Fail.fail; +import static org.awaitility.Awaitility.await; +import static org.hibernate.search.util.impl.test.JsonHelper.assertJsonEquals; +import static org.junit.jupiter.api.Assumptions.assumeFalse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletionException; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import javax.net.ssl.SSLContext; + +import org.hibernate.search.backend.elasticsearch.client.common.cfg.ElasticsearchBackendClientCommonSettings; +import org.hibernate.search.backend.elasticsearch.client.common.gson.spi.GsonProvider; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchClient; +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.URLEncodedString; +import org.hibernate.search.backend.elasticsearch.client.opensearch.ElasticsearchHttpClientConfigurationContext; +import org.hibernate.search.backend.elasticsearch.client.opensearch.ElasticsearchHttpClientConfigurer; +import org.hibernate.search.backend.elasticsearch.client.opensearch.cfg.ClientOpenSearchElasticsearchBackendClientSettings; +import org.hibernate.search.backend.elasticsearch.client.opensearch.cfg.spi.ClientOpenSearchElasticsearchBackendClientSpiSettings; +import org.hibernate.search.backend.elasticsearch.client.opensearch.impl.ClientOpenSearchElasticsearchClientFactory; +import org.hibernate.search.engine.cfg.ConfigurationPropertySource; +import org.hibernate.search.engine.cfg.spi.AllAwareConfigurationPropertySource; +import org.hibernate.search.engine.cfg.spi.ConfigurationProperty; +import org.hibernate.search.engine.cfg.spi.EngineSpiSettings; +import org.hibernate.search.engine.common.execution.spi.DelegatingSimpleScheduledExecutor; +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.bean.spi.BeanConfigurer; +import org.hibernate.search.engine.environment.thread.impl.EmbeddedThreadProvider; +import org.hibernate.search.engine.environment.thread.impl.ThreadPoolProviderImpl; +import org.hibernate.search.integrationtest.backend.elasticsearch.testsupport.util.ElasticsearchTckBackendHelper; +import org.hibernate.search.util.common.AssertionFailure; +import org.hibernate.search.util.common.SearchException; +import org.hibernate.search.util.impl.integrationtest.backend.elasticsearch.ElasticsearchTestHostConnectionConfiguration; +import org.hibernate.search.util.impl.integrationtest.backend.elasticsearch.dialect.ElasticsearchTestDialect; +import org.hibernate.search.util.impl.integrationtest.common.TestConfigurationProvider; +import org.hibernate.search.util.impl.test.annotation.PortedFromSearch5; +import org.hibernate.search.util.impl.test.annotation.TestForIssue; +import org.hibernate.search.util.impl.test.extension.ExpectedLog4jLog; +import org.hibernate.search.util.impl.test.extension.RetryExtension; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.extension.RegisterExtension; + +import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder; +import com.github.tomakehurst.wiremock.extension.Parameters; +import com.github.tomakehurst.wiremock.http.Fault; +import com.github.tomakehurst.wiremock.http.Request; +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; +import com.github.tomakehurst.wiremock.matching.MatchResult; +import com.github.tomakehurst.wiremock.matching.RequestMatcherExtension; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; + +import org.apache.hc.client5.http.HttpRoute; +import org.apache.hc.client5.http.config.ConnectionConfig; +import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager; +import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder; +import org.apache.hc.client5.http.nio.AsyncClientConnectionManager; +import org.apache.hc.client5.http.nio.AsyncConnectionEndpoint; +import org.apache.hc.client5.http.ssl.ClientTlsStrategyBuilder; +import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; +import org.apache.hc.client5.http.ssl.TrustAllStrategy; +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpResponseInterceptor; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.io.CloseMode; +import org.apache.hc.core5.pool.PoolEntry; +import org.apache.hc.core5.reactor.ConnectionInitiator; +import org.apache.hc.core5.ssl.SSLContexts; +import org.apache.hc.core5.util.TimeValue; +import org.apache.hc.core5.util.Timeout; +import org.apache.logging.log4j.Level; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.opensearch.client.RestClient; + +@MockitoSettings(strictness = Strictness.STRICT_STUBS) +@PortedFromSearch5(original = "org.hibernate.search.elasticsearch.test.DefaultElasticsearchClientFactoryTest") +class ClientOpenSearchElasticsearchClientFactoryIT { + + // Some tests in here are flaky, for some reason once in a while wiremock takes a very long time to answer + // even though no delay was configured. + // The exact reason is unknown though, so just try multiple times... + + @RegisterExtension + private final ExpectedLog4jLog logged = ExpectedLog4jLog.create(); + + @RegisterExtension + private final WireMockExtension wireMockRule1 = WireMockExtension.newInstance() + .options( wireMockConfig().dynamicPort().dynamicHttpsPort() ) + .build(); + + @RegisterExtension + private final WireMockExtension wireMockRule2 = WireMockExtension.newInstance() + .options( wireMockConfig().dynamicPort().dynamicHttpsPort() ) + .build(); + + @RegisterExtension + private final TestConfigurationProvider testConfigurationProvider = new TestConfigurationProvider(); + + private final ThreadPoolProviderImpl threadPoolProvider = new ThreadPoolProviderImpl( + BeanHolder.of( new EmbeddedThreadProvider( + ClientOpenSearchElasticsearchClientFactoryIT.class.getName() + ": " ) ) ); + + private final ScheduledExecutorService timeoutExecutorService = + threadPoolProvider.newScheduledExecutor( 1, "Timeout - " ); + + @AfterEach + void cleanup() { + timeoutExecutorService.shutdownNow(); + threadPoolProvider.close(); + + // Avoid side-effects from one test to another + // Ideally WiremockRule should do that by itself, but it doesn't... + wireMockRule1.resetAll(); + wireMockRule2.resetAll(); + } + + @RetryExtension.TestWithRetry + @TestForIssue(jiraKey = "HSEARCH-2274") + void simple_http() { + String payload = "{ \"foo\": \"bar\" }"; + String statusMessage = "OK"; + String responseBody = "{ \"foo\": \"bar\" }"; + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .andMatching( httpProtocol() ) + .willReturn( elasticsearchResponse().withStatus( 200 ) + .withStatusMessage( statusMessage ) + .withBody( responseBody ) ) ); + + try ( ElasticsearchClientImplementor client = createClient() ) { + ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + assertThat( result.statusMessage() ).as( "status message" ).isEqualTo( statusMessage ); + assertJsonEquals( responseBody, result.body().toString() ); + + wireMockRule1.verify( + postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) + .andMatching( httpProtocol() ) + ); + } + } + + @RetryExtension.TestWithRetry + void simple_httpClientConfigurer() throws Exception { + String payload = "{ \"foo\": \"bar\" }"; + String statusMessage = "OK"; + String responseBody = "{ \"foo\": \"bar\" }"; + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .andMatching( httpProtocol() ) + .willReturn( elasticsearchResponse().withStatus( 200 ) + .withStatusMessage( statusMessage ) + .withBody( responseBody ) ) ); + + HttpResponseInterceptor responseInterceptor = spy( HttpResponseInterceptor.class ); + + try ( ElasticsearchClientImplementor client = createClient( properties -> properties.accept( + ClientOpenSearchElasticsearchBackendClientSettings.CLIENT_CONFIGURER, + (ElasticsearchHttpClientConfigurer) context -> context + .clientBuilder() + .addResponseInterceptorFirst( responseInterceptor ) + ) ) ) { + ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + assertThat( result.statusMessage() ).as( "status message" ).isEqualTo( statusMessage ); + assertJsonEquals( responseBody, result.body().toString() ); + + wireMockRule1.verify( + postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) + .andMatching( httpProtocol() ) + ); + } + + verify( responseInterceptor, times( 1 ) ).process( any(), any(), any() ); + } + + @RetryExtension.TestWithRetry + @TestForIssue(jiraKey = "HSEARCH-4099") + void uris_http() { + String payload = "{ \"foo\": \"bar\" }"; + String statusMessage = "OK"; + String responseBody = "{ \"foo\": \"bar\" }"; + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .andMatching( httpProtocol() ) + .willReturn( elasticsearchResponse().withStatus( 200 ) + .withStatusMessage( statusMessage ) + .withBody( responseBody ) ) ); + + try ( ElasticsearchClientImplementor client = createClient( + properties -> { + properties.accept( ElasticsearchBackendClientCommonSettings.URIS, httpUrisFor( wireMockRule1 ) ); + } + ) ) { + ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + assertThat( result.statusMessage() ).as( "status message" ).isEqualTo( statusMessage ); + assertJsonEquals( responseBody, result.body().toString() ); + + wireMockRule1.verify( + postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) + .andMatching( httpProtocol() ) + ); + } + } + + @RetryExtension.TestWithRetry + @TestForIssue(jiraKey = "HSEARCH-4051") + void pathPrefix_http() { + String payload = "{ \"foo\": \"bar\" }"; + String statusMessage = "OK"; + String responseBody = "{ \"foo\": \"bar\" }"; + wireMockRule1.stubFor( post( urlPathMatching( "/bla/bla/bla/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .andMatching( httpProtocol() ) + .willReturn( elasticsearchResponse().withStatus( 200 ) + .withStatusMessage( statusMessage ) + .withBody( responseBody ) ) ); + + try ( ElasticsearchClientImplementor client = createClient( + properties -> { + properties.accept( ElasticsearchBackendClientCommonSettings.PATH_PREFIX, "bla/bla/bla" ); + } + ) ) { + ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + assertThat( result.statusMessage() ).as( "status message" ).isEqualTo( statusMessage ); + assertJsonEquals( responseBody, result.body().toString() ); + + wireMockRule1.verify( + postRequestedFor( urlPathMatching( "/bla/bla/bla/myIndex/myType" ) ) + .andMatching( httpProtocol() ) + ); + } + } + + @RetryExtension.TestWithRetry + @TestForIssue(jiraKey = "HSEARCH-4099") + void pathPrefix_uris() { + String payload = "{ \"foo\": \"bar\" }"; + String statusMessage = "OK"; + String responseBody = "{ \"foo\": \"bar\" }"; + wireMockRule1.stubFor( post( urlPathMatching( "/bla/bla/bla/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .andMatching( httpProtocol() ) + .willReturn( elasticsearchResponse().withStatus( 200 ) + .withStatusMessage( statusMessage ) + .withBody( responseBody ) ) ); + + try ( ElasticsearchClientImplementor client = createClient( + properties -> { + properties.accept( ElasticsearchBackendClientCommonSettings.PATH_PREFIX, "bla/bla/bla" ); + properties.accept( ElasticsearchBackendClientCommonSettings.URIS, httpUrisFor( wireMockRule1 ) ); + } + ) ) { + ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + assertThat( result.statusMessage() ).as( "status message" ).isEqualTo( statusMessage ); + assertJsonEquals( responseBody, result.body().toString() ); + + wireMockRule1.verify( + postRequestedFor( urlPathMatching( "/bla/bla/bla/myIndex/myType" ) ) + .andMatching( httpProtocol() ) + ); + } + } + + @RetryExtension.TestWithRetry + @TestForIssue(jiraKey = "HSEARCH-2274") + void simple_https() { + String payload = "{ \"foo\": \"bar\" }"; + String statusMessage = "OK"; + String responseBody = "{ \"foo\": \"bar\" }"; + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .andMatching( httpsProtocol() ) + .willReturn( elasticsearchResponse().withStatus( 200 ) + .withStatusMessage( statusMessage ) + .withBody( responseBody ) ) ); + + try ( ElasticsearchClientImplementor client = createClient( + properties -> { + properties.accept( ElasticsearchBackendClientCommonSettings.HOSTS, httpsHostAndPortFor( wireMockRule1 ) ); + properties.accept( ElasticsearchBackendClientCommonSettings.PROTOCOL, "https" ); + } + ) ) { + ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + assertThat( result.statusMessage() ).as( "status message" ).isEqualTo( statusMessage ); + assertJsonEquals( responseBody, result.body().toString() ); + + wireMockRule1.verify( + postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) + .andMatching( httpsProtocol() ) + ); + } + } + + @RetryExtension.TestWithRetry + @TestForIssue(jiraKey = "HSEARCH-2274") + void uris_https() { + String payload = "{ \"foo\": \"bar\" }"; + String statusMessage = "OK"; + String responseBody = "{ \"foo\": \"bar\" }"; + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .andMatching( httpsProtocol() ) + .willReturn( elasticsearchResponse().withStatus( 200 ) + .withStatusMessage( statusMessage ) + .withBody( responseBody ) ) ); + + try ( ElasticsearchClientImplementor client = createClient( + properties -> { + properties.accept( ElasticsearchBackendClientCommonSettings.URIS, httpsUrisFor( wireMockRule1 ) ); + } + ) ) { + ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + assertThat( result.statusMessage() ).as( "status message" ).isEqualTo( statusMessage ); + assertJsonEquals( responseBody, result.body().toString() ); + + wireMockRule1.verify( + postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) + .andMatching( httpsProtocol() ) + ); + } + } + + @RetryExtension.TestWithRetry + void error() { + String payload = "{ \"foo\": \"bar\" }"; + String responseBody = "{ \"error\": \"ErrorMessageExplainingTheError\" }"; + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .willReturn( + elasticsearchResponse().withStatus( 500 ) + .withBody( responseBody ) + ) ); + + try ( ElasticsearchClientImplementor client = createClient() ) { + ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 500 ); + assertJsonEquals( responseBody, result.body().toString() ); + } + } + + @RetryExtension.TestWithRetry + void unparseable() { + String payload = "{ \"foo\": \"bar\" }"; + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .willReturn( + elasticsearchResponse() + .withBody( "'unparseable" ) + ) ); + + assertThatThrownBy( () -> { + try ( ElasticsearchClientImplementor client = createClient() ) { + doPost( client, "/myIndex/myType", payload ); + } + } ) + .isInstanceOf( AssertionFailure.class ) + .extracting( Throwable::getCause, InstanceOfAssertFactories.THROWABLE ) + .isInstanceOf( CompletionException.class ) + .extracting( Throwable::getCause, InstanceOfAssertFactories.THROWABLE ) + .isInstanceOf( SearchException.class ) + .hasMessageContaining( "HSEARCH400089" ) + .extracting( Throwable::getCause, InstanceOfAssertFactories.THROWABLE ) + .isInstanceOf( JsonSyntaxException.class ); + } + + @RetryExtension.TestWithRetry + void timeout_read() { + String payload = "{ \"foo\": \"bar\" }"; + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .willReturn( + elasticsearchResponse() + .withFixedDelay( 2000 ) + ) ); + + assertThatThrownBy( () -> { + try ( ElasticsearchClientImplementor client = createClient( + properties -> { + properties.accept( ClientOpenSearchElasticsearchBackendClientSettings.READ_TIMEOUT, "1000" ); + properties.accept( ClientOpenSearchElasticsearchBackendClientSettings.REQUEST_TIMEOUT, "99999" ); + } + ) ) { + doPost( client, "/myIndex/myType", payload ); + } + } ) + .isInstanceOf( AssertionFailure.class ) + .extracting( Throwable::getCause, InstanceOfAssertFactories.THROWABLE ) + .isInstanceOf( CompletionException.class ) + .extracting( Throwable::getCause, InstanceOfAssertFactories.THROWABLE ) + .isInstanceOf( IOException.class ); + } + + @RetryExtension.TestWithRetry + void timeout_request() { + String payload = "{ \"foo\": \"bar\" }"; + + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .willReturn( + elasticsearchResponse() + .withFixedDelay( 2000 ) + ) ); + + assertThatThrownBy( () -> { + try ( ElasticsearchClientImplementor client = createClient( + properties -> { + properties.accept( ClientOpenSearchElasticsearchBackendClientSettings.READ_TIMEOUT, "99999" ); + properties.accept( ClientOpenSearchElasticsearchBackendClientSettings.REQUEST_TIMEOUT, "1000" ); + } + ) ) { + doPost( client, "/myIndex/myType", payload ); + } + } ) + .isInstanceOf( AssertionFailure.class ) + .extracting( Throwable::getCause, InstanceOfAssertFactories.THROWABLE ) + .isInstanceOf( CompletionException.class ) + .extracting( Throwable::getCause, InstanceOfAssertFactories.THROWABLE ) + .isInstanceOf( SearchException.class ) + .hasMessageContainingAll( "Request execution exceeded the timeout of 1s, 0ms and 0ns", + "Request was POST /myIndex/myType with parameters {}" ); + } + + /** + * Verify that by default, even when the client is clogged (many pending requests), + * we don't trigger timeouts just because requests spend a long time waiting; + * timeouts are only related to how long the *server* takes to answer. + */ + @RetryExtension.TestWithRetry + @TestForIssue(jiraKey = "HSEARCH-2836") + void cloggedClient_noTimeout_read() { + String payload = "{ \"foo\": \"bar\" }"; + wireMockRule1.stubFor( post( urlPathMatching( "/long" ) ) + .willReturn( elasticsearchResponse() + .withFixedDelay( 300 /* 300ms => should not time out, but will still clog up the client */ ) ) ); + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .willReturn( elasticsearchResponse().withStatus( 200 ) + .withFixedDelay( 100 /* 100ms => should not time out */ ) ) ); + + try ( ElasticsearchClientImplementor client = createClient( + properties -> { + properties.accept( ElasticsearchBackendClientCommonSettings.HOSTS, httpHostAndPortFor( wireMockRule1 ) ); + properties.accept( ClientOpenSearchElasticsearchBackendClientSettings.MAX_CONNECTIONS_PER_ROUTE, "1" ); + properties.accept( ClientOpenSearchElasticsearchBackendClientSettings.READ_TIMEOUT, "1000" /* 1s */ ); + } + ) ) { + // Clog up the client: put many requests in the queue, to be executed asynchronously, + // so that we're sure the next request will have to wait in the queue + // for more that the configured timeout before it ends up being executed. + for ( int i = 0; i < 10; ++i ) { + client.submit( buildRequest( ElasticsearchRequest.post(), "/long", payload ) ); + } + + ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + + wireMockRule1.verify( 1, postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) ); + } + } + + /** + * Verify that when a request timeout is set, and when the client is clogged (many pending requests), + * we do trigger timeouts just because requests spend a long time waiting. + */ + @RetryExtension.TestWithRetry + @TestForIssue(jiraKey = "HSEARCH-2836") + void cloggedClient_timeout_request() { + String payload = "{ \"foo\": \"bar\" }"; + wireMockRule1.stubFor( post( urlPathMatching( "/long" ) ) + .willReturn( elasticsearchResponse() + .withFixedDelay( 300 /* 300ms => should not time out, but will still clog up the client */ ) ) ); + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .willReturn( elasticsearchResponse().withStatus( 200 ) + .withFixedDelay( 100 /* 100ms => should not time out */ ) ) ); + + try ( ElasticsearchClientImplementor client = createClient( + properties -> { + properties.accept( ElasticsearchBackendClientCommonSettings.HOSTS, httpHostAndPortFor( wireMockRule1 ) ); + properties.accept( ClientOpenSearchElasticsearchBackendClientSettings.MAX_CONNECTIONS_PER_ROUTE, "1" ); + properties.accept( ClientOpenSearchElasticsearchBackendClientSettings.REQUEST_TIMEOUT, "1000" /* 1s */ ); + } + ) ) { + // Clog up the client: put many requests in the queue, to be executed asynchronously, + // so that we're sure the next request will have to wait in the queue + // for more that the configured timeout before it ends up being executed. + for ( int i = 0; i < 10; ++i ) { + client.submit( buildRequest( ElasticsearchRequest.post(), "/long", payload ) ); + } + + assertThatThrownBy( () -> { + doPost( client, "/myIndex/myType", payload ); + } ) + .isInstanceOf( AssertionFailure.class ) + .extracting( Throwable::getCause, InstanceOfAssertFactories.THROWABLE ) + .isInstanceOf( CompletionException.class ) + .extracting( Throwable::getCause, InstanceOfAssertFactories.THROWABLE ) + .isInstanceOf( SearchException.class ) + .hasMessageContainingAll( "Request execution exceeded the timeout of 1s, 0ms and 0ns", + "Request was POST /myIndex/myType with parameters {}" ); + } + } + + @RetryExtension.TestWithRetry + @TestForIssue(jiraKey = "HSEARCH-2235") + void multipleHosts() { + String payload = "{ \"foo\": \"bar\" }"; + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .willReturn( elasticsearchResponse().withStatus( 200 ) ) ); + wireMockRule2.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .willReturn( elasticsearchResponse().withStatus( 200 ) ) ); + + try ( ElasticsearchClientImplementor client = createClient( + properties -> { + properties.accept( ElasticsearchBackendClientCommonSettings.HOSTS, + httpHostAndPortFor( wireMockRule1, wireMockRule2 ) ); + } + ) ) { + ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + + wireMockRule1.verify( postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) ); + wireMockRule2.verify( postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) ); + } + } + + @RetryExtension.TestWithRetry + void multipleURIs() { + String payload = "{ \"foo\": \"bar\" }"; + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .willReturn( elasticsearchResponse().withStatus( 200 ) ) ); + wireMockRule2.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .willReturn( elasticsearchResponse().withStatus( 200 ) ) ); + + try ( ElasticsearchClientImplementor client = createClient( + properties -> { + properties.accept( ElasticsearchBackendClientCommonSettings.URIS, + httpsUrisFor( wireMockRule1, wireMockRule2 ) ); + } + ) ) { + ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + + wireMockRule1.verify( postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) ); + wireMockRule2.verify( postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) ); + } + } + + @RetryExtension.TestWithRetry + @TestForIssue(jiraKey = "HSEARCH-2469") + void multipleHosts_failover_serverError() { + String payload = "{ \"foo\": \"bar\" }"; + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .willReturn( elasticsearchResponse().withStatus( 200 ) ) ); + wireMockRule2.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .willReturn( elasticsearchResponse().withStatus( 503 ) ) ); + + try ( ElasticsearchClientImplementor client = createClient( + properties -> { + properties.accept( ElasticsearchBackendClientCommonSettings.HOSTS, + httpHostAndPortFor( wireMockRule1, wireMockRule2 ) ); + } + ) ) { + ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + + wireMockRule1.verify( 2, postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) ); + wireMockRule2.verify( 1, postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) ); + + wireMockRule1.resetRequests(); + wireMockRule2.resetRequests(); + + result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + + // Must not use the failing node anymore + wireMockRule1.verify( 2, postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) ); + wireMockRule2.verify( 0, postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) ); + } + } + + @RetryExtension.TestWithRetry + @TestForIssue(jiraKey = "HSEARCH-2469") + void multipleHosts_failover_timeout() { + String payload = "{ \"foo\": \"bar\" }"; + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .willReturn( elasticsearchResponse().withStatus( 200 ) ) ); + wireMockRule2.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .willReturn( elasticsearchResponse().withStatus( 200 ).withFixedDelay( 5_000 /* 5s => will time out */ ) ) ); + + try ( ElasticsearchClientImplementor client = createClient( + properties -> { + properties.accept( ElasticsearchBackendClientCommonSettings.HOSTS, + httpHostAndPortFor( wireMockRule1, wireMockRule2 ) ); + // Use a timeout much higher than 1s, because wiremock can be really slow... + properties.accept( ClientOpenSearchElasticsearchBackendClientSettings.READ_TIMEOUT, "1000" /* 1s */ ); + } + ) ) { + ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + + wireMockRule1.verify( 2, postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) ); + wireMockRule2.verify( 1, postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) ); + + wireMockRule1.resetRequests(); + wireMockRule2.resetRequests(); + + /* + * Remove the failure in the previously failing node, + * so that we can detect if requests are sent to this node. + */ + wireMockRule2.resetMappings(); + wireMockRule2.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .willReturn( elasticsearchResponse().withStatus( 200 ) ) ); + + result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + + // Must not use the failing node anymore + wireMockRule1.verify( 2, postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) ); + wireMockRule2.verify( 0, postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) ); + } + } + + @RetryExtension.TestWithRetry + @TestForIssue(jiraKey = "HSEARCH-2469") + void multipleHosts_failover_fault() { + String payload = "{ \"foo\": \"bar\" }"; + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .willReturn( elasticsearchResponse().withStatus( 200 ) ) ); + wireMockRule2.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .willReturn( elasticsearchResponse().withStatus( 200 ).withFault( Fault.MALFORMED_RESPONSE_CHUNK ) ) ); + + try ( ElasticsearchClientImplementor client = createClient( + properties -> { + properties.accept( ElasticsearchBackendClientCommonSettings.HOSTS, + httpHostAndPortFor( wireMockRule1, wireMockRule2 ) ); + } + ) ) { + ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + + wireMockRule1.verify( 2, postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) ); + wireMockRule2.verify( 1, postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) ); + + wireMockRule1.resetRequests(); + wireMockRule2.resetRequests(); + + result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + + // Must not use the failing node anymore + wireMockRule1.verify( 2, postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) ); + wireMockRule2.verify( 0, postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) ); + } + } + + @RetryExtension.TestWithRetry + @TestForIssue(jiraKey = "HSEARCH-2449") + void discovery_http() { + String nodesInfoResult = dummyNodeInfoResponse( wireMockRule1.getPort(), wireMockRule2.getPort() ); + + wireMockRule1.stubFor( get( urlPathMatching( "/_nodes.*" ) ) + .andMatching( httpProtocol() ) + .willReturn( elasticsearchResponse().withStatus( 200 ).withBody( nodesInfoResult ) ) ); + wireMockRule2.stubFor( get( urlPathMatching( "/_nodes.*" ) ) + .andMatching( httpProtocol() ) + .willReturn( elasticsearchResponse().withStatus( 200 ).withBody( nodesInfoResult ) ) ); + + String payload = "{ \"foo\": \"bar\" }"; + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .andMatching( httpProtocol() ) + .willReturn( elasticsearchResponse().withStatus( 200 ) ) ); + wireMockRule2.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .andMatching( httpProtocol() ) + .willReturn( elasticsearchResponse().withStatus( 200 ) ) ); + + try ( ElasticsearchClientImplementor client = createClient( + properties -> { + properties.accept( ClientOpenSearchElasticsearchBackendClientSettings.DISCOVERY_ENABLED, "true" ); + properties.accept( ClientOpenSearchElasticsearchBackendClientSettings.DISCOVERY_REFRESH_INTERVAL, "1" ); + } + ) ) { + ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + + /* + * Send requests repeatedly until both hosts have been targeted. + * This should happen pretty early (as soon as we sent two requests, actually), + * but there is always the risk that the sniffer would send a request + * between our own requests, effectively making our own requests target the same host + * (since the hosts are each targeted in turn). + */ + await().untilAsserted( () -> { + ElasticsearchResponse newResult = doPost( client, "/myIndex/myType", payload ); + assertThat( newResult.statusCode() ).as( "status code" ).isEqualTo( 200 ); + + wireMockRule2.verify( + postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) + .andMatching( httpProtocol() ) + ); + wireMockRule2.verify( + postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) + .andMatching( httpProtocol() ) + ); + } ); + } + } + + @RetryExtension.TestWithRetry + @TestForIssue(jiraKey = "HSEARCH-2736") + void discovery_https() { + String nodesInfoResult = dummyNodeInfoResponse( wireMockRule1.getHttpsPort(), wireMockRule2.getHttpsPort() ); + + wireMockRule1.stubFor( get( urlPathMatching( "/_nodes.*" ) ) + .andMatching( httpsProtocol() ) + .willReturn( elasticsearchResponse().withStatus( 200 ).withBody( nodesInfoResult ) ) ); + wireMockRule2.stubFor( get( urlPathMatching( "/_nodes.*" ) ) + .andMatching( httpsProtocol() ) + .willReturn( elasticsearchResponse().withStatus( 200 ).withBody( nodesInfoResult ) ) ); + + String payload = "{ \"foo\": \"bar\" }"; + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .andMatching( httpsProtocol() ) + .willReturn( elasticsearchResponse().withStatus( 200 ) ) ); + wireMockRule2.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .andMatching( httpsProtocol() ) + .willReturn( elasticsearchResponse().withStatus( 200 ) ) ); + + try ( ElasticsearchClientImplementor client = createClient( + properties -> { + properties.accept( ElasticsearchBackendClientCommonSettings.HOSTS, httpsHostAndPortFor( wireMockRule1 ) ); + properties.accept( ElasticsearchBackendClientCommonSettings.PROTOCOL, "https" ); + properties.accept( ClientOpenSearchElasticsearchBackendClientSettings.DISCOVERY_ENABLED, "true" ); + properties.accept( ClientOpenSearchElasticsearchBackendClientSettings.DISCOVERY_REFRESH_INTERVAL, "1" ); + } + ) ) { + /* + * Send requests repeatedly until both hosts have been targeted. + * This should happen pretty early (as soon as we sent two requests, actually), + * but there is always the risk that the sniffer would send a request + * between our own requests, effectively making our own requests target the same host + * (since the hosts are each targeted in turn). + */ + await().untilAsserted( () -> { + ElasticsearchResponse newResult = doPost( client, "/myIndex/myType", payload ); + assertThat( newResult.statusCode() ).as( "status code" ).isEqualTo( 200 ); + + wireMockRule2.verify( + postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) + .andMatching( httpsProtocol() ) + ); + wireMockRule2.verify( + postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) + .andMatching( httpsProtocol() ) + ); + } ); + } + } + + private static RequestMatcherExtension httpProtocol() { + return protocol( "http" ); + } + + private static RequestMatcherExtension httpsProtocol() { + return protocol( "https" ); + } + + private static RequestMatcherExtension protocol(String protocol) { + return new RequestMatcherExtension() { + @Override + public MatchResult match(Request request, Parameters parameters) { + return MatchResult.of( protocol.equals( request.getScheme() ) ); + } + + @Override + public String getName() { + return "expected protocol: " + protocol; + } + }; + } + + @RetryExtension.TestWithRetry + @TestForIssue(jiraKey = "HSEARCH-2453") + void authentication() { + assumeFalse( + ElasticsearchTestHostConnectionConfiguration.get().isAws(), + "This test only is only relevant if Elasticsearch request are *NOT* automatically" + + " augmented with an \"Authentication:\" header." + + " \"Authentication:\" headers are added by the AWS integration in particular." + ); + String username = "ironman"; + String password = "j@rV1s"; + + String payload = "{ \"foo\": \"bar\" }"; + + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType/_search" ) ) + .withBasicAuth( username, password ) + .withRequestBody( equalToJson( payload ) ) + .willReturn( elasticsearchResponse().withStatus( 200 ) ) ); + + try ( ElasticsearchClientImplementor client = createClient( + properties -> { + properties.accept( ElasticsearchBackendClientCommonSettings.USERNAME, username ); + properties.accept( ElasticsearchBackendClientCommonSettings.PASSWORD, password ); + } + ) ) { + ElasticsearchResponse result = doPost( client, "/myIndex/myType/_search", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + } + } + + @RetryExtension.TestWithRetry + @TestForIssue(jiraKey = "HSEARCH-2453") + void authentication_error() { + String payload = "{ \"foo\": \"bar\" }"; + String statusMessage = "StatusMessageUnauthorized"; + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType/_search" ) ) + .withRequestBody( equalToJson( payload ) ) + .willReturn( + elasticsearchResponse().withStatus( 401 /* Unauthorized */ ) + .withStatusMessage( statusMessage ) + ) ); + + try ( ElasticsearchClientImplementor client = createClient() ) { + ElasticsearchResponse result = doPost( client, "/myIndex/myType/_search", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 401 ); + assertThat( result.statusMessage() ).as( "status message" ).isEqualTo( statusMessage ); + } + } + + @RetryExtension.TestWithRetry + @TestForIssue(jiraKey = "HSEARCH-2453") + void authentication_http_password() { + String username = "ironman"; + String password = "j@rV1s"; + + logged.expectEvent( Level.WARN, "The password will be sent in clear text over the network" ); + + try ( ElasticsearchClientImplementor client = createClient( + properties -> { + properties.accept( ElasticsearchBackendClientCommonSettings.USERNAME, username ); + properties.accept( ElasticsearchBackendClientCommonSettings.PASSWORD, password ); + } + ) ) { + // Nothing to do here + } + } + + @RetryExtension.TestWithRetry + void uriAndProtocol() { + Consumer> additionalProperties = properties -> { + properties.accept( ElasticsearchBackendClientCommonSettings.URIS, "http://is-not-called:12345" ); + properties.accept( ElasticsearchBackendClientCommonSettings.PROTOCOL, "http" ); + }; + + assertThatThrownBy( () -> createClient( additionalProperties ) ) + .isInstanceOf( SearchException.class ) + .hasMessageContainingAll( + "Invalid target hosts configuration", + "both the 'uris' property and the 'protocol' property are set", + "Uris: '[http://is-not-called:12345]'", "Protocol: 'http'", + "Either set the protocol and hosts simultaneously using the 'uris' property", + "or set them separately using the 'protocol' property and the 'hosts' property" + ); + } + + @RetryExtension.TestWithRetry + void uriAndHosts() { + Consumer> additionalProperties = properties -> { + properties.accept( ElasticsearchBackendClientCommonSettings.URIS, "http://is-not-called:12345" ); + properties.accept( ElasticsearchBackendClientCommonSettings.HOSTS, "not-called-either:234" ); + }; + + assertThatThrownBy( () -> createClient( additionalProperties ) ) + .isInstanceOf( SearchException.class ) + .hasMessageContainingAll( + "Invalid target hosts configuration", + "both the 'uris' property and the 'hosts' property are set", + "Uris: '[http://is-not-called:12345]'", "Hosts: '[", // host and port are dynamic + "Either set the protocol and hosts simultaneously using the 'uris' property", + "or set them separately using the 'protocol' property and the 'hosts' property" + ); + } + + @RetryExtension.TestWithRetry + void differentProtocolsOnUris() { + Consumer> additionalProperties = properties -> { + properties.accept( ElasticsearchBackendClientCommonSettings.URIS, + "http://is-not-called:12345, https://neather-is:12345" ); + }; + + assertThatThrownBy( () -> createClient( additionalProperties ) ) + .isInstanceOf( SearchException.class ) + .hasMessageContainingAll( + "Invalid target hosts configuration: the 'uris' use different protocols (http, https)", + "All URIs must use the same protocol", + "Uris: '[http://is-not-called:12345, https://neather-is:12345]'" + ); + } + + @RetryExtension.TestWithRetry + void emptyListOfUris() { + Consumer> additionalProperties = properties -> { + properties.accept( ElasticsearchBackendClientCommonSettings.URIS, Collections.emptyList() ); + }; + + assertThatThrownBy( () -> createClient( additionalProperties ) ) + .isInstanceOf( SearchException.class ) + .hasMessageContaining( + "Invalid target hosts configuration: the list of URIs must not be empty" + ); + } + + @RetryExtension.TestWithRetry + void emptyListOfHosts() { + Consumer> additionalProperties = properties -> { + properties.accept( ElasticsearchBackendClientCommonSettings.HOSTS, Collections.emptyList() ); + }; + + assertThatThrownBy( () -> createClient( additionalProperties ) ) + .isInstanceOf( SearchException.class ) + .hasMessageContaining( + "Invalid target hosts configuration: the list of hosts must not be empty" + ); + } + + @RetryExtension.TestWithRetry + void clientInstance() throws IOException, URISyntaxException { + try ( RestClient myRestClient = RestClient.builder( HttpHost.create( httpUrisFor( wireMockRule1 ) ) ).build() ) { + String payload = "{ \"foo\": \"bar\" }"; + String statusMessage = "OK"; + String responseBody = "{ \"foo\": \"bar\" }"; + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .andMatching( httpProtocol() ) + .willReturn( elasticsearchResponse().withStatus( 200 ) + .withStatusMessage( statusMessage ) + .withBody( responseBody ) ) ); + + try ( ElasticsearchClientImplementor client = createClient( properties -> { + properties.accept( ClientOpenSearchElasticsearchBackendClientSpiSettings.CLIENT_INSTANCE, + BeanReference.ofInstance( myRestClient ) ); + } ) ) { + ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + assertThat( result.statusMessage() ).as( "status message" ).isEqualTo( statusMessage ); + assertJsonEquals( responseBody, result.body().toString() ); + + wireMockRule1.verify( + postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) + .andMatching( httpProtocol() ) + ); + } + + // Hibernate Search must not close provided client instances + assertThat( myRestClient ).returns( true, RestClient::isRunning ); + } + } + + @RetryExtension.TestWithRetry + void maxKeepAliveNegativeValue() { + assertThatThrownBy( () -> createClient( + properties -> { + properties.accept( ClientOpenSearchElasticsearchBackendClientSettings.MAX_KEEP_ALIVE, -1 ); + } + ) ).isInstanceOf( SearchException.class ) + .hasMessageContainingAll( + "Invalid value for configuration property", + "must be positive or zero" + ); + } + + @RetryExtension.TestWithRetry + void maxKeepAliveConnectionIsNotClosed() throws InterruptedException { + maxKeepAliveConnection( 100000, 1 ); + } + + @RetryExtension.TestWithRetry + void maxKeepAliveConnectionIsClosed() throws InterruptedException { + maxKeepAliveConnection( 10, 2 ); + } + + void maxKeepAliveConnection(long time, int connections) throws InterruptedException { + String payload = "{ \"foo\": \"bar\" }"; + String statusMessage = "OK"; + String responseBody = "{ \"foo\": \"bar\" }"; + wireMockRule1.stubFor( post( urlPathMatching( "/myIndex/myType" ) ) + .withRequestBody( equalToJson( payload ) ) + .andMatching( httpProtocol() ) + .willReturn( elasticsearchResponse().withStatus( 200 ) + .withStatusMessage( statusMessage ) + .withBody( responseBody ) ) ); + + Set usedConnections = Collections.synchronizedSet( new HashSet<>() ); + try ( ElasticsearchClientImplementor client = createClient( properties -> { + properties.accept( ClientOpenSearchElasticsearchBackendClientSettings.MAX_KEEP_ALIVE, time ); + properties.accept( + ClientOpenSearchElasticsearchBackendClientSettings.CLIENT_CONFIGURER, + (ElasticsearchHttpClientConfigurer) context -> { + context.clientBuilder() + .setConnectionManager( createPoolManager( usedConnections, connections, connections ) ); + } + ); + } ) ) { + + ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + assertThat( result.statusMessage() ).as( "status message" ).isEqualTo( statusMessage ); + assertJsonEquals( responseBody, result.body().toString() ); + + wireMockRule1.verify( + postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) + .andMatching( httpProtocol() ) + ); + + TimeUnit.SECONDS.sleep( 2 ); + + result = doPost( client, "/myIndex/myType", payload ); + assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); + assertThat( result.statusMessage() ).as( "status message" ).isEqualTo( statusMessage ); + assertJsonEquals( responseBody, result.body().toString() ); + + wireMockRule1.verify( + postRequestedFor( urlPathMatching( "/myIndex/myType" ) ) + .andMatching( httpProtocol() ) + ); + + assertThat( usedConnections ).hasSize( connections ); + } + } + + private AsyncClientConnectionManager createPoolManager(Set connections, int maxConnTotal, int maxConnPerRoute) { + PoolingAsyncClientConnectionManager delegate = PoolingAsyncClientConnectionManagerBuilder.create() + .setMaxConnTotal( maxConnTotal ) + .setMaxConnPerRoute( maxConnPerRoute ) + .setTlsStrategy( + ClientTlsStrategyBuilder.create() + .setSslContext( buildAllowAnythingSSLContext() ) + .setHostnameVerifier( org.apache.http.conn.ssl.NoopHostnameVerifier.INSTANCE ) + .buildAsync() + ) + .build(); + + return new AsyncClientConnectionManager() { + private Field poolEntryRef; + + @Override + public void close() throws IOException { + delegate.close(); + } + + @Override + public void close(CloseMode closeMode) { + delegate.close( closeMode ); + } + + @Override + public Future lease(String id, HttpRoute route, Object state, Timeout requestTimeout, + FutureCallback callback) { + return delegate.lease( id, route, state, requestTimeout, callback ); + } + + @SuppressWarnings("unchecked") + @Override + public void release(AsyncConnectionEndpoint endpoint, Object state, TimeValue keepAlive) { + try { + if ( poolEntryRef == null ) { + poolEntryRef = endpoint.getClass().getDeclaredField( "poolEntryRef" ); + poolEntryRef.setAccessible( true ); + } + AtomicReference> ref = (AtomicReference>) poolEntryRef.get( endpoint ); + connections.add( ref.get().getConnection().toString() ); + + } + catch (NoSuchFieldException | IllegalAccessException e) { + fail( e.getMessage(), e ); + } + delegate.release( endpoint, state, keepAlive ); + } + + @Override + public Future connect(AsyncConnectionEndpoint endpoint, + ConnectionInitiator connectionInitiator, Timeout connectTimeout, Object attachment, HttpContext context, + FutureCallback callback) { + return delegate.connect( endpoint, connectionInitiator, connectTimeout, attachment, context, callback ); + } + + @Override + public void upgrade(AsyncConnectionEndpoint endpoint, Object attachment, HttpContext context) { + delegate.upgrade( endpoint, attachment, context ); + } + }; + } + + private ElasticsearchClientImplementor createClient() { + return createClient( ignored -> {} ); + } + + private ElasticsearchClientImplementor createClient(Consumer> additionalProperties) { + Map defaultBackendProperties = + new ElasticsearchTckBackendHelper().createDefaultBackendSetupStrategy() + .createBackendConfigurationProperties( testConfigurationProvider ); + + Map clientProperties = new HashMap<>( defaultBackendProperties ); + + // We won't target the provided ES instance, but Wiremock + clientProperties.remove( ElasticsearchBackendClientCommonSettings.HOSTS ); + clientProperties.remove( ElasticsearchBackendClientCommonSettings.PROTOCOL ); + clientProperties.remove( ElasticsearchBackendClientCommonSettings.URIS ); + + // Per-test overrides + additionalProperties.accept( clientProperties::put ); + + // By default, target the Wiremock server 1 using HTTP + if ( !clientProperties.containsKey( ElasticsearchBackendClientCommonSettings.HOSTS ) + && !clientProperties.containsKey( ElasticsearchBackendClientCommonSettings.URIS ) ) { + clientProperties.put( ElasticsearchBackendClientCommonSettings.URIS, httpUrisFor( wireMockRule1 ) ); + } + + ConfigurationPropertySource clientPropertySource = AllAwareConfigurationPropertySource.fromMap( clientProperties ); + + Map beanResolverConfiguration = new HashMap<>(); + // Accept Wiremock's self-signed SSL certificates + beanResolverConfiguration.put( + EngineSpiSettings.BEAN_CONFIGURERS, + Collections.singletonList( elasticsearchSslBeanConfigurer( clientPropertySource ) ) + ); + + BeanResolver beanResolver = testConfigurationProvider.createBeanResolverForTest( + AllAwareConfigurationPropertySource.fromMap( beanResolverConfiguration ) + + ); + return new ClientOpenSearchElasticsearchClientFactory().create( beanResolver, clientPropertySource, + threadPoolProvider.threadProvider(), "Client", + new DelegatingSimpleScheduledExecutor( timeoutExecutorService, true ), + GsonProvider.create( GsonBuilder::new, true ) + ); + } + + private ElasticsearchResponse doPost(ElasticsearchClient client, String path, String payload) { + try { + return client.submit( buildRequest( ElasticsearchRequest.post(), path, payload ) ).join(); + } + catch (RuntimeException e) { + throw new AssertionFailure( "Unexpected exception during POST: " + e.getMessage(), e ); + } + } + + private ElasticsearchRequest buildRequest(ElasticsearchRequest.Builder builder, String path, String payload) { + for ( String pathComponent : path.split( "/" ) ) { + if ( !pathComponent.isEmpty() ) { + URLEncodedString fromString = URLEncodedString.fromString( pathComponent ); + builder = builder.pathComponent( fromString ); + } + } + if ( payload != null ) { + builder = builder.body( JsonParser.parseString( payload ).getAsJsonObject() ); + } + return builder.build(); + } + + private static String httpHostAndPortFor(WireMockExtension... extensions) { + return Arrays.stream( extensions ) + .map( extension -> "localhost:" + extension.getPort() ) + .collect( Collectors.joining( "," ) ); + } + + private static String httpsHostAndPortFor(WireMockExtension... extensions) { + return Arrays.stream( extensions ) + .map( extension -> "localhost:" + extension.getHttpsPort() ) + .collect( Collectors.joining( "," ) ); + } + + private static String httpUrisFor(WireMockExtension... extensions) { + return Arrays.stream( extensions ) + .map( extension -> "http://localhost:" + extension.getPort() ) + .collect( Collectors.joining( "," ) ); + } + + private static String httpsUrisFor(WireMockExtension... extensions) { + return Arrays.stream( extensions ) + .map( extension -> "https://localhost:" + extension.getHttpsPort() ) + .collect( Collectors.joining( "," ) ); + } + + private static ResponseDefinitionBuilder elasticsearchResponse() { + return ResponseDefinitionBuilder.okForEmptyJson(); + } + + private String dummyNodeInfoResponse(int... ports) { + JsonObject body = new JsonObject(); + body.addProperty( "cluster_name", "foo-cluster.local" ); + + JsonObject nodes = new JsonObject(); + body.add( "nodes", nodes ); + int index = 1; + for ( int port : ports ) { + nodes.add( "hJLXmY_NTrCytiIMbX4_" + index + "g", dummyNodeInfo( port ) ); + ++index; + } + + return body.toString(); + } + + private JsonObject dummyNodeInfo(int port) { + JsonObject node = new JsonObject(); + node.addProperty( "name", "nodeForPort" + port ); + node.addProperty( "version", ElasticsearchTestDialect.getActualVersion().versionString() ); + + JsonObject http = new JsonObject(); + node.add( "http", http ); + http.addProperty( "publish_address", "127.0.0.1:" + port ); + JsonArray boundAddresses = new JsonArray(); + http.add( "bound_address", boundAddresses ); + boundAddresses.add( "[::]:" + port ); + boundAddresses.add( "127.0.0.1:" + port ); + + JsonArray roles = new JsonArray(); + node.add( "roles", roles ); + roles.add( "ingest" ); + roles.add( "master" ); + roles.add( "data" ); + roles.add( "ml" ); + + node.add( "plugins", new JsonObject() ); + + return node; + } + + private static final ConfigurationProperty MAX_TOTAL_CONNECTION = + ConfigurationProperty.forKey( ClientOpenSearchElasticsearchBackendClientSettings.MAX_CONNECTIONS ) + .asIntegerStrictlyPositive() + .withDefault( ClientOpenSearchElasticsearchBackendClientSettings.Defaults.MAX_CONNECTIONS ) + .build(); + + private static final ConfigurationProperty MAX_TOTAL_CONNECTION_PER_ROUTE = + ConfigurationProperty.forKey( ClientOpenSearchElasticsearchBackendClientSettings.MAX_CONNECTIONS_PER_ROUTE ) + .asIntegerStrictlyPositive() + .withDefault( ClientOpenSearchElasticsearchBackendClientSettings.Defaults.MAX_CONNECTIONS_PER_ROUTE ) + .build(); + + private static final ConfigurationProperty READ_TIMEOUT = + ConfigurationProperty.forKey( ClientOpenSearchElasticsearchBackendClientSettings.READ_TIMEOUT ) + .asIntegerPositiveOrZeroOrNegative() + .withDefault( ClientOpenSearchElasticsearchBackendClientSettings.Defaults.READ_TIMEOUT ) + .build(); + + private static final ConfigurationProperty CONNECTION_TIMEOUT = + ConfigurationProperty.forKey( ClientOpenSearchElasticsearchBackendClientSettings.CONNECTION_TIMEOUT ) + .asIntegerPositiveOrZeroOrNegative() + .withDefault( ClientOpenSearchElasticsearchBackendClientSettings.Defaults.CONNECTION_TIMEOUT ) + .build(); + + private static BeanConfigurer elasticsearchSslBeanConfigurer(ConfigurationPropertySource propertySource) { + return context -> { + context.define( + ElasticsearchHttpClientConfigurer.class, + BeanReference.ofInstance( new ElasticsearchHttpClientConfigurer() { + @Override + public void configure(ElasticsearchHttpClientConfigurationContext context) { + context.clientBuilder() + .setConnectionManager( + PoolingAsyncClientConnectionManagerBuilder.create() + .setMaxConnTotal( MAX_TOTAL_CONNECTION.get( propertySource ) ) + .setMaxConnPerRoute( MAX_TOTAL_CONNECTION_PER_ROUTE.get( propertySource ) ) + .setDefaultConnectionConfig( + ConnectionConfig.copy( ConnectionConfig.DEFAULT ) + .setConnectTimeout( + CONNECTION_TIMEOUT.get( propertySource ), + TimeUnit.MILLISECONDS ) + .setSocketTimeout( READ_TIMEOUT.get( propertySource ), + TimeUnit.MILLISECONDS ) + .build() + ) + .setTlsStrategy( + ClientTlsStrategyBuilder.create() + .setSslContext( buildAllowAnythingSSLContext() ) + .setHostnameVerifier( NoopHostnameVerifier.INSTANCE ) + .buildAsync() + ) + .build() + ); + } + } ) + ); + }; + } + + private static SSLContext buildAllowAnythingSSLContext() { + try { + return SSLContexts.custom().loadTrustMaterial( null, new TrustAllStrategy() ).build(); + } + catch (Exception e) { + throw new AssertionFailure( "Unexpected exception", e ); + } + } +} diff --git a/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/client/ElasticsearchClientFactoryImplIT.java b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/client/ClientRestElasticsearchClientFactoryIT.java similarity index 87% rename from integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/client/ElasticsearchClientFactoryImplIT.java rename to integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/client/ClientRestElasticsearchClientFactoryIT.java index 2ffe9d7981c..782a39f723a 100644 --- a/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/client/ElasticsearchClientFactoryImplIT.java +++ b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/client/ClientRestElasticsearchClientFactoryIT.java @@ -27,7 +27,6 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletionException; import java.util.concurrent.ScheduledExecutorService; @@ -38,17 +37,18 @@ import javax.net.ssl.SSLContext; -import org.hibernate.search.backend.elasticsearch.cfg.ElasticsearchBackendSettings; -import org.hibernate.search.backend.elasticsearch.cfg.spi.ElasticsearchBackendSpiSettings; import org.hibernate.search.backend.elasticsearch.client.ElasticsearchHttpClientConfigurationContext; import org.hibernate.search.backend.elasticsearch.client.ElasticsearchHttpClientConfigurer; -import org.hibernate.search.backend.elasticsearch.client.impl.ElasticsearchClientFactoryImpl; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchClient; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchClientImplementor; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchRequest; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchResponse; -import org.hibernate.search.backend.elasticsearch.gson.spi.GsonProvider; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; +import org.hibernate.search.backend.elasticsearch.client.common.cfg.ElasticsearchBackendClientCommonSettings; +import org.hibernate.search.backend.elasticsearch.client.common.gson.spi.GsonProvider; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchClient; +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.URLEncodedString; +import org.hibernate.search.backend.elasticsearch.client.rest.cfg.ClientRestElasticsearchBackendClientSettings; +import org.hibernate.search.backend.elasticsearch.client.rest.cfg.spi.ClientRestElasticsearchBackendClientSpiSettings; +import org.hibernate.search.backend.elasticsearch.client.rest.impl.ClientRestElasticsearchClientFactory; import org.hibernate.search.engine.cfg.ConfigurationPropertySource; import org.hibernate.search.engine.cfg.spi.AllAwareConfigurationPropertySource; import org.hibernate.search.engine.cfg.spi.EngineSpiSettings; @@ -112,7 +112,7 @@ @MockitoSettings(strictness = Strictness.STRICT_STUBS) @PortedFromSearch5(original = "org.hibernate.search.elasticsearch.test.DefaultElasticsearchClientFactoryTest") -class ElasticsearchClientFactoryImplIT { +class ClientRestElasticsearchClientFactoryIT { // Some tests in here are flaky, for some reason once in a while wiremock takes a very long time to answer // even though no delay was configured. @@ -135,7 +135,7 @@ class ElasticsearchClientFactoryImplIT { private final TestConfigurationProvider testConfigurationProvider = new TestConfigurationProvider(); private final ThreadPoolProviderImpl threadPoolProvider = new ThreadPoolProviderImpl( - BeanHolder.of( new EmbeddedThreadProvider( ElasticsearchClientFactoryImplIT.class.getName() + ": " ) ) ); + BeanHolder.of( new EmbeddedThreadProvider( ClientRestElasticsearchClientFactoryIT.class.getName() + ": " ) ) ); private final ScheduledExecutorService timeoutExecutorService = threadPoolProvider.newScheduledExecutor( 1, "Timeout - " ); @@ -192,8 +192,9 @@ void simple_httpClientConfigurer() throws Exception { HttpResponseInterceptor responseInterceptor = spy( HttpResponseInterceptor.class ); try ( ElasticsearchClientImplementor client = createClient( properties -> properties.accept( - ElasticsearchBackendSettings.CLIENT_CONFIGURER, - (ElasticsearchHttpClientConfigurer) context -> context.clientBuilder() + ClientRestElasticsearchBackendClientSettings.CLIENT_CONFIGURER, + (org.hibernate.search.backend.elasticsearch.client.rest.ElasticsearchHttpClientConfigurer) context -> context + .clientBuilder() .addInterceptorFirst( responseInterceptor ) ) ) ) { ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); @@ -225,7 +226,7 @@ void uris_http() { try ( ElasticsearchClientImplementor client = createClient( properties -> { - properties.accept( ElasticsearchBackendSettings.URIS, httpUrisFor( wireMockRule1 ) ); + properties.accept( ElasticsearchBackendClientCommonSettings.URIS, httpUrisFor( wireMockRule1 ) ); } ) ) { ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); @@ -255,7 +256,7 @@ void pathPrefix_http() { try ( ElasticsearchClientImplementor client = createClient( properties -> { - properties.accept( ElasticsearchBackendSettings.PATH_PREFIX, "bla/bla/bla" ); + properties.accept( ElasticsearchBackendClientCommonSettings.PATH_PREFIX, "bla/bla/bla" ); } ) ) { ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); @@ -285,8 +286,8 @@ void pathPrefix_uris() { try ( ElasticsearchClientImplementor client = createClient( properties -> { - properties.accept( ElasticsearchBackendSettings.PATH_PREFIX, "bla/bla/bla" ); - properties.accept( ElasticsearchBackendSettings.URIS, httpUrisFor( wireMockRule1 ) ); + properties.accept( ElasticsearchBackendClientCommonSettings.PATH_PREFIX, "bla/bla/bla" ); + properties.accept( ElasticsearchBackendClientCommonSettings.URIS, httpUrisFor( wireMockRule1 ) ); } ) ) { ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); @@ -316,8 +317,8 @@ void simple_https() { try ( ElasticsearchClientImplementor client = createClient( properties -> { - properties.accept( ElasticsearchBackendSettings.HOSTS, httpsHostAndPortFor( wireMockRule1 ) ); - properties.accept( ElasticsearchBackendSettings.PROTOCOL, "https" ); + properties.accept( ElasticsearchBackendClientCommonSettings.HOSTS, httpsHostAndPortFor( wireMockRule1 ) ); + properties.accept( ElasticsearchBackendClientCommonSettings.PROTOCOL, "https" ); } ) ) { ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); @@ -347,7 +348,7 @@ void uris_https() { try ( ElasticsearchClientImplementor client = createClient( properties -> { - properties.accept( ElasticsearchBackendSettings.URIS, httpsUrisFor( wireMockRule1 ) ); + properties.accept( ElasticsearchBackendClientCommonSettings.URIS, httpsUrisFor( wireMockRule1 ) ); } ) ) { ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); @@ -418,8 +419,8 @@ void timeout_read() { assertThatThrownBy( () -> { try ( ElasticsearchClientImplementor client = createClient( properties -> { - properties.accept( ElasticsearchBackendSettings.READ_TIMEOUT, "1000" ); - properties.accept( ElasticsearchBackendSettings.REQUEST_TIMEOUT, "99999" ); + properties.accept( ClientRestElasticsearchBackendClientSettings.READ_TIMEOUT, "1000" ); + properties.accept( ClientRestElasticsearchBackendClientSettings.REQUEST_TIMEOUT, "99999" ); } ) ) { doPost( client, "/myIndex/myType", payload ); @@ -446,8 +447,8 @@ void timeout_request() { assertThatThrownBy( () -> { try ( ElasticsearchClientImplementor client = createClient( properties -> { - properties.accept( ElasticsearchBackendSettings.READ_TIMEOUT, "99999" ); - properties.accept( ElasticsearchBackendSettings.REQUEST_TIMEOUT, "1000" ); + properties.accept( ClientRestElasticsearchBackendClientSettings.READ_TIMEOUT, "99999" ); + properties.accept( ClientRestElasticsearchBackendClientSettings.REQUEST_TIMEOUT, "1000" ); } ) ) { doPost( client, "/myIndex/myType", payload ); @@ -481,9 +482,9 @@ void cloggedClient_noTimeout_read() { try ( ElasticsearchClientImplementor client = createClient( properties -> { - properties.accept( ElasticsearchBackendSettings.HOSTS, httpHostAndPortFor( wireMockRule1 ) ); - properties.accept( ElasticsearchBackendSettings.MAX_CONNECTIONS_PER_ROUTE, "1" ); - properties.accept( ElasticsearchBackendSettings.READ_TIMEOUT, "1000" /* 1s */ ); + properties.accept( ElasticsearchBackendClientCommonSettings.HOSTS, httpHostAndPortFor( wireMockRule1 ) ); + properties.accept( ClientRestElasticsearchBackendClientSettings.MAX_CONNECTIONS_PER_ROUTE, "1" ); + properties.accept( ClientRestElasticsearchBackendClientSettings.READ_TIMEOUT, "1000" /* 1s */ ); } ) ) { // Clog up the client: put many requests in the queue, to be executed asynchronously, @@ -518,9 +519,9 @@ void cloggedClient_timeout_request() { try ( ElasticsearchClientImplementor client = createClient( properties -> { - properties.accept( ElasticsearchBackendSettings.HOSTS, httpHostAndPortFor( wireMockRule1 ) ); - properties.accept( ElasticsearchBackendSettings.MAX_CONNECTIONS_PER_ROUTE, "1" ); - properties.accept( ElasticsearchBackendSettings.REQUEST_TIMEOUT, "1000" /* 1s */ ); + properties.accept( ElasticsearchBackendClientCommonSettings.HOSTS, httpHostAndPortFor( wireMockRule1 ) ); + properties.accept( ClientRestElasticsearchBackendClientSettings.MAX_CONNECTIONS_PER_ROUTE, "1" ); + properties.accept( ClientRestElasticsearchBackendClientSettings.REQUEST_TIMEOUT, "1000" /* 1s */ ); } ) ) { // Clog up the client: put many requests in the queue, to be executed asynchronously, @@ -556,7 +557,8 @@ void multipleHosts() { try ( ElasticsearchClientImplementor client = createClient( properties -> { - properties.accept( ElasticsearchBackendSettings.HOSTS, httpHostAndPortFor( wireMockRule1, wireMockRule2 ) ); + properties.accept( ElasticsearchBackendClientCommonSettings.HOSTS, + httpHostAndPortFor( wireMockRule1, wireMockRule2 ) ); } ) ) { ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); @@ -581,7 +583,8 @@ void multipleURIs() { try ( ElasticsearchClientImplementor client = createClient( properties -> { - properties.accept( ElasticsearchBackendSettings.URIS, httpsUrisFor( wireMockRule1, wireMockRule2 ) ); + properties.accept( ElasticsearchBackendClientCommonSettings.URIS, + httpsUrisFor( wireMockRule1, wireMockRule2 ) ); } ) ) { ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); @@ -607,7 +610,8 @@ void multipleHosts_failover_serverError() { try ( ElasticsearchClientImplementor client = createClient( properties -> { - properties.accept( ElasticsearchBackendSettings.HOSTS, httpHostAndPortFor( wireMockRule1, wireMockRule2 ) ); + properties.accept( ElasticsearchBackendClientCommonSettings.HOSTS, + httpHostAndPortFor( wireMockRule1, wireMockRule2 ) ); } ) ) { ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); @@ -645,9 +649,10 @@ void multipleHosts_failover_timeout() { try ( ElasticsearchClientImplementor client = createClient( properties -> { - properties.accept( ElasticsearchBackendSettings.HOSTS, httpHostAndPortFor( wireMockRule1, wireMockRule2 ) ); + properties.accept( ElasticsearchBackendClientCommonSettings.HOSTS, + httpHostAndPortFor( wireMockRule1, wireMockRule2 ) ); // Use a timeout much higher than 1s, because wiremock can be really slow... - properties.accept( ElasticsearchBackendSettings.READ_TIMEOUT, "1000" /* 1s */ ); + properties.accept( ClientRestElasticsearchBackendClientSettings.READ_TIMEOUT, "1000" /* 1s */ ); } ) ) { ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); @@ -694,7 +699,8 @@ void multipleHosts_failover_fault() { try ( ElasticsearchClientImplementor client = createClient( properties -> { - properties.accept( ElasticsearchBackendSettings.HOSTS, httpHostAndPortFor( wireMockRule1, wireMockRule2 ) ); + properties.accept( ElasticsearchBackendClientCommonSettings.HOSTS, + httpHostAndPortFor( wireMockRule1, wireMockRule2 ) ); } ) ) { ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); @@ -743,8 +749,8 @@ void discovery_http() { try ( ElasticsearchClientImplementor client = createClient( properties -> { - properties.accept( ElasticsearchBackendSettings.DISCOVERY_ENABLED, "true" ); - properties.accept( ElasticsearchBackendSettings.DISCOVERY_REFRESH_INTERVAL, "1" ); + properties.accept( ClientRestElasticsearchBackendClientSettings.DISCOVERY_ENABLED, "true" ); + properties.accept( ClientRestElasticsearchBackendClientSettings.DISCOVERY_REFRESH_INTERVAL, "1" ); } ) ) { ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); @@ -797,10 +803,10 @@ void discovery_https() { try ( ElasticsearchClientImplementor client = createClient( properties -> { - properties.accept( ElasticsearchBackendSettings.HOSTS, httpsHostAndPortFor( wireMockRule1 ) ); - properties.accept( ElasticsearchBackendSettings.PROTOCOL, "https" ); - properties.accept( ElasticsearchBackendSettings.DISCOVERY_ENABLED, "true" ); - properties.accept( ElasticsearchBackendSettings.DISCOVERY_REFRESH_INTERVAL, "1" ); + properties.accept( ElasticsearchBackendClientCommonSettings.HOSTS, httpsHostAndPortFor( wireMockRule1 ) ); + properties.accept( ElasticsearchBackendClientCommonSettings.PROTOCOL, "https" ); + properties.accept( ClientRestElasticsearchBackendClientSettings.DISCOVERY_ENABLED, "true" ); + properties.accept( ClientRestElasticsearchBackendClientSettings.DISCOVERY_REFRESH_INTERVAL, "1" ); } ) ) { /* @@ -869,8 +875,8 @@ void authentication() { try ( ElasticsearchClientImplementor client = createClient( properties -> { - properties.accept( ElasticsearchBackendSettings.USERNAME, username ); - properties.accept( ElasticsearchBackendSettings.PASSWORD, password ); + properties.accept( ElasticsearchBackendClientCommonSettings.USERNAME, username ); + properties.accept( ElasticsearchBackendClientCommonSettings.PASSWORD, password ); } ) ) { ElasticsearchResponse result = doPost( client, "/myIndex/myType/_search", payload ); @@ -907,8 +913,8 @@ void authentication_http_password() { try ( ElasticsearchClientImplementor client = createClient( properties -> { - properties.accept( ElasticsearchBackendSettings.USERNAME, username ); - properties.accept( ElasticsearchBackendSettings.PASSWORD, password ); + properties.accept( ElasticsearchBackendClientCommonSettings.USERNAME, username ); + properties.accept( ElasticsearchBackendClientCommonSettings.PASSWORD, password ); } ) ) { // Nothing to do here @@ -918,8 +924,8 @@ void authentication_http_password() { @RetryExtension.TestWithRetry void uriAndProtocol() { Consumer> additionalProperties = properties -> { - properties.accept( ElasticsearchBackendSettings.URIS, "http://is-not-called:12345" ); - properties.accept( ElasticsearchBackendSettings.PROTOCOL, "http" ); + properties.accept( ElasticsearchBackendClientCommonSettings.URIS, "http://is-not-called:12345" ); + properties.accept( ElasticsearchBackendClientCommonSettings.PROTOCOL, "http" ); }; assertThatThrownBy( () -> createClient( additionalProperties ) ) @@ -936,8 +942,8 @@ void uriAndProtocol() { @RetryExtension.TestWithRetry void uriAndHosts() { Consumer> additionalProperties = properties -> { - properties.accept( ElasticsearchBackendSettings.URIS, "http://is-not-called:12345" ); - properties.accept( ElasticsearchBackendSettings.HOSTS, "not-called-either:234" ); + properties.accept( ElasticsearchBackendClientCommonSettings.URIS, "http://is-not-called:12345" ); + properties.accept( ElasticsearchBackendClientCommonSettings.HOSTS, "not-called-either:234" ); }; assertThatThrownBy( () -> createClient( additionalProperties ) ) @@ -954,7 +960,8 @@ void uriAndHosts() { @RetryExtension.TestWithRetry void differentProtocolsOnUris() { Consumer> additionalProperties = properties -> { - properties.accept( ElasticsearchBackendSettings.URIS, "http://is-not-called:12345, https://neather-is:12345" ); + properties.accept( ElasticsearchBackendClientCommonSettings.URIS, + "http://is-not-called:12345, https://neather-is:12345" ); }; assertThatThrownBy( () -> createClient( additionalProperties ) ) @@ -969,7 +976,7 @@ void differentProtocolsOnUris() { @RetryExtension.TestWithRetry void emptyListOfUris() { Consumer> additionalProperties = properties -> { - properties.accept( ElasticsearchBackendSettings.URIS, Collections.emptyList() ); + properties.accept( ElasticsearchBackendClientCommonSettings.URIS, Collections.emptyList() ); }; assertThatThrownBy( () -> createClient( additionalProperties ) ) @@ -982,7 +989,7 @@ void emptyListOfUris() { @RetryExtension.TestWithRetry void emptyListOfHosts() { Consumer> additionalProperties = properties -> { - properties.accept( ElasticsearchBackendSettings.HOSTS, Collections.emptyList() ); + properties.accept( ElasticsearchBackendClientCommonSettings.HOSTS, Collections.emptyList() ); }; assertThatThrownBy( () -> createClient( additionalProperties ) ) @@ -1006,7 +1013,8 @@ void clientInstance() throws IOException { .withBody( responseBody ) ) ); try ( ElasticsearchClientImplementor client = createClient( properties -> { - properties.accept( ElasticsearchBackendSpiSettings.CLIENT_INSTANCE, BeanReference.ofInstance( myRestClient ) ); + properties.accept( ClientRestElasticsearchBackendClientSpiSettings.CLIENT_INSTANCE, + BeanReference.ofInstance( myRestClient ) ); } ) ) { ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); assertThat( result.statusCode() ).as( "status code" ).isEqualTo( 200 ); @@ -1028,7 +1036,7 @@ void clientInstance() throws IOException { void maxKeepAliveNegativeValue() { assertThatThrownBy( () -> createClient( properties -> { - properties.accept( ElasticsearchBackendSettings.MAX_KEEP_ALIVE, -1 ); + properties.accept( ClientRestElasticsearchBackendClientSettings.MAX_KEEP_ALIVE, -1 ); } ) ).isInstanceOf( SearchException.class ) .hasMessageContainingAll( @@ -1061,12 +1069,12 @@ void maxKeepAliveConnection(long time, int connections) throws InterruptedExcept Set usedConnections = Collections.synchronizedSet( new HashSet<>() ); try ( ElasticsearchClientImplementor client = createClient( properties -> { properties.accept( - ElasticsearchBackendSettings.CLIENT_CONFIGURER, - (ElasticsearchHttpClientConfigurer) context -> { + ClientRestElasticsearchBackendClientSettings.CLIENT_CONFIGURER, + (org.hibernate.search.backend.elasticsearch.client.rest.ElasticsearchHttpClientConfigurer) context -> { context.clientBuilder().setConnectionManager( createPoolManager( usedConnections ) ); } ); - properties.accept( ElasticsearchBackendSettings.MAX_KEEP_ALIVE, time ); + properties.accept( ClientRestElasticsearchBackendClientSettings.MAX_KEEP_ALIVE, time ); } ) ) { ElasticsearchResponse result = doPost( client, "/myIndex/myType", payload ); @@ -1138,17 +1146,17 @@ private ElasticsearchClientImplementor createClient(Consumer clientProperties = new HashMap<>( defaultBackendProperties ); // We won't target the provided ES instance, but Wiremock - clientProperties.remove( ElasticsearchBackendSettings.HOSTS ); - clientProperties.remove( ElasticsearchBackendSettings.PROTOCOL ); - clientProperties.remove( ElasticsearchBackendSettings.URIS ); + clientProperties.remove( ElasticsearchBackendClientCommonSettings.HOSTS ); + clientProperties.remove( ElasticsearchBackendClientCommonSettings.PROTOCOL ); + clientProperties.remove( ElasticsearchBackendClientCommonSettings.URIS ); // Per-test overrides additionalProperties.accept( clientProperties::put ); // By default, target the Wiremock server 1 using HTTP - if ( !clientProperties.containsKey( ElasticsearchBackendSettings.HOSTS ) - && !clientProperties.containsKey( ElasticsearchBackendSettings.URIS ) ) { - clientProperties.put( ElasticsearchBackendSettings.URIS, httpUrisFor( wireMockRule1 ) ); + if ( !clientProperties.containsKey( ElasticsearchBackendClientCommonSettings.HOSTS ) + && !clientProperties.containsKey( ElasticsearchBackendClientCommonSettings.URIS ) ) { + clientProperties.put( ElasticsearchBackendClientCommonSettings.URIS, httpUrisFor( wireMockRule1 ) ); } ConfigurationPropertySource clientPropertySource = AllAwareConfigurationPropertySource.fromMap( clientProperties ); @@ -1164,11 +1172,10 @@ private ElasticsearchClientImplementor createClient(Consumer { context.define( - ElasticsearchHttpClientConfigurer.class, + org.hibernate.search.backend.elasticsearch.client.rest.ElasticsearchHttpClientConfigurer.class, BeanReference.ofInstance( new ElasticsearchHttpClientConfigurer() { @Override public void configure(ElasticsearchHttpClientConfigurationContext context) { diff --git a/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/client/ElasticsearchContentLengthIT.java b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/client/ElasticsearchContentLengthIT.java index 92d4bf6b29b..24abc45bc71 100644 --- a/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/client/ElasticsearchContentLengthIT.java +++ b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/client/ElasticsearchContentLengthIT.java @@ -9,6 +9,7 @@ import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static org.hibernate.search.util.impl.integrationtest.backend.elasticsearch.extension.TestElasticsearchClient.getDiscoveredClientFactory; import static org.junit.jupiter.api.Assumptions.assumeFalse; import static org.junit.jupiter.api.Assumptions.assumeTrue; @@ -21,17 +22,15 @@ import java.util.Collection; import java.util.HashMap; import java.util.Map; -import java.util.Optional; import java.util.concurrent.ScheduledExecutorService; -import org.hibernate.search.backend.elasticsearch.cfg.ElasticsearchBackendSettings; -import org.hibernate.search.backend.elasticsearch.client.impl.ElasticsearchClientFactoryImpl; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchClient; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchClientImplementor; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchRequest; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchResponse; -import org.hibernate.search.backend.elasticsearch.gson.spi.GsonProvider; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; +import org.hibernate.search.backend.elasticsearch.client.common.cfg.ElasticsearchBackendClientCommonSettings; +import org.hibernate.search.backend.elasticsearch.client.common.gson.spi.GsonProvider; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchClient; +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.URLEncodedString; import org.hibernate.search.engine.cfg.ConfigurationPropertySource; import org.hibernate.search.engine.cfg.spi.AllAwareConfigurationPropertySource; import org.hibernate.search.engine.common.execution.spi.DelegatingSimpleScheduledExecutor; @@ -41,7 +40,6 @@ import org.hibernate.search.engine.environment.thread.impl.ThreadPoolProviderImpl; import org.hibernate.search.integrationtest.backend.elasticsearch.testsupport.util.ElasticsearchTckBackendHelper; import org.hibernate.search.util.impl.integrationtest.backend.elasticsearch.ElasticsearchTestHostConnectionConfiguration; -import org.hibernate.search.util.impl.integrationtest.backend.elasticsearch.dialect.ElasticsearchTestDialect; import org.hibernate.search.util.impl.integrationtest.common.TestConfigurationProvider; import org.hibernate.search.util.impl.test.annotation.PortedFromSearch5; import org.hibernate.search.util.impl.test.annotation.TestForIssue; @@ -207,21 +205,21 @@ private ElasticsearchClientImplementor createClient() { Map clientProperties = new HashMap<>( defaultBackendProperties ); // We won't target the provided ES instance, but Wiremock - clientProperties.remove( ElasticsearchBackendSettings.HOSTS ); - clientProperties.remove( ElasticsearchBackendSettings.PROTOCOL ); - clientProperties.remove( ElasticsearchBackendSettings.URIS ); + clientProperties.remove( ElasticsearchBackendClientCommonSettings.HOSTS ); + clientProperties.remove( ElasticsearchBackendClientCommonSettings.PROTOCOL ); + clientProperties.remove( ElasticsearchBackendClientCommonSettings.URIS ); // Target the Wiremock server using HTTP - clientProperties.put( ElasticsearchBackendSettings.URIS, httpUriFor( wireMockRule ) ); + clientProperties.put( ElasticsearchBackendClientCommonSettings.URIS, httpUriFor( wireMockRule ) ); ConfigurationPropertySource clientPropertySource = AllAwareConfigurationPropertySource.fromMap( clientProperties ); BeanResolver beanResolver = testConfigurationProvider.createBeanResolverForTest(); - return new ElasticsearchClientFactoryImpl().create( beanResolver, clientPropertySource, - threadPoolProvider.threadProvider(), "Client", - new DelegatingSimpleScheduledExecutor( timeoutExecutorService, true ), - GsonProvider.create( GsonBuilder::new, true ), - Optional.of( ElasticsearchTestDialect.getActualVersion() ) ); + return getDiscoveredClientFactory( beanResolver ).get() + .create( beanResolver, clientPropertySource, + threadPoolProvider.threadProvider(), "Client", + new DelegatingSimpleScheduledExecutor( timeoutExecutorService, true ), + GsonProvider.create( GsonBuilder::new, true ) ); } private ElasticsearchResponse doPost(ElasticsearchClient client, String path, Collection bodyParts) { 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 e6715597140..eb9877ebe9d 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 @@ -10,8 +10,8 @@ import java.util.function.Consumer; import org.hibernate.search.backend.elasticsearch.ElasticsearchExtension; -import org.hibernate.search.backend.elasticsearch.cfg.impl.ElasticsearchBackendImplSettings; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchRequest; +import org.hibernate.search.backend.elasticsearch.client.common.cfg.spi.ElasticsearchBackendClientSpiSettings; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchRequest; import org.hibernate.search.engine.backend.document.model.dsl.IndexSchemaElement; import org.hibernate.search.engine.backend.types.Norms; import org.hibernate.search.engine.backend.types.TermVector; @@ -113,7 +113,7 @@ private void matchMapping(Consumer mapping, JsonObject prope ); setupHelper.start() - .withBackendProperty( ElasticsearchBackendImplSettings.CLIENT_FACTORY, clientSpy.factoryReference() ) + .withBackendProperty( ElasticsearchBackendClientSpiSettings.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 f3eda2f57b8..305124e7fd1 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 @@ -8,8 +8,8 @@ import static org.hibernate.search.util.impl.integrationtest.backend.elasticsearch.ElasticsearchIndexMetadataTestUtils.defaultPrimaryName; import org.hibernate.search.backend.elasticsearch.ElasticsearchExtension; -import org.hibernate.search.backend.elasticsearch.cfg.impl.ElasticsearchBackendImplSettings; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchRequest; +import org.hibernate.search.backend.elasticsearch.client.common.cfg.spi.ElasticsearchBackendClientSpiSettings; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchRequest; import org.hibernate.search.engine.backend.document.model.dsl.IndexSchemaElement; import org.hibernate.search.integrationtest.backend.elasticsearch.testsupport.util.ElasticsearchClientSpy; import org.hibernate.search.integrationtest.backend.elasticsearch.testsupport.util.ElasticsearchRequestAssertionMode; @@ -62,7 +62,7 @@ void test() { setupHelper.start() .withBackendProperty( - ElasticsearchBackendImplSettings.CLIENT_FACTORY, clientSpy.factoryReference() + ElasticsearchBackendClientSpiSettings.CLIENT_FACTORY, clientSpy.factoryReference() ) .withIndex( index ) .setup(); diff --git a/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/mapping/ElasticsearchTypeNameMappingBaseIT.java b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/mapping/ElasticsearchTypeNameMappingBaseIT.java index 708710e2909..07e45cfe103 100644 --- a/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/mapping/ElasticsearchTypeNameMappingBaseIT.java +++ b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/mapping/ElasticsearchTypeNameMappingBaseIT.java @@ -15,8 +15,8 @@ import java.util.List; import org.hibernate.search.backend.elasticsearch.cfg.ElasticsearchBackendSettings; +import org.hibernate.search.backend.elasticsearch.client.common.util.spi.URLEncodedString; import org.hibernate.search.backend.elasticsearch.index.layout.impl.IndexNames; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; import org.hibernate.search.engine.backend.common.DocumentReference; import org.hibernate.search.engine.search.query.SearchQuery; import org.hibernate.search.integrationtest.backend.tck.testsupport.util.extension.SearchSetupHelper; 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 a30fbf7c68a..c70cfeb7c97 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 @@ -17,8 +17,8 @@ import org.hibernate.search.backend.elasticsearch.analysis.ElasticsearchAnalysisConfigurer; import org.hibernate.search.backend.elasticsearch.cfg.ElasticsearchBackendSettings; import org.hibernate.search.backend.elasticsearch.cfg.ElasticsearchIndexSettings; -import org.hibernate.search.backend.elasticsearch.cfg.impl.ElasticsearchBackendImplSettings; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchRequest; +import org.hibernate.search.backend.elasticsearch.client.common.cfg.spi.ElasticsearchBackendClientSpiSettings; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchRequest; import org.hibernate.search.integrationtest.backend.elasticsearch.testsupport.util.ElasticsearchClientSpy; import org.hibernate.search.integrationtest.backend.elasticsearch.testsupport.util.ElasticsearchRequestAssertionMode; import org.hibernate.search.integrationtest.backend.elasticsearch.testsupport.util.ElasticsearchTckBackendFeatures; @@ -75,7 +75,7 @@ void schema(String strategyName, JsonObject expectedMappingContent) { setupHelper.start() .withBackendProperty( - ElasticsearchBackendImplSettings.CLIENT_FACTORY, clientSpy.factoryReference() + ElasticsearchBackendClientSpiSettings.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/schema/management/ElasticsearchIndexSchemaManagerCreationAliasesIT.java b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/schema/management/ElasticsearchIndexSchemaManagerCreationAliasesIT.java index 6b3686af185..95be403ee5b 100644 --- a/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/schema/management/ElasticsearchIndexSchemaManagerCreationAliasesIT.java +++ b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/schema/management/ElasticsearchIndexSchemaManagerCreationAliasesIT.java @@ -19,7 +19,7 @@ import org.hibernate.search.backend.elasticsearch.analysis.ElasticsearchAnalysisConfigurer; import org.hibernate.search.backend.elasticsearch.cfg.ElasticsearchBackendSettings; import org.hibernate.search.backend.elasticsearch.cfg.ElasticsearchIndexSettings; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; +import org.hibernate.search.backend.elasticsearch.client.common.util.spi.URLEncodedString; import org.hibernate.search.integrationtest.backend.elasticsearch.testsupport.configuration.StubSingleIndexLayoutStrategy; import org.hibernate.search.integrationtest.backend.tck.testsupport.util.extension.SearchSetupHelper; import org.hibernate.search.util.impl.integrationtest.backend.elasticsearch.extension.TestElasticsearchClient; 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 a5c3aff27a0..095e14c7b0b 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 @@ -11,10 +11,10 @@ import java.util.List; import org.hibernate.search.backend.elasticsearch.cfg.ElasticsearchBackendSettings; -import org.hibernate.search.backend.elasticsearch.cfg.impl.ElasticsearchBackendImplSettings; +import org.hibernate.search.backend.elasticsearch.client.common.cfg.spi.ElasticsearchBackendClientSpiSettings; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchRequest; +import org.hibernate.search.backend.elasticsearch.client.common.util.spi.URLEncodedString; import org.hibernate.search.backend.elasticsearch.client.impl.Paths; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchRequest; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; import org.hibernate.search.engine.backend.document.IndexFieldReference; import org.hibernate.search.engine.backend.document.model.dsl.IndexSchemaElement; import org.hibernate.search.engine.backend.types.Projectable; @@ -58,7 +58,7 @@ public static List params() { public void init(Object layoutStrategy, URLEncodedString readName) { setupHelper.start() .withBackendProperty( - ElasticsearchBackendImplSettings.CLIENT_FACTORY, clientSpy.factoryReference() + ElasticsearchBackendClientSpiSettings.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 5b92b4e9b6f..ceec2c4723b 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 @@ -9,11 +9,11 @@ import static org.hibernate.search.util.impl.integrationtest.backend.elasticsearch.ElasticsearchIndexMetadataTestUtils.defaultReadAlias; import org.hibernate.search.backend.elasticsearch.ElasticsearchExtension; -import org.hibernate.search.backend.elasticsearch.cfg.impl.ElasticsearchBackendImplSettings; +import org.hibernate.search.backend.elasticsearch.client.common.cfg.spi.ElasticsearchBackendClientSpiSettings; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchRequest; +import org.hibernate.search.backend.elasticsearch.client.common.util.spi.URLEncodedString; import org.hibernate.search.backend.elasticsearch.client.impl.Paths; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchRequest; import org.hibernate.search.backend.elasticsearch.search.query.ElasticsearchSearchRequestTransformer; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; import org.hibernate.search.engine.backend.common.DocumentReference; import org.hibernate.search.engine.backend.document.IndexFieldReference; import org.hibernate.search.engine.backend.document.model.dsl.IndexSchemaElement; @@ -53,7 +53,7 @@ class ElasticsearchSearchQueryRequestTransformerIT { void setup() { setupHelper.start() .withBackendProperty( - ElasticsearchBackendImplSettings.CLIENT_FACTORY, clientSpy.factoryReference() + ElasticsearchBackendClientSpiSettings.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/ElasticsearchClientSpy.java b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/testsupport/util/ElasticsearchClientSpy.java index 1fa0e253ce6..ac921062454 100644 --- a/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/testsupport/util/ElasticsearchClientSpy.java +++ b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/testsupport/util/ElasticsearchClientSpy.java @@ -4,17 +4,16 @@ */ package org.hibernate.search.integrationtest.backend.elasticsearch.testsupport.util; -import java.util.Optional; +import static org.hibernate.search.util.impl.integrationtest.backend.elasticsearch.extension.TestElasticsearchClient.getDiscoveredClientFactory; + import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicInteger; -import org.hibernate.search.backend.elasticsearch.ElasticsearchVersion; -import org.hibernate.search.backend.elasticsearch.client.impl.ElasticsearchClientFactoryImpl; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchClientFactory; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchClientImplementor; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchRequest; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchResponse; -import org.hibernate.search.backend.elasticsearch.gson.spi.GsonProvider; +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.ElasticsearchRequest; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchResponse; import org.hibernate.search.engine.cfg.ConfigurationPropertySource; import org.hibernate.search.engine.common.execution.spi.SimpleScheduledExecutor; import org.hibernate.search.engine.environment.bean.BeanHolder; @@ -78,7 +77,8 @@ public int getRequestCount() { } public BeanReference factoryReference() { - return beanResolver -> BeanHolder.of( new SpyingElasticsearchClientFactory( new ElasticsearchClientFactoryImpl() ) ); + return beanResolver -> BeanHolder.of( + new SpyingElasticsearchClientFactory( getDiscoveredClientFactory( beanResolver ).get() ) ); } public void expectNext(ElasticsearchRequest request, ElasticsearchRequestAssertionMode assertionMode) { @@ -100,11 +100,11 @@ private SpyingElasticsearchClientFactory( public ElasticsearchClientImplementor create(BeanResolver beanResolver, ConfigurationPropertySource propertySource, ThreadProvider threadProvider, String threadNamePrefix, SimpleScheduledExecutor timeoutExecutorService, - GsonProvider gsonProvider, Optional configuredVersion) { + GsonProvider gsonProvider) { createdClientCount.incrementAndGet(); return new SpyingElasticsearchClient( delegate.create( beanResolver, propertySource, threadProvider, threadNamePrefix, - timeoutExecutorService, gsonProvider, configuredVersion + timeoutExecutorService, gsonProvider ) ); } } diff --git a/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/testsupport/util/ElasticsearchClientSubmitCall.java b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/testsupport/util/ElasticsearchClientSubmitCall.java index 74ea36bafaf..930567bc2c4 100644 --- a/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/testsupport/util/ElasticsearchClientSubmitCall.java +++ b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/testsupport/util/ElasticsearchClientSubmitCall.java @@ -9,7 +9,7 @@ import java.util.List; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchRequest; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchRequest; import org.hibernate.search.util.impl.integrationtest.common.extension.Call; import com.google.gson.Gson; 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 0f9d2f7f4a9..26509315e5c 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 @@ -12,10 +12,10 @@ import java.util.List; import org.hibernate.search.backend.elasticsearch.cfg.ElasticsearchBackendSettings; -import org.hibernate.search.backend.elasticsearch.cfg.impl.ElasticsearchBackendImplSettings; +import org.hibernate.search.backend.elasticsearch.client.common.cfg.spi.ElasticsearchBackendClientSpiSettings; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchRequest; +import org.hibernate.search.backend.elasticsearch.client.common.util.spi.URLEncodedString; import org.hibernate.search.backend.elasticsearch.client.impl.Paths; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchRequest; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; import org.hibernate.search.engine.backend.document.IndexFieldReference; import org.hibernate.search.engine.backend.document.model.dsl.IndexSchemaElement; import org.hibernate.search.engine.backend.work.execution.OperationSubmitter; @@ -61,7 +61,7 @@ public static List params() { public void init(Object layoutStrategy, URLEncodedString writeName) { setupHelper.start() .withBackendProperty( - ElasticsearchBackendImplSettings.CLIENT_FACTORY, clientSpy.factoryReference() + ElasticsearchBackendClientSpiSettings.CLIENT_FACTORY, clientSpy.factoryReference() ) .withBackendProperty( ElasticsearchBackendSettings.LAYOUT_STRATEGY, layoutStrategy diff --git a/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/work/ElasticsearchZeroDowntimeReindexingIT.java b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/work/ElasticsearchZeroDowntimeReindexingIT.java index e8fc091cdf6..0fe8d028d5c 100644 --- a/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/work/ElasticsearchZeroDowntimeReindexingIT.java +++ b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/work/ElasticsearchZeroDowntimeReindexingIT.java @@ -12,7 +12,7 @@ import static org.hibernate.search.util.impl.integrationtest.common.assertion.SearchResultAssert.assertThatQuery; import static org.hibernate.search.util.impl.integrationtest.mapper.stub.StubMapperUtils.referenceProvider; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; +import org.hibernate.search.backend.elasticsearch.client.common.util.spi.URLEncodedString; import org.hibernate.search.engine.backend.common.DocumentReference; import org.hibernate.search.engine.backend.document.IndexFieldReference; import org.hibernate.search.engine.backend.document.model.dsl.IndexSchemaElement; diff --git a/integrationtest/mapper/orm-jakarta-batch/src/test/java/org/hibernate/search/integrationtest/jakarta/batch/util/HibernateSearchBatchTestConnectionProperties.java b/integrationtest/mapper/orm-jakarta-batch/src/test/java/org/hibernate/search/integrationtest/jakarta/batch/util/HibernateSearchBatchTestConnectionProperties.java index 3194c030a29..b66989ec57b 100644 --- a/integrationtest/mapper/orm-jakarta-batch/src/test/java/org/hibernate/search/integrationtest/jakarta/batch/util/HibernateSearchBatchTestConnectionProperties.java +++ b/integrationtest/mapper/orm-jakarta-batch/src/test/java/org/hibernate/search/integrationtest/jakarta/batch/util/HibernateSearchBatchTestConnectionProperties.java @@ -7,7 +7,7 @@ import java.util.HashMap; import java.util.Map; -import org.hibernate.search.backend.elasticsearch.cfg.ElasticsearchBackendSettings; +import org.hibernate.search.backend.elasticsearch.client.common.cfg.ElasticsearchBackendClientCommonSettings; import org.hibernate.search.engine.cfg.BackendSettings; import org.hibernate.search.util.impl.integrationtest.backend.elasticsearch.SearchBackendContainer; import org.hibernate.search.util.impl.integrationtest.mapper.orm.DatabaseContainer; @@ -21,7 +21,7 @@ public static Map connectionProperties() { // if we run the ES version of the tests we have to set correct connection info based on testcontainer: if ( "elasticsearch_pu".equals( PersistenceUnitTestUtil.getPersistenceUnitName() ) ) { properties.put( - BackendSettings.backendKey( ElasticsearchBackendSettings.URIS ), + BackendSettings.backendKey( ElasticsearchBackendClientCommonSettings.URIS ), SearchBackendContainer.connectionUrl() ); } diff --git a/pom.xml b/pom.xml index 31767c64c54..1c4f98e5ba2 100644 --- a/pom.xml +++ b/pom.xml @@ -174,6 +174,10 @@ backend/lucene backend/elasticsearch backend/elasticsearch-aws + backend/elasticsearch-client/common + backend/elasticsearch-client/elasticsearch-rest-client + backend/elasticsearch-client/elasticsearch-java-client + backend/elasticsearch-client/opensearch-rest-client mapper/pojo-base mapper/pojo-standalone mapper/orm diff --git a/util/common/pom.xml b/util/common/pom.xml index 316946dca56..31809b14643 100644 --- a/util/common/pom.xml +++ b/util/common/pom.xml @@ -62,7 +62,7 @@ - org.hibernate.search.*.impl to org.hibernate.search.util.common, org.hibernate.search.engine, org.hibernate.search.backend.lucene, org.hibernate.search.backend.elasticsearch, org.hibernate.search.backend.elasticsearch.aws, org.hibernate.search.mapper.pojo, org.hibernate.search.mapper.pojo.standalone, org.hibernate.search.mapper.orm, org.hibernate.search.mapper.orm.outboxpolling, org.hibernate.search.jakarta.batch.core, org.jboss.logging; + org.hibernate.search.*.impl to org.hibernate.search.util.common, org.hibernate.search.engine, org.hibernate.search.backend.lucene, org.hibernate.search.backend.elasticsearch,org.hibernate.search.backend.elasticsearch.client.common, org.hibernate.search.backend.elasticsearch.client.rest, org.hibernate.search.backend.elasticsearch.client.opensearch, org.hibernate.search.backend.elasticsearch.client.java, org.hibernate.search.backend.elasticsearch.aws, org.hibernate.search.mapper.pojo, org.hibernate.search.mapper.pojo.standalone, org.hibernate.search.mapper.orm, org.hibernate.search.mapper.orm.outboxpolling, org.hibernate.search.jakarta.batch.core, org.jboss.logging; *; diff --git a/util/common/src/main/java/org/hibernate/search/util/common/logging/impl/MessageConstants.java b/util/common/src/main/java/org/hibernate/search/util/common/logging/impl/MessageConstants.java index d792c7a0a48..0e0cc1eda24 100644 --- a/util/common/src/main/java/org/hibernate/search/util/common/logging/impl/MessageConstants.java +++ b/util/common/src/main/java/org/hibernate/search/util/common/logging/impl/MessageConstants.java @@ -45,6 +45,9 @@ private MessageConstants() { public static final int BACKEND_ES_AWS_ID_RANGE_MIN = 409000; public static final int BACKEND_ES_AWS_ID_RANGE_MAX = 409999; + public static final int BACKEND_ES_CLIENT_ID_RANGE_MIN = 410000; + public static final int BACKEND_ES_CLIENT_ID_RANGE_MAX = 418999; + public static final int JAKARTA_BATCH_CORE_ID_RANGE_MIN = 500000; public static final int JAKARTA_BATCH_CORE_ID_RANGE_MAX = 508999; diff --git a/util/internal/integrationtest/backend/elasticsearch/src/main/java/org/hibernate/search/util/impl/integrationtest/backend/elasticsearch/ElasticsearchIndexMetadataTestUtils.java b/util/internal/integrationtest/backend/elasticsearch/src/main/java/org/hibernate/search/util/impl/integrationtest/backend/elasticsearch/ElasticsearchIndexMetadataTestUtils.java index 3118a93640f..7e5af071768 100644 --- a/util/internal/integrationtest/backend/elasticsearch/src/main/java/org/hibernate/search/util/impl/integrationtest/backend/elasticsearch/ElasticsearchIndexMetadataTestUtils.java +++ b/util/internal/integrationtest/backend/elasticsearch/src/main/java/org/hibernate/search/util/impl/integrationtest/backend/elasticsearch/ElasticsearchIndexMetadataTestUtils.java @@ -6,8 +6,8 @@ import java.util.Set; +import org.hibernate.search.backend.elasticsearch.client.common.util.spi.URLEncodedString; import org.hibernate.search.backend.elasticsearch.index.layout.impl.IndexNames; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; import org.hibernate.search.util.common.impl.CollectionHelper; import com.google.gson.Gson; diff --git a/util/internal/integrationtest/backend/elasticsearch/src/main/java/org/hibernate/search/util/impl/integrationtest/backend/elasticsearch/extension/TestElasticsearchClient.java b/util/internal/integrationtest/backend/elasticsearch/src/main/java/org/hibernate/search/util/impl/integrationtest/backend/elasticsearch/extension/TestElasticsearchClient.java index eefa4d14562..3f1ceb4e333 100644 --- a/util/internal/integrationtest/backend/elasticsearch/src/main/java/org/hibernate/search/util/impl/integrationtest/backend/elasticsearch/extension/TestElasticsearchClient.java +++ b/util/internal/integrationtest/backend/elasticsearch/src/main/java/org/hibernate/search/util/impl/integrationtest/backend/elasticsearch/extension/TestElasticsearchClient.java @@ -23,22 +23,23 @@ import org.hibernate.search.backend.elasticsearch.ElasticsearchVersion; import org.hibernate.search.backend.elasticsearch.cfg.ElasticsearchIndexSettings; -import org.hibernate.search.backend.elasticsearch.client.impl.ElasticsearchClientFactoryImpl; +import org.hibernate.search.backend.elasticsearch.client.common.gson.spi.GsonProvider; +import org.hibernate.search.backend.elasticsearch.client.common.logging.spi.ElasticsearchRequestFormatter; +import org.hibernate.search.backend.elasticsearch.client.common.logging.spi.ElasticsearchResponseFormatter; +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.ElasticsearchRequest; +import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchResponse; +import org.hibernate.search.backend.elasticsearch.client.common.util.spi.URLEncodedString; import org.hibernate.search.backend.elasticsearch.client.impl.ElasticsearchClientUtils; import org.hibernate.search.backend.elasticsearch.client.impl.Paths; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchClientImplementor; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchRequest; -import org.hibernate.search.backend.elasticsearch.client.spi.ElasticsearchResponse; -import org.hibernate.search.backend.elasticsearch.gson.spi.GsonProvider; import org.hibernate.search.backend.elasticsearch.index.IndexStatus; import org.hibernate.search.backend.elasticsearch.index.layout.impl.IndexNames; -import org.hibernate.search.backend.elasticsearch.logging.impl.ElasticsearchRequestFormatter; -import org.hibernate.search.backend.elasticsearch.logging.impl.ElasticsearchResponseFormatter; -import org.hibernate.search.backend.elasticsearch.util.spi.URLEncodedString; import org.hibernate.search.engine.cfg.ConfigurationPropertySource; import org.hibernate.search.engine.cfg.spi.AllAwareConfigurationPropertySource; import org.hibernate.search.engine.common.execution.spi.DelegatingSimpleScheduledExecutor; 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.impl.EmbeddedThreadProvider; import org.hibernate.search.engine.environment.thread.impl.ThreadPoolProviderImpl; @@ -497,21 +498,22 @@ public void open(TestConfigurationProvider configurationProvider, Optional getDiscoveredClientFactory(BeanResolver beanResolver) { + BeanReference reference = null; + for ( var entry : beanResolver.namedConfiguredForRole( ElasticsearchClientFactory.class ).entrySet() ) { + reference = entry.getValue(); + if ( !ElasticsearchClientFactory.DEFAULT_BEAN_NAME.equals( entry.getKey() ) ) { + break; + } + } + return beanResolver.resolve( reference ); + } }