diff --git a/ChangeLog.md b/ChangeLog.md index d2871f4df..3bf444105 100755 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,5 +1,11 @@ # ChangeLog +## 3.6.1 + +### Feat + +- feat: add alternate constructor to ApacheHttpRequester to permit external management of the client + ## [3.6.0](https://github.com/algolia/algoliasearch-client-java-2/compare/3.5.0...3.6.0) (2019-11-06) ### Feat diff --git a/algoliasearch-apache/src/main/java/com/algolia/search/AbstractApacheHttpRequester.java b/algoliasearch-apache/src/main/java/com/algolia/search/AbstractApacheHttpRequester.java new file mode 100644 index 000000000..cef59761f --- /dev/null +++ b/algoliasearch-apache/src/main/java/com/algolia/search/AbstractApacheHttpRequester.java @@ -0,0 +1,201 @@ +package com.algolia.search; + +import com.algolia.search.exceptions.AlgoliaRuntimeException; +import com.algolia.search.models.HttpRequest; +import com.algolia.search.models.HttpResponse; +import com.algolia.search.util.HttpStatusCodeUtils; +import java.io.IOException; +import java.net.ConnectException; +import java.net.SocketTimeoutException; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeoutException; +import java.util.function.Consumer; +import javax.annotation.Nonnull; +import org.apache.http.*; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.entity.DeflateDecompressingEntity; +import org.apache.http.client.entity.GzipDecompressingEntity; +import org.apache.http.client.methods.*; +import org.apache.http.concurrent.FutureCallback; +import org.apache.http.conn.ConnectTimeoutException; +import org.apache.http.conn.ConnectionPoolTimeoutException; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.InputStreamEntity; +import org.apache.http.impl.nio.client.CloseableHttpAsyncClient; +import org.apache.http.util.EntityUtils; + +/** + * The Algolia http requester is a wrapper on top of the HttpAsyncClient of Apache. It's an + * implementation of {@link HttpRequester} It takes an {@link HttpRequest} as input. It returns an + * {@link HttpResponse}. + */ +public abstract class AbstractApacheHttpRequester implements HttpRequester { + + protected abstract CloseableHttpAsyncClient getAsyncHttpClient(); + + protected abstract ConfigBase getConfig(); + + protected abstract RequestConfig getRequestConfig(); + + /** + * Sends the http request asynchronously to the API If the request is time out it creates a new + * response object with timeout set to true Otherwise it throws a run time exception + * + * @param request the request to send + * @throws AlgoliaRuntimeException When an error occurred while sending the request + */ + public CompletableFuture performRequestAsync(HttpRequest request) { + HttpRequestBase requestToSend = buildRequest(request); + return toCompletableFuture(fc -> getAsyncHttpClient().execute(requestToSend, fc)) + .thenApplyAsync(this::buildResponse, getConfig().getExecutor()) + .exceptionally( + t -> { + if (t.getCause() instanceof ConnectTimeoutException + || t.getCause() instanceof SocketTimeoutException + || t.getCause() instanceof ConnectException + || t.getCause() instanceof TimeoutException + || t.getCause() instanceof ConnectionPoolTimeoutException + || t.getCause() instanceof NoHttpResponseException) { + return new HttpResponse(true); + } else if (t.getCause() instanceof HttpException) { + return new HttpResponse().setNetworkError(true); + } + throw new AlgoliaRuntimeException(t); + }); + } + + /** Closes the http client. */ + public abstract void close() throws IOException; + + /** + * Builds an Algolia response from the server response + * + * @param response The server response + */ + protected HttpResponse buildResponse(org.apache.http.HttpResponse response) { + try { + if (HttpStatusCodeUtils.isSuccess(response.getStatusLine().getStatusCode())) { + + HttpEntity entity = handleCompressedEntity(response.getEntity()); + + return new HttpResponse(response.getStatusLine().getStatusCode(), entity.getContent()); + } + return new HttpResponse( + response.getStatusLine().getStatusCode(), EntityUtils.toString(response.getEntity())); + } catch (IOException e) { + throw new AlgoliaRuntimeException(e); + } + } + + /** + * Builds an Apache HttpRequest from an Algolia Request object + * + * @param algoliaRequest The Algolia request object + */ + protected HttpRequestBase buildRequest(HttpRequest algoliaRequest) { + + switch (algoliaRequest.getMethod().toString()) { + case HttpGet.METHOD_NAME: + HttpGet get = new HttpGet(algoliaRequest.getUri().toString()); + get.setConfig(buildRequestConfig(algoliaRequest)); + return addHeaders(get, algoliaRequest.getHeaders()); + + case HttpDelete.METHOD_NAME: + HttpDelete delete = new HttpDelete(algoliaRequest.getUri().toString()); + delete.setConfig(buildRequestConfig(algoliaRequest)); + return addHeaders(delete, algoliaRequest.getHeaders()); + + case HttpPost.METHOD_NAME: + HttpPost post = new HttpPost(algoliaRequest.getUri().toString()); + if (algoliaRequest.getBody() != null) post.setEntity(addEntity(algoliaRequest)); + post.setConfig(buildRequestConfig(algoliaRequest)); + return addHeaders(post, algoliaRequest.getHeaders()); + + case HttpPut.METHOD_NAME: + HttpPut put = new HttpPut(algoliaRequest.getUri().toString()); + if (algoliaRequest.getBody() != null) put.setEntity(addEntity(algoliaRequest)); + put.setConfig(buildRequestConfig(algoliaRequest)); + return addHeaders(put, algoliaRequest.getHeaders()); + + case HttpPatch.METHOD_NAME: + HttpPatch patch = new HttpPatch(algoliaRequest.getUri().toString()); + if (algoliaRequest.getBody() != null) patch.setEntity(addEntity(algoliaRequest)); + patch.setConfig(buildRequestConfig(algoliaRequest)); + return addHeaders(patch, algoliaRequest.getHeaders()); + + default: + throw new UnsupportedOperationException( + "HTTP method not supported: " + algoliaRequest.getMethod().toString()); + } + } + + protected RequestConfig buildRequestConfig(HttpRequest algoliaRequest) { + return RequestConfig.copy(getRequestConfig()) + .setSocketTimeout(algoliaRequest.getTimeout()) + .build(); + } + + protected HttpRequestBase addHeaders(HttpRequestBase request, Map headers) { + headers.forEach(request::addHeader); + return request; + } + + protected HttpEntity addEntity(@Nonnull HttpRequest request) { + try { + InputStreamEntity entity = + new InputStreamEntity( + request.getBody(), request.getBody().available(), ContentType.APPLICATION_JSON); + + if (request.canCompress()) { + entity.setContentEncoding(Defaults.CONTENT_ENCODING_GZIP); + } + + return entity; + } catch (IOException e) { + throw new AlgoliaRuntimeException("Error while getting body's content length.", e); + } + } + + protected HttpEntity handleCompressedEntity(HttpEntity entity) { + + Header contentEncoding = entity.getContentEncoding(); + + if (contentEncoding != null) + for (HeaderElement e : contentEncoding.getElements()) { + if (Defaults.CONTENT_ENCODING_GZIP.equalsIgnoreCase(e.getName())) { + return new GzipDecompressingEntity(entity); + } + + if (Defaults.CONTENT_ENCODING_DEFLATE.equalsIgnoreCase(e.getName())) { + return new DeflateDecompressingEntity(entity); + } + } + + return entity; + } + + protected static CompletableFuture toCompletableFuture( + Consumer> c) { + CompletableFuture promise = new CompletableFuture<>(); + + c.accept( + new FutureCallback() { + @Override + public void completed(org.apache.http.HttpResponse t) { + promise.complete(t); + } + + @Override + public void failed(Exception e) { + promise.completeExceptionally(e); + } + + @Override + public void cancelled() { + promise.cancel(true); + } + }); + return promise; + } +} diff --git a/algoliasearch-apache/src/main/java/com/algolia/search/ApacheHttpRequester.java b/algoliasearch-apache/src/main/java/com/algolia/search/ApacheHttpRequester.java index 7e53ac3b7..bcdc36042 100644 --- a/algoliasearch-apache/src/main/java/com/algolia/search/ApacheHttpRequester.java +++ b/algoliasearch-apache/src/main/java/com/algolia/search/ApacheHttpRequester.java @@ -1,37 +1,19 @@ package com.algolia.search; -import com.algolia.search.exceptions.AlgoliaRuntimeException; import com.algolia.search.models.HttpRequest; import com.algolia.search.models.HttpResponse; -import com.algolia.search.util.HttpStatusCodeUtils; import java.io.IOException; -import java.net.ConnectException; -import java.net.SocketTimeoutException; -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeoutException; -import java.util.function.Consumer; import javax.annotation.Nonnull; -import org.apache.http.*; import org.apache.http.client.config.RequestConfig; -import org.apache.http.client.entity.DeflateDecompressingEntity; -import org.apache.http.client.entity.GzipDecompressingEntity; -import org.apache.http.client.methods.*; -import org.apache.http.concurrent.FutureCallback; -import org.apache.http.conn.ConnectTimeoutException; -import org.apache.http.conn.ConnectionPoolTimeoutException; -import org.apache.http.entity.ContentType; -import org.apache.http.entity.InputStreamEntity; import org.apache.http.impl.nio.client.CloseableHttpAsyncClient; import org.apache.http.impl.nio.client.HttpAsyncClients; -import org.apache.http.util.EntityUtils; /** * The Algolia http requester is a wrapper on top of the HttpAsyncClient of Apache. It's an * implementation of {@link HttpRequester} It takes an {@link HttpRequest} as input. It returns an * {@link HttpResponse}. */ -final class ApacheHttpRequester implements HttpRequester { +final class ApacheHttpRequester extends AbstractApacheHttpRequester { private final CloseableHttpAsyncClient asyncHttpClient; private final RequestConfig requestConfig; @@ -55,165 +37,23 @@ final class ApacheHttpRequester implements HttpRequester { asyncHttpClient.start(); } - /** - * Sends the http request asynchronously to the API If the request is time out it creates a new - * response object with timeout set to true Otherwise it throws a run time exception - * - * @param request the request to send - * @throws AlgoliaRuntimeException When an error occurred while sending the request - */ - public CompletableFuture performRequestAsync(HttpRequest request) { - HttpRequestBase requestToSend = buildRequest(request); - return toCompletableFuture(fc -> asyncHttpClient.execute(requestToSend, fc)) - .thenApplyAsync(this::buildResponse, config.getExecutor()) - .exceptionally( - t -> { - if (t.getCause() instanceof ConnectTimeoutException - || t.getCause() instanceof SocketTimeoutException - || t.getCause() instanceof ConnectException - || t.getCause() instanceof TimeoutException - || t.getCause() instanceof ConnectionPoolTimeoutException - || t.getCause() instanceof NoHttpResponseException) { - return new HttpResponse(true); - } else if (t.getCause() instanceof HttpException) { - return new HttpResponse().setNetworkError(true); - } - throw new AlgoliaRuntimeException(t); - }); - } - - /** Closes the http client. */ + @Override public void close() throws IOException { - asyncHttpClient.close(); - } - - /** - * Builds an Algolia response from the server response - * - * @param response The server response - */ - private HttpResponse buildResponse(org.apache.http.HttpResponse response) { - try { - if (HttpStatusCodeUtils.isSuccess(response.getStatusLine().getStatusCode())) { - - HttpEntity entity = handleCompressedEntity(response.getEntity()); - - return new HttpResponse(response.getStatusLine().getStatusCode(), entity.getContent()); - } - return new HttpResponse( - response.getStatusLine().getStatusCode(), EntityUtils.toString(response.getEntity())); - } catch (IOException e) { - throw new AlgoliaRuntimeException(e); - } - } - - /** - * Builds an Apache HttpRequest from an Algolia Request object - * - * @param algoliaRequest The Algolia request object - */ - private HttpRequestBase buildRequest(HttpRequest algoliaRequest) { - - switch (algoliaRequest.getMethod().toString()) { - case HttpGet.METHOD_NAME: - HttpGet get = new HttpGet(algoliaRequest.getUri().toString()); - get.setConfig(buildRequestConfig(algoliaRequest)); - return addHeaders(get, algoliaRequest.getHeaders()); - - case HttpDelete.METHOD_NAME: - HttpDelete delete = new HttpDelete(algoliaRequest.getUri().toString()); - delete.setConfig(buildRequestConfig(algoliaRequest)); - return addHeaders(delete, algoliaRequest.getHeaders()); - - case HttpPost.METHOD_NAME: - HttpPost post = new HttpPost(algoliaRequest.getUri().toString()); - if (algoliaRequest.getBody() != null) post.setEntity(addEntity(algoliaRequest)); - post.setConfig(buildRequestConfig(algoliaRequest)); - return addHeaders(post, algoliaRequest.getHeaders()); - - case HttpPut.METHOD_NAME: - HttpPut put = new HttpPut(algoliaRequest.getUri().toString()); - if (algoliaRequest.getBody() != null) put.setEntity(addEntity(algoliaRequest)); - put.setConfig(buildRequestConfig(algoliaRequest)); - return addHeaders(put, algoliaRequest.getHeaders()); - - case HttpPatch.METHOD_NAME: - HttpPatch patch = new HttpPatch(algoliaRequest.getUri().toString()); - if (algoliaRequest.getBody() != null) patch.setEntity(addEntity(algoliaRequest)); - patch.setConfig(buildRequestConfig(algoliaRequest)); - return addHeaders(patch, algoliaRequest.getHeaders()); - - default: - throw new UnsupportedOperationException( - "HTTP method not supported: " + algoliaRequest.getMethod().toString()); - } + getAsyncHttpClient().close(); } - private RequestConfig buildRequestConfig(HttpRequest algoliaRequest) { - return RequestConfig.copy(requestConfig).setSocketTimeout(algoliaRequest.getTimeout()).build(); + @Override + protected CloseableHttpAsyncClient getAsyncHttpClient() { + return asyncHttpClient; } - private HttpRequestBase addHeaders( - org.apache.http.client.methods.HttpRequestBase request, Map headers) { - headers.forEach(request::addHeader); - return request; + @Override + protected ConfigBase getConfig() { + return config; } - private HttpEntity addEntity(@Nonnull HttpRequest request) { - try { - InputStreamEntity entity = - new InputStreamEntity( - request.getBody(), request.getBody().available(), ContentType.APPLICATION_JSON); - - if (request.canCompress()) { - entity.setContentEncoding(Defaults.CONTENT_ENCODING_GZIP); - } - - return entity; - } catch (IOException e) { - throw new AlgoliaRuntimeException("Error while getting body's content length.", e); - } - } - - private static HttpEntity handleCompressedEntity(org.apache.http.HttpEntity entity) { - - Header contentEncoding = entity.getContentEncoding(); - - if (contentEncoding != null) - for (HeaderElement e : contentEncoding.getElements()) { - if (Defaults.CONTENT_ENCODING_GZIP.equalsIgnoreCase(e.getName())) { - return new GzipDecompressingEntity(entity); - } - - if (Defaults.CONTENT_ENCODING_DEFLATE.equalsIgnoreCase(e.getName())) { - return new DeflateDecompressingEntity(entity); - } - } - - return entity; - } - - private static CompletableFuture toCompletableFuture( - Consumer> c) { - CompletableFuture promise = new CompletableFuture<>(); - - c.accept( - new FutureCallback() { - @Override - public void completed(org.apache.http.HttpResponse t) { - promise.complete(t); - } - - @Override - public void failed(Exception e) { - promise.completeExceptionally(e); - } - - @Override - public void cancelled() { - promise.cancel(true); - } - }); - return promise; + @Override + protected RequestConfig getRequestConfig() { + return requestConfig; } } diff --git a/algoliasearch-apache/src/main/java/com/algolia/search/ExternalApacheHttpRequester.java b/algoliasearch-apache/src/main/java/com/algolia/search/ExternalApacheHttpRequester.java new file mode 100644 index 000000000..0dfe07c44 --- /dev/null +++ b/algoliasearch-apache/src/main/java/com/algolia/search/ExternalApacheHttpRequester.java @@ -0,0 +1,55 @@ +package com.algolia.search; + +import com.algolia.search.models.HttpRequest; +import com.algolia.search.models.HttpResponse; +import java.io.IOException; +import java.util.Objects; +import javax.annotation.Nonnull; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.impl.nio.client.CloseableHttpAsyncClient; + +/** + * The Algolia http requester is a wrapper on top of the HttpAsyncClient of Apache. It's an + * implementation of {@link HttpRequester} It takes an {@link HttpRequest} as input. It returns an + * {@link HttpResponse}. + */ +public class ExternalApacheHttpRequester extends AbstractApacheHttpRequester { + + private final CloseableHttpAsyncClient asyncHttpClient; + private final RequestConfig requestConfig; + private final ConfigBase config; + + public ExternalApacheHttpRequester( + @Nonnull ConfigBase config, @Nonnull CloseableHttpAsyncClient client) { + + this.config = config; + + requestConfig = + RequestConfig.custom() + .setConnectTimeout(config.getConnectTimeOut()) + .setContentCompressionEnabled(true) + .build(); + + asyncHttpClient = Objects.requireNonNull(client); + } + + @Override + public void close() throws IOException { + // noop + } + + @Override + protected CloseableHttpAsyncClient getAsyncHttpClient() { + return asyncHttpClient; + } + + @Override + protected ConfigBase getConfig() { + return config; + } + + @Override + protected RequestConfig getRequestConfig() { + return requestConfig; + } +} diff --git a/algoliasearch-core/src/main/java/com/algolia/search/models/indexing/Explain.java b/algoliasearch-core/src/main/java/com/algolia/search/models/indexing/Explain.java index 2b1df6284..8269a3435 100644 --- a/algoliasearch-core/src/main/java/com/algolia/search/models/indexing/Explain.java +++ b/algoliasearch-core/src/main/java/com/algolia/search/models/indexing/Explain.java @@ -1,7 +1,6 @@ package com.algolia.search.models.indexing; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.Map; public class Explain { @@ -26,5 +25,6 @@ public Explain setParams(Map params) { @JsonProperty("match") private QueryMatch queryMatch; - private Map params; + + private Map params; } diff --git a/pom.xml b/pom.xml index 42be329c6..5782b20bb 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 com.algolia algoliasearch - 3.6.0 + 3.6.1 pom