diff --git a/pom.xml b/pom.xml
index 6d8a51983..f742959c6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -22,6 +22,7 @@
scribejava-httpclient-ning
scribejava-httpclient-okhttp
scribejava-httpclient-apache
+ scribejava-httpclient-apache5
scribejava-httpclient-armeria
diff --git a/scribejava-httpclient-apache5/pom.xml b/scribejava-httpclient-apache5/pom.xml
new file mode 100644
index 000000000..eb8935d80
--- /dev/null
+++ b/scribejava-httpclient-apache5/pom.xml
@@ -0,0 +1,49 @@
+
+
+ 4.0.0
+
+
+ com.github.scribejava
+ scribejava
+ 8.3.2-SNAPSHOT
+ ../pom.xml
+
+
+ com.github.scribejava
+ scribejava-httpclient-apache5
+ ScribeJava Apache HttpComponents HttpClient 5 support
+ jar
+
+
+
+ com.github.scribejava
+ scribejava-core
+ ${project.version}
+
+
+ org.apache.httpcomponents.client5
+ httpclient5
+ 5.1
+
+
+ com.github.scribejava
+ scribejava-core
+ ${project.version}
+ test-jar
+ test
+
+
+
+
+
+
+ org.apache.felix
+ maven-bundle-plugin
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+
+
+
+
diff --git a/scribejava-httpclient-apache5/src/main/java/com/github/scribejava/httpclient/apache5/ApacheHttpClient5.java b/scribejava-httpclient-apache5/src/main/java/com/github/scribejava/httpclient/apache5/ApacheHttpClient5.java
new file mode 100644
index 000000000..8d6cddfc1
--- /dev/null
+++ b/scribejava-httpclient-apache5/src/main/java/com/github/scribejava/httpclient/apache5/ApacheHttpClient5.java
@@ -0,0 +1,136 @@
+package com.github.scribejava.httpclient.apache5;
+
+import com.github.scribejava.core.httpclient.AbstractAsyncOnlyHttpClient;
+import com.github.scribejava.core.httpclient.multipart.MultipartPayload;
+import com.github.scribejava.core.model.OAuthAsyncRequestCallback;
+import com.github.scribejava.core.model.OAuthConstants;
+import com.github.scribejava.core.model.OAuthRequest;
+import com.github.scribejava.core.model.Verb;
+import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
+import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder;
+import org.apache.hc.core5.http.nio.AsyncEntityProducer;
+import org.apache.hc.core5.http.nio.entity.BasicAsyncEntityProducer;
+import org.apache.hc.core5.http.nio.entity.FileEntityProducer;
+import org.apache.hc.core5.http.nio.entity.StringAsyncEntityProducer;
+import org.apache.hc.core5.http.nio.support.AsyncRequestBuilder;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+public class ApacheHttpClient5 extends AbstractAsyncOnlyHttpClient {
+
+ private static final int ENTITY_CONSUMER_BUFFER_SIZE = 4096;
+ private static final int ENTITY_CONSUMER_THREADS = 5;
+
+ private final CloseableHttpAsyncClient client;
+ private final ExecutorService entityConsumerExecutor;
+
+ public ApacheHttpClient5() {
+ this(ApacheHttpClient5Config.defaultConfig());
+ }
+
+ public ApacheHttpClient5(ApacheHttpClient5Config config) {
+ this(config.getHttpAsyncClientBuilder());
+ }
+
+ public ApacheHttpClient5(HttpAsyncClientBuilder builder) {
+ this(builder.build());
+ }
+
+ public ApacheHttpClient5(CloseableHttpAsyncClient client) {
+ this.client = client;
+ this.client.start();
+
+ entityConsumerExecutor = Executors.newFixedThreadPool(ENTITY_CONSUMER_THREADS);
+ }
+
+ @Override
+ public void close() throws IOException {
+ client.close();
+ }
+
+ @Override
+ public Future executeAsync(String userAgent, Map headers, Verb httpVerb, String completeUrl,
+ byte[] bodyContents, OAuthAsyncRequestCallback callback, OAuthRequest.ResponseConverter converter) {
+ final AsyncEntityProducer entity = bodyContents == null ? null : new BasicAsyncEntityProducer(bodyContents);
+ return doExecuteAsync(userAgent, headers, httpVerb, completeUrl, entity, callback, converter);
+ }
+
+ @Override
+ public Future executeAsync(String userAgent, Map headers, Verb httpVerb, String completeUrl,
+ MultipartPayload bodyContents, OAuthAsyncRequestCallback callback,
+ OAuthRequest.ResponseConverter converter) {
+
+ throw new UnsupportedOperationException("ApacheHttpClient does not support MultipartPayload yet.");
+ }
+
+ @Override
+ public Future executeAsync(String userAgent, Map headers, Verb httpVerb, String completeUrl,
+ String bodyContents, OAuthAsyncRequestCallback callback, OAuthRequest.ResponseConverter converter) {
+ final AsyncEntityProducer entity = bodyContents == null ? null : new StringAsyncEntityProducer(bodyContents);
+ return doExecuteAsync(userAgent, headers, httpVerb, completeUrl, entity, callback, converter);
+ }
+
+ @Override
+ public Future executeAsync(String userAgent, Map headers, Verb httpVerb, String completeUrl,
+ File bodyContents, OAuthAsyncRequestCallback callback, OAuthRequest.ResponseConverter converter) {
+ final AsyncEntityProducer entity = bodyContents == null ? null : new FileEntityProducer(bodyContents);
+ return doExecuteAsync(userAgent, headers, httpVerb, completeUrl, entity, callback, converter);
+ }
+
+ private Future doExecuteAsync(String userAgent, Map headers, Verb httpVerb,
+ String completeUrl, AsyncEntityProducer entityProducer, OAuthAsyncRequestCallback callback,
+ OAuthRequest.ResponseConverter converter) {
+ final AsyncRequestBuilder builder = getRequestBuilder(httpVerb);
+ builder.setUri(completeUrl);
+
+ if (httpVerb.isPermitBody()) {
+ if (!headers.containsKey(CONTENT_TYPE)) {
+ builder.addHeader(CONTENT_TYPE, DEFAULT_CONTENT_TYPE);
+ }
+ builder.setEntity(entityProducer);
+ }
+
+ for (Map.Entry header : headers.entrySet()) {
+ builder.addHeader(header.getKey(), header.getValue());
+ }
+
+ if (userAgent != null) {
+ builder.setHeader(OAuthConstants.USER_AGENT_HEADER_NAME, userAgent);
+ }
+
+ final AsyncHttpEntityConsumer entityConsumer = new AsyncHttpEntityConsumer(
+ ENTITY_CONSUMER_BUFFER_SIZE, entityConsumerExecutor);
+ final ResponseWithEntityConsumer responseConsumer = new ResponseWithEntityConsumer(entityConsumer);
+ final OAuthAsyncCompletionHandler handler = new OAuthAsyncCompletionHandler<>(callback, converter);
+ final Future future = client.execute(builder.build(), responseConsumer, handler);
+ return new ApacheHttpFuture<>(future, handler);
+ }
+
+ private static AsyncRequestBuilder getRequestBuilder(Verb httpVerb) {
+ switch (httpVerb) {
+ case GET:
+ return AsyncRequestBuilder.get();
+ case PUT:
+ return AsyncRequestBuilder.put();
+ case DELETE:
+ return AsyncRequestBuilder.delete();
+ case HEAD:
+ return AsyncRequestBuilder.head();
+ case POST:
+ return AsyncRequestBuilder.post();
+ case PATCH:
+ return AsyncRequestBuilder.patch();
+ case TRACE:
+ return AsyncRequestBuilder.trace();
+ case OPTIONS:
+ return AsyncRequestBuilder.options();
+ default:
+ throw new IllegalArgumentException("message build error: unknown verb type");
+ }
+ }
+}
diff --git a/scribejava-httpclient-apache5/src/main/java/com/github/scribejava/httpclient/apache5/ApacheHttpClient5Config.java b/scribejava-httpclient-apache5/src/main/java/com/github/scribejava/httpclient/apache5/ApacheHttpClient5Config.java
new file mode 100644
index 000000000..e570823c0
--- /dev/null
+++ b/scribejava-httpclient-apache5/src/main/java/com/github/scribejava/httpclient/apache5/ApacheHttpClient5Config.java
@@ -0,0 +1,26 @@
+package com.github.scribejava.httpclient.apache5;
+
+import com.github.scribejava.core.httpclient.HttpClientConfig;
+import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder;
+
+public class ApacheHttpClient5Config implements HttpClientConfig {
+
+ private final HttpAsyncClientBuilder httpAsyncClientBuilder;
+
+ public ApacheHttpClient5Config(HttpAsyncClientBuilder httpAsyncClientBuilder) {
+ this.httpAsyncClientBuilder = httpAsyncClientBuilder;
+ }
+
+ public HttpAsyncClientBuilder getHttpAsyncClientBuilder() {
+ return httpAsyncClientBuilder;
+ }
+
+ @Override
+ public HttpClientConfig createDefaultConfig() {
+ return defaultConfig();
+ }
+
+ public static ApacheHttpClient5Config defaultConfig() {
+ return new ApacheHttpClient5Config(HttpAsyncClientBuilder.create());
+ }
+}
diff --git a/scribejava-httpclient-apache5/src/main/java/com/github/scribejava/httpclient/apache5/ApacheHttpClient5Provider.java b/scribejava-httpclient-apache5/src/main/java/com/github/scribejava/httpclient/apache5/ApacheHttpClient5Provider.java
new file mode 100644
index 000000000..d66a2550e
--- /dev/null
+++ b/scribejava-httpclient-apache5/src/main/java/com/github/scribejava/httpclient/apache5/ApacheHttpClient5Provider.java
@@ -0,0 +1,16 @@
+package com.github.scribejava.httpclient.apache5;
+
+import com.github.scribejava.core.httpclient.HttpClient;
+import com.github.scribejava.core.httpclient.HttpClientConfig;
+import com.github.scribejava.core.httpclient.HttpClientProvider;
+
+public class ApacheHttpClient5Provider implements HttpClientProvider {
+
+ @Override
+ public HttpClient createClient(HttpClientConfig httpClientConfig) {
+ if (httpClientConfig instanceof ApacheHttpClient5Config) {
+ return new ApacheHttpClient5((ApacheHttpClient5Config) httpClientConfig);
+ }
+ return null;
+ }
+}
diff --git a/scribejava-httpclient-apache5/src/main/java/com/github/scribejava/httpclient/apache5/ApacheHttpFuture.java b/scribejava-httpclient-apache5/src/main/java/com/github/scribejava/httpclient/apache5/ApacheHttpFuture.java
new file mode 100644
index 000000000..41d27a6c1
--- /dev/null
+++ b/scribejava-httpclient-apache5/src/main/java/com/github/scribejava/httpclient/apache5/ApacheHttpFuture.java
@@ -0,0 +1,42 @@
+package com.github.scribejava.httpclient.apache5;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+class ApacheHttpFuture implements Future {
+
+ private final Future future;
+ private final OAuthAsyncCompletionHandler handler;
+
+ ApacheHttpFuture(Future future, OAuthAsyncCompletionHandler handler) {
+ this.future = future;
+ this.handler = handler;
+ }
+
+ @Override
+ public boolean cancel(boolean mayInterruptIfRunning) {
+ return future.cancel(mayInterruptIfRunning);
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return future.isCancelled();
+ }
+
+ @Override
+ public boolean isDone() {
+ return future.isDone();
+ }
+
+ @Override
+ public T get() throws InterruptedException, ExecutionException {
+ return handler.getResult();
+ }
+
+ @Override
+ public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
+ return handler.getResult(timeout, unit);
+ }
+}
diff --git a/scribejava-httpclient-apache5/src/main/java/com/github/scribejava/httpclient/apache5/AsyncHttpEntityConsumer.java b/scribejava-httpclient-apache5/src/main/java/com/github/scribejava/httpclient/apache5/AsyncHttpEntityConsumer.java
new file mode 100644
index 000000000..10630a90c
--- /dev/null
+++ b/scribejava-httpclient-apache5/src/main/java/com/github/scribejava/httpclient/apache5/AsyncHttpEntityConsumer.java
@@ -0,0 +1,26 @@
+package com.github.scribejava.httpclient.apache5;
+
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.HttpEntity;
+import org.apache.hc.core5.http.io.entity.InputStreamEntity;
+import org.apache.hc.core5.http.nio.support.classic.AbstractClassicEntityConsumer;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.concurrent.Executor;
+
+/**
+ * Compatibility with classic HttpClient API:
+ * Consumes the async response as an input stream and creates an {@link HttpEntity}.
+ */
+public class AsyncHttpEntityConsumer extends AbstractClassicEntityConsumer {
+
+ public AsyncHttpEntityConsumer(int initialBufferSize, Executor executor) {
+ super(initialBufferSize, executor);
+ }
+
+ @Override
+ protected HttpEntity consumeData(ContentType contentType, InputStream inputStream) throws IOException {
+ return new InputStreamEntity(inputStream, contentType);
+ }
+}
diff --git a/scribejava-httpclient-apache5/src/main/java/com/github/scribejava/httpclient/apache5/OAuthAsyncCompletionHandler.java b/scribejava-httpclient-apache5/src/main/java/com/github/scribejava/httpclient/apache5/OAuthAsyncCompletionHandler.java
new file mode 100644
index 000000000..ecd4eeb3f
--- /dev/null
+++ b/scribejava-httpclient-apache5/src/main/java/com/github/scribejava/httpclient/apache5/OAuthAsyncCompletionHandler.java
@@ -0,0 +1,108 @@
+package com.github.scribejava.httpclient.apache5;
+
+import com.github.scribejava.core.model.OAuthAsyncRequestCallback;
+import com.github.scribejava.core.model.OAuthRequest.ResponseConverter;
+import com.github.scribejava.core.model.Response;
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.HttpEntity;
+import org.apache.hc.core5.http.HttpResponse;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+class OAuthAsyncCompletionHandler implements FutureCallback {
+
+ private final OAuthAsyncRequestCallback callback;
+ private final ResponseConverter converter;
+ private final CountDownLatch latch;
+ private T result;
+ private Exception exception;
+
+ OAuthAsyncCompletionHandler(OAuthAsyncRequestCallback callback, ResponseConverter converter) {
+ this.callback = callback;
+ this.converter = converter;
+ this.latch = new CountDownLatch(1);
+ }
+
+ @Override
+ public void completed(ResponseWithEntity responseWithEntity) {
+ try {
+ final HttpResponse httpResponse = responseWithEntity.getResponse();
+
+ final Map headersMap = new HashMap<>();
+ for (Header header : httpResponse.getHeaders()) {
+ headersMap.put(header.getName(), header.getValue());
+ }
+
+ final HttpEntity entity = responseWithEntity.getEntity();
+ final InputStream contentStream = entity == null ? null : entity.getContent();
+ final Response response = new Response(httpResponse.getCode(), httpResponse.getReasonPhrase(), headersMap,
+ contentStream, contentStream);
+
+ @SuppressWarnings("unchecked")
+ final T t = converter == null ? (T) response : converter.convert(response);
+ result = t;
+ if (callback != null) {
+ callback.onCompleted(result);
+ }
+ } catch (IOException | RuntimeException e) {
+ exception = e;
+ if (callback != null) {
+ callback.onThrowable(e);
+ }
+ } finally {
+ latch.countDown();
+ }
+ }
+
+ @Override
+ public void failed(Exception e) {
+ exception = e;
+ try {
+ if (callback != null) {
+ callback.onThrowable(e);
+ }
+ } finally {
+ latch.countDown();
+ }
+ }
+
+ @Override
+ public void cancelled() {
+ exception = new CancellationException();
+ try {
+ if (callback != null) {
+ callback.onThrowable(exception);
+ }
+ } finally {
+ latch.countDown();
+ }
+ }
+
+ public T getResult() throws InterruptedException, ExecutionException {
+ latch.await();
+ if (exception != null) {
+ throw new ExecutionException(exception);
+ }
+ return result;
+ }
+
+ public T getResult(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
+
+ if (!latch.await(timeout, unit)) {
+ throw new TimeoutException();
+ }
+ if (exception != null) {
+ throw new ExecutionException(exception);
+ }
+ return result;
+ }
+}
diff --git a/scribejava-httpclient-apache5/src/main/java/com/github/scribejava/httpclient/apache5/ResponseWithEntity.java b/scribejava-httpclient-apache5/src/main/java/com/github/scribejava/httpclient/apache5/ResponseWithEntity.java
new file mode 100644
index 000000000..ece5bace9
--- /dev/null
+++ b/scribejava-httpclient-apache5/src/main/java/com/github/scribejava/httpclient/apache5/ResponseWithEntity.java
@@ -0,0 +1,23 @@
+package com.github.scribejava.httpclient.apache5;
+
+import org.apache.hc.core5.http.HttpEntity;
+import org.apache.hc.core5.http.HttpResponse;
+
+class ResponseWithEntity {
+
+ private final HttpResponse response;
+ private final HttpEntity entity;
+
+ ResponseWithEntity(HttpResponse response, HttpEntity entity) {
+ this.response = response;
+ this.entity = entity;
+ }
+
+ public HttpResponse getResponse() {
+ return response;
+ }
+
+ public HttpEntity getEntity() {
+ return entity;
+ }
+}
diff --git a/scribejava-httpclient-apache5/src/main/java/com/github/scribejava/httpclient/apache5/ResponseWithEntityConsumer.java b/scribejava-httpclient-apache5/src/main/java/com/github/scribejava/httpclient/apache5/ResponseWithEntityConsumer.java
new file mode 100644
index 000000000..ca503d443
--- /dev/null
+++ b/scribejava-httpclient-apache5/src/main/java/com/github/scribejava/httpclient/apache5/ResponseWithEntityConsumer.java
@@ -0,0 +1,36 @@
+package com.github.scribejava.httpclient.apache5;
+
+import org.apache.hc.core5.function.Supplier;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.HttpEntity;
+import org.apache.hc.core5.http.HttpException;
+import org.apache.hc.core5.http.HttpResponse;
+import org.apache.hc.core5.http.nio.AsyncEntityConsumer;
+import org.apache.hc.core5.http.nio.support.AbstractAsyncResponseConsumer;
+import org.apache.hc.core5.http.protocol.HttpContext;
+
+import java.io.IOException;
+
+/**
+ * Implementation of an {@link AsyncEntityConsumer}
+ * Produces a {@link ResponseWithEntity} containing the HTTP response and entity.
+ */
+public class ResponseWithEntityConsumer extends AbstractAsyncResponseConsumer {
+
+ public ResponseWithEntityConsumer(Supplier> dataConsumerSupplier) {
+ super(dataConsumerSupplier);
+ }
+
+ public ResponseWithEntityConsumer(AsyncEntityConsumer dataConsumer) {
+ super(dataConsumer);
+ }
+
+ @Override
+ protected ResponseWithEntity buildResult(HttpResponse response, HttpEntity entity, ContentType contentType) {
+ return new ResponseWithEntity(response, entity);
+ }
+
+ @Override
+ public void informationResponse(HttpResponse response, HttpContext context) throws HttpException, IOException {
+ }
+}
diff --git a/scribejava-httpclient-apache5/src/main/resources/META-INF/services/com.github.scribejava.core.httpclient.HttpClientProvider b/scribejava-httpclient-apache5/src/main/resources/META-INF/services/com.github.scribejava.core.httpclient.HttpClientProvider
new file mode 100644
index 000000000..53eda14a5
--- /dev/null
+++ b/scribejava-httpclient-apache5/src/main/resources/META-INF/services/com.github.scribejava.core.httpclient.HttpClientProvider
@@ -0,0 +1 @@
+com.github.scribejava.httpclient.apache5.ApacheHttpClient5Provider
diff --git a/scribejava-httpclient-apache5/src/test/java/com/github/scribejava/httpclient/apache5/ApacheHttpClient5Test.java b/scribejava-httpclient-apache5/src/test/java/com/github/scribejava/httpclient/apache5/ApacheHttpClient5Test.java
new file mode 100644
index 000000000..bd30a8233
--- /dev/null
+++ b/scribejava-httpclient-apache5/src/test/java/com/github/scribejava/httpclient/apache5/ApacheHttpClient5Test.java
@@ -0,0 +1,13 @@
+package com.github.scribejava.httpclient.apache5;
+
+import com.github.scribejava.core.AbstractClientTest;
+import com.github.scribejava.core.httpclient.HttpClient;
+
+public class ApacheHttpClient5Test extends AbstractClientTest {
+
+ @Override
+ protected HttpClient createNewClient() {
+ return new ApacheHttpClient5();
+ }
+
+}
diff --git a/scribejava-httpclient-apache5/src/test/java/com/github/scribejava/httpclient/apache5/OAuthAsyncCompletionHandlerTest.java b/scribejava-httpclient-apache5/src/test/java/com/github/scribejava/httpclient/apache5/OAuthAsyncCompletionHandlerTest.java
new file mode 100644
index 000000000..914c09803
--- /dev/null
+++ b/scribejava-httpclient-apache5/src/test/java/com/github/scribejava/httpclient/apache5/OAuthAsyncCompletionHandlerTest.java
@@ -0,0 +1,180 @@
+package com.github.scribejava.httpclient.apache5;
+
+import com.github.scribejava.core.exceptions.OAuthException;
+import com.github.scribejava.core.model.OAuthAsyncRequestCallback;
+import com.github.scribejava.core.model.OAuthRequest;
+import com.github.scribejava.core.model.Response;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.HttpResponse;
+import org.apache.hc.core5.http.io.entity.BasicHttpEntity;
+import org.apache.hc.core5.http.message.BasicHttpResponse;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.function.ThrowingRunnable;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+public class OAuthAsyncCompletionHandlerTest {
+
+ private static final AllGoodResponseConverter ALL_GOOD_RESPONSE_CONVERTER = new AllGoodResponseConverter();
+ private static final ExceptionResponseConverter EXCEPTION_RESPONSE_CONVERTER = new ExceptionResponseConverter();
+ private static final OAuthExceptionResponseConverter OAUTH_EXCEPTION_RESPONSE_CONVERTER
+ = new OAuthExceptionResponseConverter();
+
+ private OAuthAsyncCompletionHandler handler;
+ private TestCallback callback;
+
+ private static class TestCallback implements OAuthAsyncRequestCallback {
+
+ private Throwable throwable;
+ private String response;
+
+ @Override
+ public void onCompleted(String response) {
+ this.response = response;
+ }
+
+ @Override
+ public void onThrowable(Throwable throwable) {
+ this.throwable = throwable;
+ }
+
+ public Throwable getThrowable() {
+ return throwable;
+ }
+
+ public String getResponse() {
+ return response;
+ }
+
+ }
+
+ @Before
+ public void setUp() {
+ callback = new TestCallback();
+ }
+
+ @Test
+ public void shouldReleaseLatchOnSuccess() throws Exception {
+ handler = new OAuthAsyncCompletionHandler<>(callback, ALL_GOOD_RESPONSE_CONVERTER);
+ final HttpResponse response = new BasicHttpResponse(200, "ok");
+ final BasicHttpEntity entity = new BasicHttpEntity(
+ new ByteArrayInputStream(new byte[0]), ContentType.DEFAULT_BINARY);
+
+ handler.completed(new ResponseWithEntity(response, entity));
+ assertNotNull(callback.getResponse());
+ assertNull(callback.getThrowable());
+ // verify latch is released
+ assertEquals("All good", handler.getResult());
+ }
+
+ @Test
+ public void shouldReleaseLatchOnIOException() {
+ handler = new OAuthAsyncCompletionHandler<>(callback, EXCEPTION_RESPONSE_CONVERTER);
+ final HttpResponse response = new BasicHttpResponse(200, "ok");
+ final BasicHttpEntity entity = new BasicHttpEntity(
+ new ByteArrayInputStream(new byte[0]), ContentType.DEFAULT_BINARY);
+
+ handler.completed(new ResponseWithEntity(response, entity));
+ assertNull(callback.getResponse());
+ assertNotNull(callback.getThrowable());
+ assertTrue(callback.getThrowable() instanceof IOException);
+ // verify latch is released
+ assertThrows(ExecutionException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ handler.getResult();
+ }
+ });
+ }
+
+ @Test
+ public void shouldReportOAuthException() {
+ handler = new OAuthAsyncCompletionHandler<>(callback, OAUTH_EXCEPTION_RESPONSE_CONVERTER);
+ final HttpResponse response = new BasicHttpResponse(200, "ok");
+ final BasicHttpEntity entity = new BasicHttpEntity(
+ new ByteArrayInputStream(new byte[0]), ContentType.DEFAULT_BINARY);
+
+ handler.completed(new ResponseWithEntity(response, entity));
+ assertNull(callback.getResponse());
+ assertNotNull(callback.getThrowable());
+ assertTrue(callback.getThrowable() instanceof OAuthException);
+ // verify latch is released
+ assertThrows(ExecutionException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ handler.getResult();
+ }
+ });
+ }
+
+ @Test
+ public void shouldReleaseLatchOnCancel() {
+ handler = new OAuthAsyncCompletionHandler<>(callback, ALL_GOOD_RESPONSE_CONVERTER);
+
+ handler.cancelled();
+ assertNull(callback.getResponse());
+ assertNotNull(callback.getThrowable());
+ assertTrue(callback.getThrowable() instanceof CancellationException);
+ // verify latch is released
+ assertThrows(ExecutionException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ handler.getResult();
+ }
+ });
+ }
+
+ @Test
+ public void shouldReleaseLatchOnFailure() {
+ handler = new OAuthAsyncCompletionHandler<>(callback, ALL_GOOD_RESPONSE_CONVERTER);
+
+ handler.failed(new RuntimeException());
+ assertNull(callback.getResponse());
+ assertNotNull(callback.getThrowable());
+ assertTrue(callback.getThrowable() instanceof RuntimeException);
+ // verify latch is released
+ assertThrows(ExecutionException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ handler.getResult();
+ }
+ });
+ }
+
+ private static class AllGoodResponseConverter implements OAuthRequest.ResponseConverter {
+
+ @Override
+ public String convert(Response response) throws IOException {
+ response.close();
+ return "All good";
+ }
+ }
+
+ private static class ExceptionResponseConverter implements OAuthRequest.ResponseConverter {
+
+ @Override
+ public String convert(Response response) throws IOException {
+ response.close();
+ throw new IOException("Failed to convert");
+ }
+ }
+
+ private static class OAuthExceptionResponseConverter implements OAuthRequest.ResponseConverter {
+
+ @Override
+ public String convert(Response response) throws IOException {
+ response.close();
+ throw new OAuthException("bad oauth");
+ }
+ }
+}