/* * Hibernate Search, full-text search for your domain model * * License: GNU Lesser General Public License (LGPL), version 2.1 or later * See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>. */ package org.hibernate.search.elasticsearch.testutil; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.util.Arrays; import java.util.List; import java.util.Properties; import org.apache.http.HttpEntity; import org.apache.http.HttpHost; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.entity.ContentType; import org.apache.http.impl.client.BasicCredentialsProvider; import org.elasticsearch.client.Response; import org.elasticsearch.client.ResponseException; import org.elasticsearch.client.RestClient; import org.hibernate.search.elasticsearch.cfg.ElasticsearchEnvironment; import org.hibernate.search.elasticsearch.cfg.ElasticsearchIndexStatus; import org.hibernate.search.elasticsearch.client.impl.ElasticsearchRequest; import org.hibernate.search.elasticsearch.client.impl.Paths; import org.hibernate.search.elasticsearch.client.impl.URLEncodedString; import org.hibernate.search.elasticsearch.dialect.impl.DialectIndependentGsonProvider; import org.hibernate.search.elasticsearch.impl.ElasticsearchIndexNameNormalizer; import org.hibernate.search.elasticsearch.impl.JsonBuilder; import org.hibernate.search.elasticsearch.logging.impl.Log; import org.hibernate.search.elasticsearch.util.impl.ElasticsearchClientUtils; import org.hibernate.search.exception.AssertionFailure; import org.hibernate.search.testsupport.setup.TestDefaults; import org.hibernate.search.util.impl.SearchThreadFactory; import org.hibernate.search.util.logging.impl.LoggerFactory; import org.junit.rules.ExternalResource; import com.google.common.collect.Lists; import com.google.gson.Gson; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; /** * @author Yoann Rodiere */ public class TestElasticsearchClient extends ExternalResource { private static final Log LOG = LoggerFactory.make( Log.class ); private RestClient client; private Gson gson; private final List<URLEncodedString> createdIndicesNames = Lists.newArrayList(); private final List<String> createdTemplatesNames = Lists.newArrayList(); public IndexClient index(Class<?> rootClass) { return new IndexClient( ElasticsearchIndexNameNormalizer.getElasticsearchIndexName( rootClass.getName() ) ); } public IndexClient index(String indexName) { return new IndexClient( URLEncodedString.fromString( indexName ) ); } public TypeClient type(Class<?> rootClass) { return index( rootClass ).type( rootClass ); } public class IndexClient { private final URLEncodedString indexName; public IndexClient(URLEncodedString indexName) { this.indexName = indexName; } public void waitForRequiredIndexStatus() throws IOException { TestElasticsearchClient.this.waitForRequiredIndexStatus( indexName ); } public IndexClient deleteAndCreate() throws IOException { TestElasticsearchClient.this.deleteAndCreateIndex( indexName ); return this; } public IndexClient ensureDoesNotExist() throws IOException { TestElasticsearchClient.this.ensureIndexDoesNotExist( indexName ); return this; } public IndexClient registerForCleanup() { TestElasticsearchClient.this.registerIndexForCleanup( indexName ); return this; } public TypeClient type(Class<?> mappingClass) { return type( mappingClass.getName() ); } public TypeClient type(String mappingName) { return new TypeClient( this, mappingName ); } public SettingsClient settings() { return settings( "" ); } public SettingsClient settings(String settingsPath) { return new SettingsClient( this, settingsPath ); } } public class TypeClient { private final IndexClient indexClient; private final URLEncodedString typeName; public TypeClient(IndexClient indexClient, String mappingName) { this.indexClient = indexClient; this.typeName = URLEncodedString.fromString( mappingName ); } public TypeClient putMapping(String mappingJson) throws IOException { TestElasticsearchClient.this.putMapping( indexClient.indexName, typeName, mappingJson ); return this; } public String getMapping() throws IOException { return TestElasticsearchClient.this.getMapping( indexClient.indexName, typeName ); } public TypeClient index(URLEncodedString id, String jsonDocument) throws IOException { URLEncodedString indexName = indexClient.indexName; TestElasticsearchClient.this.index( indexName, typeName, id, jsonDocument ); return this; } public DocumentClient document(String id) { return new DocumentClient( this, id ); } } public class SettingsClient { private final IndexClient indexClient; private final String settingsPath; public SettingsClient(IndexClient indexClient, String settingsPath) { this.indexClient = indexClient; this.settingsPath = settingsPath; } public String get() throws IOException { URLEncodedString indexName = indexClient.indexName; return TestElasticsearchClient.this.getSettings( indexName, settingsPath ); } public void put(String settings) throws IOException { URLEncodedString indexName = indexClient.indexName; TestElasticsearchClient.this.putSettings( indexName, settingsPath, settings ); } } public class DocumentClient { private final TypeClient typeClient; private final URLEncodedString id; public DocumentClient(TypeClient typeClient, String id) { this.typeClient = typeClient; this.id = URLEncodedString.fromString( id ); } public JsonObject getSource() throws IOException { return TestElasticsearchClient.this.getDocumentSource( typeClient.indexClient.indexName, typeClient.typeName, id ); } public JsonElement getStoredField(String fieldName) throws IOException { return TestElasticsearchClient.this.getDocumentField( typeClient.indexClient.indexName, typeClient.typeName, id, fieldName ); } } public TemplateClient template(String templateName) { return new TemplateClient( templateName ); } public class TemplateClient { private final String templateName; public TemplateClient(String templateName) { this.templateName = templateName; } public TemplateClient create(String templateString, JsonObject settings) throws IOException { TestElasticsearchClient.this.createTemplate( templateName, templateString, settings ); return this; } public TemplateClient registerForCleanup() { TestElasticsearchClient.this.registerTemplateForCleanup( templateName ); return this; } } private void deleteAndCreateIndex(URLEncodedString indexName) throws IOException { // Ignore the result: if the deletion fails, we don't care unless the creation just after also fails tryDeleteESIndex( indexName ); registerIndexForCleanup( indexName ); performRequest( ElasticsearchRequest.put().pathComponent( indexName ).build() ); waitForRequiredIndexStatus( indexName ); } private void createTemplate(String templateName, String templateString, JsonObject settings) throws IOException { JsonObject source = JsonBuilder.object() .addProperty( "template", templateString ) .add( "settings", settings ) .build(); registerTemplateForCleanup( templateName ); performRequest( ElasticsearchRequest.put() .pathComponent( Paths._TEMPLATE ).pathComponent( URLEncodedString.fromString( templateName ) ) .body( source ) .build() ); } private void ensureIndexDoesNotExist(URLEncodedString indexName) throws IOException { try { performRequest( ElasticsearchRequest.delete() .pathComponent( indexName ) .build() ); } catch (ResponseException e) { if ( e.getResponse().getStatusLine().getStatusCode() != 404 /* Index not found is ok */ ) { throw e; } } } private void registerIndexForCleanup(URLEncodedString indexName) { createdIndicesNames.add( indexName ); } private void registerTemplateForCleanup(String templateName) { createdTemplatesNames.add( templateName ); } private void waitForRequiredIndexStatus(final URLEncodedString indexName) throws IOException { performRequest( ElasticsearchRequest.get() .pathComponent( Paths._CLUSTER ).pathComponent( Paths.HEALTH ).pathComponent( indexName ) /* * We only wait for YELLOW: it's perfectly fine, and some tests actually expect * the indexes to never reach a green status */ .param( "wait_for_status", ElasticsearchIndexStatus.YELLOW.getElasticsearchString() ) .param( "timeout", ElasticsearchEnvironment.Defaults.INDEX_MANAGEMENT_WAIT_TIMEOUT + "ms" ) .build() ); } private void putMapping(URLEncodedString indexName, URLEncodedString mappingName, String mappingJson) throws IOException { JsonObject mappingJsonObject = toJsonElement( mappingJson ).getAsJsonObject(); performRequest( ElasticsearchRequest.put() .pathComponent( indexName ).pathComponent( Paths._MAPPING ).pathComponent( mappingName ) .body( mappingJsonObject ) .build() ); } private String getMapping(URLEncodedString indexName, URLEncodedString mappingName) throws IOException { Response response = performRequest( ElasticsearchRequest.get() .pathComponent( indexName ).pathComponent( Paths._MAPPING ).pathComponent( mappingName ) .build() ); JsonObject result = toJsonObject( response ); JsonElement index = result.get( indexName.original ); if ( index == null ) { return new JsonObject().toString(); } JsonElement mappings = index.getAsJsonObject().get( "mappings" ); if ( mappings == null ) { return new JsonObject().toString(); } JsonElement mapping = mappings.getAsJsonObject().get( mappingName.original ); if ( mapping == null ) { return new JsonObject().toString(); } return mapping.toString(); } private void putSettings(URLEncodedString indexName, String settingsPath, String settings) throws IOException { JsonElement settingsJsonElement = toJsonElement( settings ); for ( String property : Lists.reverse( Arrays.asList( settingsPath.split( "\\." ) ) ) ) { settingsJsonElement = JsonBuilder.object().add( property, settingsJsonElement ).build(); } performRequest( ElasticsearchRequest.post() .pathComponent( indexName ) .pathComponent( Paths._CLOSE ) .build() ); performRequest( ElasticsearchRequest.put() .pathComponent( indexName ).pathComponent( Paths._SETTINGS ) .body( settingsJsonElement.getAsJsonObject() ) .build() ); performRequest( ElasticsearchRequest.post() .pathComponent( indexName ) .pathComponent( Paths._OPEN ) .build() ); } private String getSettings(URLEncodedString indexName, String path) throws IOException { Response response = performRequest( ElasticsearchRequest.get() .pathComponent( indexName ).pathComponent( Paths._SETTINGS ) .build() ); JsonObject result = toJsonObject( response ); JsonElement index = result.get( indexName.original ); if ( index == null ) { return new JsonObject().toString(); } JsonElement settings = index.getAsJsonObject().get( "settings" ); for ( String property : path.split( "\\." ) ) { if ( settings == null ) { break; } settings = settings.getAsJsonObject().get( property ); } if ( settings == null ) { return new JsonObject().toString(); } return settings.toString(); } private void index(URLEncodedString indexName, URLEncodedString typeName, URLEncodedString id, String jsonDocument) throws IOException { JsonObject documentJsonObject = toJsonElement( jsonDocument ).getAsJsonObject(); performRequest( ElasticsearchRequest.put() .pathComponent( indexName ).pathComponent( typeName ).pathComponent( id ) .body( documentJsonObject ) .param( "refresh", true ) .build() ); } private JsonObject getDocumentSource(URLEncodedString indexName, URLEncodedString typeName, URLEncodedString id) throws IOException { Response response = performRequest( ElasticsearchRequest.get() .pathComponent( indexName ).pathComponent( typeName ).pathComponent( id ) .build() ); JsonObject result = toJsonObject( response ); return result.get( "_source" ).getAsJsonObject(); } protected JsonElement getDocumentField(URLEncodedString indexName, URLEncodedString typeName, URLEncodedString id, String fieldName) throws IOException { Response response = performRequest( ElasticsearchRequest.get() .pathComponent( indexName ).pathComponent( typeName ).pathComponent( id ) .param( "stored_fields", fieldName ) .build() ); JsonObject result = toJsonObject( response ); return result.get( "fields" ).getAsJsonObject().get( fieldName ); } @Override protected void before() throws Throwable { gson = DialectIndependentGsonProvider.INSTANCE.getGson(); Properties properties = TestDefaults.getProperties(); String username = properties.getProperty( "hibernate.search.default." + ElasticsearchEnvironment.SERVER_USERNAME ); String password = properties.getProperty( "hibernate.search.default." + ElasticsearchEnvironment.SERVER_PASSWORD ); this.client = RestClient.builder( HttpHost.create( ElasticsearchEnvironment.Defaults.SERVER_URI ) ) /* * Note: this timeout is not only used on retries, * but also when executing requests synchronously. * See https://github.com/elastic/elasticsearch/issues/21789#issuecomment-287399115 */ .setMaxRetryTimeoutMillis( ElasticsearchEnvironment.Defaults.SERVER_REQUEST_TIMEOUT ) .setHttpClientConfigCallback( (builder) -> { if ( username != null ) { BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider(); credentialsProvider.setCredentials( new AuthScope( AuthScope.ANY_HOST, AuthScope.ANY_PORT, AuthScope.ANY_REALM, AuthScope.ANY_SCHEME ), new UsernamePasswordCredentials( username, password ) ); builder = builder.setDefaultCredentialsProvider( credentialsProvider ); } return builder .setMaxConnTotal( ElasticsearchEnvironment.Defaults.MAX_TOTAL_CONNECTION ) .setMaxConnPerRoute( ElasticsearchEnvironment.Defaults.MAX_TOTAL_CONNECTION_PER_ROUTE ) .setThreadFactory( new SearchThreadFactory( "Test Elasticsearch client transport thread" ) ); } ) .setRequestConfigCallback( (builder) -> { return builder .setSocketTimeout( ElasticsearchEnvironment.Defaults.SERVER_READ_TIMEOUT ) .setConnectTimeout( ElasticsearchEnvironment.Defaults.SERVER_CONNECTION_TIMEOUT ); } ) .build(); } @Override protected void after() { for ( URLEncodedString indexName : createdIndicesNames ) { tryDeleteESIndex( indexName ); } createdIndicesNames.clear(); for ( String templateName : createdTemplatesNames ) { tryDeleteESTemplate( templateName ); } createdTemplatesNames.clear(); try { client.close(); client = null; } catch (IOException e) { throw new AssertionFailure( "Unexpected exception when closing the RestClient", e ); } } private void tryDeleteESIndex(URLEncodedString indexName) { try { performRequest( ElasticsearchRequest.delete() .pathComponent( indexName ) .build() ); } catch (ResponseException e) { if ( e.getResponse().getStatusLine().getStatusCode() != 404 /* Index not found is ok */ ) { LOG.warnf( e, "Error while trying to delete index '%s' as part of test cleanup", indexName ); } } catch (IOException | RuntimeException e) { LOG.warnf( e, "Error while trying to delete index '%s' as part of test cleanup", indexName ); } } private void tryDeleteESTemplate(String templateName) { try { performRequest( ElasticsearchRequest.delete() .pathComponent( Paths._TEMPLATE ).pathComponent( URLEncodedString.fromString( templateName ) ) .build() ); } catch (ResponseException e) { if ( e.getResponse().getStatusLine().getStatusCode() != 404 /* Template not found is ok */ ) { LOG.warnf( e, "Error while trying to delete template '%s' as part of test cleanup", templateName ); } } catch (IOException | RuntimeException e) { LOG.warnf( e, "Error while trying to delete template '%s' as part of test cleanup", templateName ); } } protected Response performRequest(ElasticsearchRequest request) throws IOException { return client.performRequest( request.getMethod(), request.getPath(), request.getParameters(), ElasticsearchClientUtils.toEntity( gson, request ) ); } protected JsonObject toJsonObject(Response response) throws IOException { HttpEntity entity = response.getEntity(); if ( entity == null ) { return null; } ContentType contentType = ContentType.get( entity ); try ( InputStream inputStream = entity.getContent(); Reader reader = new InputStreamReader( inputStream, contentType.getCharset() ) ) { return gson.fromJson( reader, JsonObject.class ); } } /* * Convert provided JSON to JsonElement, so that some Elasticsearch peculiarities (such as the fact that * single quotes are not accepted as a substitute for single quotes) can be worked around. * In tests, single quotes are way easier to include in JSON strings, because we don't have to escape them. */ private JsonElement toJsonElement(String jsonAsString) { return new JsonParser().parse( jsonAsString ); } }