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"); + } + } +}