/* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch licenses this file to you under * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.elasticsearch.client; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.ActionRequest; import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.bulk.BulkRequest; import org.elasticsearch.action.bulk.BulkResponse; import org.elasticsearch.action.delete.DeleteRequest; import org.elasticsearch.action.delete.DeleteResponse; import org.elasticsearch.action.get.GetRequest; import org.elasticsearch.action.get.GetResponse; import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.index.IndexResponse; import org.elasticsearch.action.main.MainRequest; import org.elasticsearch.action.main.MainResponse; import org.elasticsearch.action.update.UpdateRequest; import org.elasticsearch.action.update.UpdateResponse; import org.elasticsearch.common.CheckedFunction; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.rest.BytesRestResponse; import org.elasticsearch.rest.RestStatus; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.function.Function; import java.util.stream.Stream; import static java.util.Collections.emptySet; import static java.util.Collections.singleton; import static java.util.stream.Collectors.toList; /** * High level REST client that wraps an instance of the low level {@link RestClient} and allows to build requests and read responses. * The provided {@link RestClient} is externally built and closed. * Can be sub-classed to expose additional client methods that make use of endpoints added to Elasticsearch through plugins, or to * add support for custom response sections, again added to Elasticsearch through plugins. */ public class RestHighLevelClient { private final RestClient client; private final NamedXContentRegistry registry; /** * Creates a {@link RestHighLevelClient} given the low level {@link RestClient} that it should use to perform requests. */ public RestHighLevelClient(RestClient restClient) { this(restClient, Collections.emptyList()); } /** * Creates a {@link RestHighLevelClient} given the low level {@link RestClient} that it should use to perform requests and * a list of entries that allow to parse custom response sections added to Elasticsearch through plugins. */ protected RestHighLevelClient(RestClient restClient, List<NamedXContentRegistry.Entry> namedXContentEntries) { this.client = Objects.requireNonNull(restClient); this.registry = new NamedXContentRegistry(Stream.of( getNamedXContents().stream(), namedXContentEntries.stream() ).flatMap(Function.identity()).collect(toList())); } /** * Executes a bulk request using the Bulk API * * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html">Bulk API on elastic.co</a> */ public BulkResponse bulk(BulkRequest bulkRequest, Header... headers) throws IOException { return performRequestAndParseEntity(bulkRequest, Request::bulk, BulkResponse::fromXContent, emptySet(), headers); } /** * Asynchronously executes a bulk request using the Bulk API * * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html">Bulk API on elastic.co</a> */ public void bulkAsync(BulkRequest bulkRequest, ActionListener<BulkResponse> listener, Header... headers) { performRequestAsyncAndParseEntity(bulkRequest, Request::bulk, BulkResponse::fromXContent, listener, emptySet(), headers); } /** * Pings the remote Elasticsearch cluster and returns true if the ping succeeded, false otherwise */ public boolean ping(Header... headers) throws IOException { return performRequest(new MainRequest(), (request) -> Request.ping(), RestHighLevelClient::convertExistsResponse, emptySet(), headers); } /** * Get the cluster info otherwise provided when sending an HTTP request to port 9200 */ public MainResponse info(Header... headers) throws IOException { return performRequestAndParseEntity(new MainRequest(), (request) -> Request.info(), MainResponse::fromXContent, emptySet(), headers); } /** * Retrieves a document by id using the Get API * * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-get.html">Get API on elastic.co</a> */ public GetResponse get(GetRequest getRequest, Header... headers) throws IOException { return performRequestAndParseEntity(getRequest, Request::get, GetResponse::fromXContent, singleton(404), headers); } /** * Asynchronously retrieves a document by id using the Get API * * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-get.html">Get API on elastic.co</a> */ public void getAsync(GetRequest getRequest, ActionListener<GetResponse> listener, Header... headers) { performRequestAsyncAndParseEntity(getRequest, Request::get, GetResponse::fromXContent, listener, singleton(404), headers); } /** * Checks for the existence of a document. Returns true if it exists, false otherwise * * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-get.html">Get API on elastic.co</a> */ public boolean exists(GetRequest getRequest, Header... headers) throws IOException { return performRequest(getRequest, Request::exists, RestHighLevelClient::convertExistsResponse, emptySet(), headers); } /** * Asynchronously checks for the existence of a document. Returns true if it exists, false otherwise * * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-get.html">Get API on elastic.co</a> */ public void existsAsync(GetRequest getRequest, ActionListener<Boolean> listener, Header... headers) { performRequestAsync(getRequest, Request::exists, RestHighLevelClient::convertExistsResponse, listener, emptySet(), headers); } /** * Index a document using the Index API * * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html">Index API on elastic.co</a> */ public IndexResponse index(IndexRequest indexRequest, Header... headers) throws IOException { return performRequestAndParseEntity(indexRequest, Request::index, IndexResponse::fromXContent, emptySet(), headers); } /** * Asynchronously index a document using the Index API * * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html">Index API on elastic.co</a> */ public void indexAsync(IndexRequest indexRequest, ActionListener<IndexResponse> listener, Header... headers) { performRequestAsyncAndParseEntity(indexRequest, Request::index, IndexResponse::fromXContent, listener, emptySet(), headers); } /** * Updates a document using the Update API * <p> * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update.html">Update API on elastic.co</a> */ public UpdateResponse update(UpdateRequest updateRequest, Header... headers) throws IOException { return performRequestAndParseEntity(updateRequest, Request::update, UpdateResponse::fromXContent, emptySet(), headers); } /** * Asynchronously updates a document using the Update API * <p> * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update.html">Update API on elastic.co</a> */ public void updateAsync(UpdateRequest updateRequest, ActionListener<UpdateResponse> listener, Header... headers) { performRequestAsyncAndParseEntity(updateRequest, Request::update, UpdateResponse::fromXContent, listener, emptySet(), headers); } /** * Deletes a document by id using the Delete api * * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete.html">Delete API on elastic.co</a> */ public DeleteResponse delete(DeleteRequest deleteRequest, Header... headers) throws IOException { return performRequestAndParseEntity(deleteRequest, Request::delete, DeleteResponse::fromXContent, Collections.singleton(404), headers); } /** * Asynchronously deletes a document by id using the Delete api * * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete.html">Delete API on elastic.co</a> */ public void deleteAsync(DeleteRequest deleteRequest, ActionListener<DeleteResponse> listener, Header... headers) { performRequestAsyncAndParseEntity(deleteRequest, Request::delete, DeleteResponse::fromXContent, listener, Collections.singleton(404), headers); } private <Req extends ActionRequest, Resp> Resp performRequestAndParseEntity(Req request, CheckedFunction<Req, Request, IOException> requestConverter, CheckedFunction<XContentParser, Resp, IOException> entityParser, Set<Integer> ignores, Header... headers) throws IOException { return performRequest(request, requestConverter, (response) -> parseEntity(response.getEntity(), entityParser), ignores, headers); } <Req extends ActionRequest, Resp> Resp performRequest(Req request, CheckedFunction<Req, Request, IOException> requestConverter, CheckedFunction<Response, Resp, IOException> responseConverter, Set<Integer> ignores, Header... headers) throws IOException { ActionRequestValidationException validationException = request.validate(); if (validationException != null) { throw validationException; } Request req = requestConverter.apply(request); Response response; try { response = client.performRequest(req.method, req.endpoint, req.params, req.entity, headers); } catch (ResponseException e) { if (ignores.contains(e.getResponse().getStatusLine().getStatusCode())) { try { return responseConverter.apply(e.getResponse()); } catch (Exception innerException) { throw parseResponseException(e); } } throw parseResponseException(e); } try { return responseConverter.apply(response); } catch(Exception e) { throw new IOException("Unable to parse response body for " + response, e); } } private <Req extends ActionRequest, Resp> void performRequestAsyncAndParseEntity(Req request, CheckedFunction<Req, Request, IOException> requestConverter, CheckedFunction<XContentParser, Resp, IOException> entityParser, ActionListener<Resp> listener, Set<Integer> ignores, Header... headers) { performRequestAsync(request, requestConverter, (response) -> parseEntity(response.getEntity(), entityParser), listener, ignores, headers); } <Req extends ActionRequest, Resp> void performRequestAsync(Req request, CheckedFunction<Req, Request, IOException> requestConverter, CheckedFunction<Response, Resp, IOException> responseConverter, ActionListener<Resp> listener, Set<Integer> ignores, Header... headers) { ActionRequestValidationException validationException = request.validate(); if (validationException != null) { listener.onFailure(validationException); return; } Request req; try { req = requestConverter.apply(request); } catch (Exception e) { listener.onFailure(e); return; } ResponseListener responseListener = wrapResponseListener(responseConverter, listener, ignores); client.performRequestAsync(req.method, req.endpoint, req.params, req.entity, responseListener, headers); } <Resp> ResponseListener wrapResponseListener(CheckedFunction<Response, Resp, IOException> responseConverter, ActionListener<Resp> actionListener, Set<Integer> ignores) { return new ResponseListener() { @Override public void onSuccess(Response response) { try { actionListener.onResponse(responseConverter.apply(response)); } catch(Exception e) { IOException ioe = new IOException("Unable to parse response body for " + response, e); onFailure(ioe); } } @Override public void onFailure(Exception exception) { if (exception instanceof ResponseException) { ResponseException responseException = (ResponseException) exception; Response response = responseException.getResponse(); if (ignores.contains(response.getStatusLine().getStatusCode())) { try { actionListener.onResponse(responseConverter.apply(response)); } catch (Exception innerException) { //the exception is ignored as we now try to parse the response as an error. //this covers cases like get where 404 can either be a valid document not found response, //or an error for which parsing is completely different. We try to consider the 404 response as a valid one //first. If parsing of the response breaks, we fall back to parsing it as an error. actionListener.onFailure(parseResponseException(responseException)); } } else { actionListener.onFailure(parseResponseException(responseException)); } } else { actionListener.onFailure(exception); } } }; } /** * Converts a {@link ResponseException} obtained from the low level REST client into an {@link ElasticsearchException}. * If a response body was returned, tries to parse it as an error returned from Elasticsearch. * If no response body was returned or anything goes wrong while parsing the error, returns a new {@link ElasticsearchStatusException} * that wraps the original {@link ResponseException}. The potential exception obtained while parsing is added to the returned * exception as a suppressed exception. This method is guaranteed to not throw any exception eventually thrown while parsing. */ ElasticsearchStatusException parseResponseException(ResponseException responseException) { Response response = responseException.getResponse(); HttpEntity entity = response.getEntity(); ElasticsearchStatusException elasticsearchException; if (entity == null) { elasticsearchException = new ElasticsearchStatusException( responseException.getMessage(), RestStatus.fromCode(response.getStatusLine().getStatusCode()), responseException); } else { try { elasticsearchException = parseEntity(entity, BytesRestResponse::errorFromXContent); elasticsearchException.addSuppressed(responseException); } catch (Exception e) { RestStatus restStatus = RestStatus.fromCode(response.getStatusLine().getStatusCode()); elasticsearchException = new ElasticsearchStatusException("Unable to parse response body", restStatus, responseException); elasticsearchException.addSuppressed(e); } } return elasticsearchException; } <Resp> Resp parseEntity( HttpEntity entity, CheckedFunction<XContentParser, Resp, IOException> entityParser) throws IOException { if (entity == null) { throw new IllegalStateException("Response body expected but not returned"); } if (entity.getContentType() == null) { throw new IllegalStateException("Elasticsearch didn't return the [Content-Type] header, unable to parse response body"); } XContentType xContentType = XContentType.fromMediaTypeOrFormat(entity.getContentType().getValue()); if (xContentType == null) { throw new IllegalStateException("Unsupported Content-Type: " + entity.getContentType().getValue()); } try (XContentParser parser = xContentType.xContent().createParser(registry, entity.getContent())) { return entityParser.apply(parser); } } static boolean convertExistsResponse(Response response) { return response.getStatusLine().getStatusCode() == 200; } static List<NamedXContentRegistry.Entry> getNamedXContents() { List<NamedXContentRegistry.Entry> namedXContents = new ArrayList<>(); //namedXContents.add(new NamedXContentRegistry.Entry(Aggregation.class, new ParseField("sterms"), StringTerms::fromXContent)); return namedXContents; } }