Skip to content

Feature/http requester external client #654

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: v3
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<HttpResponse> 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<String, String> 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<org.apache.http.HttpResponse> toCompletableFuture(
Consumer<FutureCallback<org.apache.http.HttpResponse>> c) {
CompletableFuture<org.apache.http.HttpResponse> promise = new CompletableFuture<>();

c.accept(
new FutureCallback<org.apache.http.HttpResponse>() {
@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;
}
}
Loading