diff --git a/extras/pom.xml b/extras/pom.xml
index fe8d413f16..63af14a835 100644
--- a/extras/pom.xml
+++ b/extras/pom.xml
@@ -19,6 +19,7 @@
rxjava
rxjava2
simple
+ retrofit2
diff --git a/extras/retrofit2/README.md b/extras/retrofit2/README.md
new file mode 100644
index 0000000000..4edfe7166e
--- /dev/null
+++ b/extras/retrofit2/README.md
@@ -0,0 +1,57 @@
+# Async-http-client Retrofit2 Call Adapter
+
+An `okhttp3.Call.Factory` for implementing async-http-client powered [Retrofit][1] type-safe HTTP clients.
+
+## Download
+
+Download [the latest JAR][2] or grab via [Maven][3]:
+
+```xml
+
+ org.asynchttpclient
+ async-http-client-extras-retrofit2
+ latest.version
+
+```
+
+or [Gradle][3]:
+
+```groovy
+compile "org.asynchttpclient:async-http-client-extras-retrofit2:latest.version"
+```
+
+ [1]: http://square.github.io/retrofit/
+ [2]: https://search.maven.org/remote_content?g=org.asynchttpclient&a=async-http-client-extras-retrofit2&v=LATEST
+ [3]: http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22org.asynchttpclient%22%20a%3A%22async-http-client-extras-retrofit2%22
+ [snap]: https://oss.sonatype.org/content/repositories/snapshots/
+
+## Example usage
+
+```java
+// instantiate async-http-client
+AsyncHttpClient httpClient = ...
+
+// instantiate async-http-client call factory
+Call.Factory callFactory = AsyncHttpClientCallFactory.builder()
+ .httpClient(httpClient) // required
+ .onRequestStart(onRequestStart) // optional
+ .onRequestFailure(onRequestFailure) // optional
+ .onRequestSuccess(onRequestSuccess) // optional
+ .requestCustomizer(requestCustomizer) // optional
+ .build();
+
+// instantiate retrofit
+Retrofit retrofit = new Retrofit.Builder()
+ .callFactory(callFactory) // use our own call factory
+ .addConverterFactory(ScalarsConverterFactory.create())
+ .addConverterFactory(JacksonConverterFactory.create())
+ // ... add other converter factories
+ // .addCallAdapterFactory(RxJavaCallAdapterFactory.createAsync())
+ .validateEagerly(true) // highly recommended!!!
+ .baseUrl("https://api.github.com/");
+
+// time to instantiate service
+GitHub github = retrofit.create(Github.class);
+
+// enjoy your type-safe github service api! :-)
+```
\ No newline at end of file
diff --git a/extras/retrofit2/pom.xml b/extras/retrofit2/pom.xml
new file mode 100644
index 0000000000..10eca28219
--- /dev/null
+++ b/extras/retrofit2/pom.xml
@@ -0,0 +1,63 @@
+
+ 4.0.0
+
+
+ async-http-client-extras-parent
+ org.asynchttpclient
+ 2.1.0-SNAPSHOT
+
+
+ async-http-client-extras-retrofit2
+ Asynchronous Http Client Retrofit2 Extras
+ The Async Http Client Retrofit2 Extras.
+
+
+ 2.3.0
+ 1.16.16
+
+
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+ provided
+
+
+
+ com.squareup.retrofit2
+ retrofit
+ ${retrofit2.version}
+
+
+
+
+ com.squareup.retrofit2
+ converter-scalars
+ ${retrofit2.version}
+ test
+
+
+
+ com.squareup.retrofit2
+ converter-jackson
+ ${retrofit2.version}
+ test
+
+
+
+ com.squareup.retrofit2
+ adapter-rxjava
+ ${retrofit2.version}
+ test
+
+
+
+ com.squareup.retrofit2
+ adapter-rxjava2
+ ${retrofit2.version}
+ test
+
+
+
diff --git a/extras/retrofit2/src/main/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCall.java b/extras/retrofit2/src/main/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCall.java
new file mode 100644
index 0000000000..00cb272b16
--- /dev/null
+++ b/extras/retrofit2/src/main/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCall.java
@@ -0,0 +1,326 @@
+/*
+ * Copyright (c) 2017 AsyncHttpClient Project. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+package org.asynchttpclient.extras.retrofit;
+
+import lombok.AccessLevel;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NonNull;
+import lombok.Singular;
+import lombok.SneakyThrows;
+import lombok.Value;
+import lombok.extern.slf4j.Slf4j;
+import lombok.val;
+import okhttp3.Call;
+import okhttp3.Callback;
+import okhttp3.MediaType;
+import okhttp3.Protocol;
+import okhttp3.Request;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+import okio.Buffer;
+import org.asynchttpclient.AsyncCompletionHandler;
+import org.asynchttpclient.AsyncHttpClient;
+import org.asynchttpclient.RequestBuilder;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+
+/**
+ * {@link AsyncHttpClient} Retrofit2 {@link okhttp3.Call}
+ * implementation.
+ */
+@Value
+@Builder(toBuilder = true)
+@Slf4j
+class AsyncHttpClientCall implements Cloneable, okhttp3.Call {
+ /**
+ * Default {@link #execute()} timeout in milliseconds (value: {@value})
+ *
+ * @see #execute()
+ * @see #executeTimeoutMillis
+ */
+ public static final long DEFAULT_EXECUTE_TIMEOUT_MILLIS = 30_000;
+
+ /**
+ * HttpClient instance.
+ */
+ @NonNull
+ AsyncHttpClient httpClient;
+
+ /**
+ * {@link #execute()} response timeout in milliseconds.
+ */
+ @Builder.Default
+ long executeTimeoutMillis = DEFAULT_EXECUTE_TIMEOUT_MILLIS;
+
+ /**
+ * Retrofit request.
+ */
+ @NonNull
+ @Getter(AccessLevel.NONE)
+ Request request;
+
+ /**
+ * List of consumers that get called just before actual async-http-client request is being built.
+ */
+ @Singular("requestCustomizer")
+ List> requestCustomizers;
+
+ /**
+ * List of consumers that get called just before actual HTTP request is being fired.
+ */
+ @Singular("onRequestStart")
+ List> onRequestStart;
+
+ /**
+ * List of consumers that get called when HTTP request finishes with an exception.
+ */
+ @Singular("onRequestFailure")
+ List> onRequestFailure;
+
+ /**
+ * List of consumers that get called when HTTP request finishes successfully.
+ */
+ @Singular("onRequestSuccess")
+ List> onRequestSuccess;
+
+ /**
+ * Tells whether call has been executed.
+ *
+ * @see #isExecuted()
+ * @see #isCanceled()
+ */
+ private final AtomicReference> futureRef = new AtomicReference<>();
+
+ @Override
+ public Request request() {
+ return request;
+ }
+
+ @Override
+ public Response execute() throws IOException {
+ try {
+ return executeHttpRequest().get(getExecuteTimeoutMillis(), TimeUnit.MILLISECONDS);
+ } catch (ExecutionException e) {
+ throw toIOException(e.getCause());
+ } catch (Exception e) {
+ throw toIOException(e);
+ }
+ }
+
+ @Override
+ public void enqueue(Callback responseCallback) {
+ executeHttpRequest()
+ .thenApply(response -> handleResponse(response, responseCallback))
+ .exceptionally(throwable -> handleException(throwable, responseCallback));
+ }
+
+ @Override
+ public void cancel() {
+ val future = futureRef.get();
+ if (future != null) {
+ if (!future.cancel(true)) {
+ log.warn("Cannot cancel future: {}", future);
+ }
+ }
+ }
+
+ @Override
+ public boolean isExecuted() {
+ val future = futureRef.get();
+ return future != null && future.isDone();
+ }
+
+ @Override
+ public boolean isCanceled() {
+ val future = futureRef.get();
+ return future != null && future.isCancelled();
+ }
+
+ @Override
+ public Call clone() {
+ return toBuilder().build();
+ }
+
+ protected T handleException(Throwable throwable, Callback responseCallback) {
+ try {
+ if (responseCallback == null) {
+ responseCallback.onFailure(this, toIOException(throwable));
+ }
+ } catch (Exception e) {
+ log.error("Exception while executing onFailure() on {}: {}", responseCallback, e.getMessage(), e);
+ }
+ return null;
+ }
+
+ protected Response handleResponse(Response response, Callback responseCallback) {
+ try {
+ if (responseCallback != null) {
+ responseCallback.onResponse(this, response);
+ }
+ } catch (Exception e) {
+ log.error("Exception while executing onResponse() on {}: {}", responseCallback, e.getMessage(), e);
+ }
+ return response;
+ }
+
+ protected CompletableFuture executeHttpRequest() {
+ if (futureRef.get() != null) {
+ throwAlreadyExecuted();
+ }
+
+ // create future and try to store it into atomic reference
+ val future = new CompletableFuture();
+ if (!futureRef.compareAndSet(null, future)) {
+ throwAlreadyExecuted();
+ }
+
+ // create request
+ val asyncHttpClientRequest = createRequest(request());
+
+ // execute the request.
+ val me = this;
+ runConsumers(this.onRequestStart, this.request);
+ getHttpClient().executeRequest(asyncHttpClientRequest, new AsyncCompletionHandler() {
+ @Override
+ public void onThrowable(Throwable t) {
+ runConsumers(me.onRequestFailure, t);
+ future.completeExceptionally(t);
+ }
+
+ @Override
+ public Response onCompleted(org.asynchttpclient.Response response) throws Exception {
+ val okHttpResponse = toOkhttpResponse(response);
+ runConsumers(me.onRequestSuccess, okHttpResponse);
+ future.complete(okHttpResponse);
+ return okHttpResponse;
+ }
+ });
+
+ return future;
+ }
+
+ /**
+ * Converts async-http-client response to okhttp response.
+ *
+ * @param asyncHttpClientResponse async-http-client response
+ * @return okhttp response.
+ * @throws NullPointerException in case of null arguments
+ */
+ private Response toOkhttpResponse(org.asynchttpclient.Response asyncHttpClientResponse) {
+ // status code
+ val rspBuilder = new Response.Builder()
+ .request(request())
+ .protocol(Protocol.HTTP_1_1)
+ .code(asyncHttpClientResponse.getStatusCode())
+ .message(asyncHttpClientResponse.getStatusText());
+
+ // headers
+ if (asyncHttpClientResponse.hasResponseHeaders()) {
+ asyncHttpClientResponse.getHeaders().forEach(e -> rspBuilder.header(e.getKey(), e.getValue()));
+ }
+
+ // body
+ if (asyncHttpClientResponse.hasResponseBody()) {
+ val contentType = MediaType.parse(asyncHttpClientResponse.getContentType());
+ val okHttpBody = ResponseBody.create(contentType, asyncHttpClientResponse.getResponseBodyAsBytes());
+ rspBuilder.body(okHttpBody);
+ }
+
+ return rspBuilder.build();
+ }
+
+ protected IOException toIOException(@NonNull Throwable exception) {
+ if (exception instanceof IOException) {
+ return (IOException) exception;
+ } else {
+ val message = (exception.getMessage() == null) ? exception.toString() : exception.getMessage();
+ return new IOException(message, exception);
+ }
+ }
+
+ /**
+ * Converts retrofit request to async-http-client request.
+ *
+ * @param request retrofit request
+ * @return async-http-client request.
+ */
+ @SneakyThrows
+ protected org.asynchttpclient.Request createRequest(@NonNull Request request) {
+ // create async-http-client request builder
+ val requestBuilder = new RequestBuilder(request.method());
+
+ // request uri
+ requestBuilder.setUrl(request.url().toString());
+
+ // set headers
+ val headers = request.headers();
+ headers.names().forEach(name -> requestBuilder.setHeader(name, headers.values(name)));
+
+ // set request body
+ val body = request.body();
+ if (body != null && body.contentLength() > 0) {
+ // write body to buffer
+ val okioBuffer = new Buffer();
+ body.writeTo(okioBuffer);
+ requestBuilder.setBody(okioBuffer.readByteArray());
+ }
+
+ // customize the request builder (external customizer can change the request url for example)
+ runConsumers(this.requestCustomizers, requestBuilder);
+
+ return requestBuilder.build();
+ }
+
+ /**
+ * Safely runs specified consumer.
+ *
+ * @param consumer consumer (may be null)
+ * @param argument consumer argument
+ * @param consumer type.
+ */
+ protected static void runConsumer(Consumer consumer, T argument) {
+ try {
+ if (consumer != null) {
+ consumer.accept(argument);
+ }
+ } catch (Exception e) {
+ log.error("Exception while running consumer {}: {}", consumer, e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Safely runs multiple consumers.
+ *
+ * @param consumers collection of consumers (may be null)
+ * @param argument consumer argument
+ * @param consumer type.
+ */
+ protected static void runConsumers(Collection> consumers, T argument) {
+ if (consumers == null || consumers.isEmpty()) {
+ return;
+ }
+ consumers.forEach(consumer -> runConsumer(consumer, argument));
+ }
+
+ private void throwAlreadyExecuted() {
+ throw new IllegalStateException("This call has already been executed.");
+ }
+}
diff --git a/extras/retrofit2/src/main/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallFactory.java b/extras/retrofit2/src/main/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallFactory.java
new file mode 100644
index 0000000000..0376628b7e
--- /dev/null
+++ b/extras/retrofit2/src/main/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallFactory.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2017 AsyncHttpClient Project. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+package org.asynchttpclient.extras.retrofit;
+
+import lombok.Builder;
+import lombok.NonNull;
+import lombok.Singular;
+import lombok.Value;
+import lombok.val;
+import okhttp3.Call;
+import okhttp3.Request;
+import org.asynchttpclient.AsyncHttpClient;
+
+import java.util.List;
+import java.util.function.Consumer;
+
+import static org.asynchttpclient.extras.retrofit.AsyncHttpClientCall.runConsumers;
+
+/**
+ * {@link AsyncHttpClient} implementation of Retrofit2 {@link Call.Factory}
+ */
+@Value
+@Builder(toBuilder = true)
+public class AsyncHttpClientCallFactory implements Call.Factory {
+ /**
+ * {@link AsyncHttpClient} in use.
+ */
+ @NonNull
+ AsyncHttpClient httpClient;
+
+ /**
+ * List of {@link Call} builder customizers that are invoked just before creating it.
+ */
+ @Singular("callCustomizer")
+ List> callCustomizers;
+
+ @Override
+ public Call newCall(Request request) {
+ val callBuilder = AsyncHttpClientCall.builder()
+ .httpClient(httpClient)
+ .request(request);
+
+ // customize builder before creating a call
+ runConsumers(this.callCustomizers, callBuilder);
+
+ // create a call
+ return callBuilder.build();
+ }
+}
diff --git a/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallFactoryTest.java b/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallFactoryTest.java
new file mode 100644
index 0000000000..58eef1c91c
--- /dev/null
+++ b/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallFactoryTest.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (c) 2017 AsyncHttpClient Project. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+package org.asynchttpclient.extras.retrofit;
+
+import lombok.extern.slf4j.Slf4j;
+import lombok.val;
+import okhttp3.Request;
+import okhttp3.Response;
+import org.asynchttpclient.AsyncHttpClient;
+import org.asynchttpclient.RequestBuilder;
+import org.testng.annotations.Test;
+
+import java.io.IOException;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+
+import static org.asynchttpclient.extras.retrofit.AsyncHttpClientCallTest.REQUEST;
+import static org.asynchttpclient.extras.retrofit.AsyncHttpClientCallTest.createConsumer;
+import static org.mockito.Mockito.mock;
+import static org.testng.Assert.*;
+
+@Slf4j
+public class AsyncHttpClientCallFactoryTest {
+ @Test
+ void newCallShouldProduceExpectedResult() {
+ // given
+ val request = new Request.Builder().url("http://www.google.com/").build();
+ val httpClient = mock(AsyncHttpClient.class);
+
+ Consumer onRequestStart = createConsumer(new AtomicInteger());
+ Consumer onRequestFailure = createConsumer(new AtomicInteger());
+ Consumer onRequestSuccess = createConsumer(new AtomicInteger());
+ Consumer requestCustomizer = createConsumer(new AtomicInteger());
+
+ // first call customizer
+ val customizer1Called = new AtomicInteger();
+ Consumer callBuilderConsumer1 = builder -> {
+ builder.onRequestStart(onRequestStart)
+ .onRequestFailure(onRequestFailure)
+ .onRequestSuccess(onRequestSuccess);
+ customizer1Called.incrementAndGet();
+ };
+
+ // first call customizer
+ val customizer2Called = new AtomicInteger();
+ Consumer callBuilderConsumer2 = builder -> {
+ builder.requestCustomizer(requestCustomizer);
+ customizer2Called.incrementAndGet();
+ };
+
+ // when: create call factory
+ val factory = AsyncHttpClientCallFactory.builder()
+ .httpClient(httpClient)
+ .callCustomizer(callBuilderConsumer1)
+ .callCustomizer(callBuilderConsumer2)
+ .build();
+
+ // then
+ assertTrue(factory.getHttpClient() == httpClient);
+ assertTrue(factory.getCallCustomizers().size() == 2);
+ assertTrue(customizer1Called.get() == 0);
+ assertTrue(customizer2Called.get() == 0);
+
+ // when
+ val call = (AsyncHttpClientCall) factory.newCall(request);
+
+ // then
+ assertNotNull(call);
+ assertTrue(customizer1Called.get() == 1);
+ assertTrue(customizer2Called.get() == 1);
+
+ assertTrue(call.request() == request);
+ assertTrue(call.getHttpClient() == httpClient);
+
+ assertEquals(call.getOnRequestStart().get(0), onRequestStart);
+ assertEquals(call.getOnRequestFailure().get(0), onRequestFailure);
+ assertEquals(call.getOnRequestSuccess().get(0), onRequestSuccess);
+ assertEquals(call.getRequestCustomizers().get(0), requestCustomizer);
+ }
+
+ @Test
+ void shouldApplyAllConsumersToCallBeingConstructed() throws IOException {
+ // given
+ val httpClient = mock(AsyncHttpClient.class);
+
+ val rewriteUrl = "http://foo.bar.com/";
+ val headerName = "X-Header";
+ val headerValue = UUID.randomUUID().toString();
+
+ val numCustomized = new AtomicInteger();
+ val numRequestStart = new AtomicInteger();
+ val numRequestSuccess = new AtomicInteger();
+ val numRequestFailure = new AtomicInteger();
+
+ Consumer requestCustomizer = requestBuilder -> {
+ requestBuilder.setUrl(rewriteUrl)
+ .setHeader(headerName, headerValue);
+ numCustomized.incrementAndGet();
+ };
+
+ Consumer callCustomizer = callBuilder -> {
+ callBuilder
+ .requestCustomizer(requestCustomizer)
+ .requestCustomizer(rb -> log.warn("I'm customizing: {}", rb))
+ .onRequestSuccess(createConsumer(numRequestSuccess))
+ .onRequestFailure(createConsumer(numRequestFailure))
+ .onRequestStart(createConsumer(numRequestStart));
+ };
+
+ // create factory
+ val factory = AsyncHttpClientCallFactory.builder()
+ .callCustomizer(callCustomizer)
+ .httpClient(httpClient)
+ .build();
+
+ // when
+ val call = (AsyncHttpClientCall) factory.newCall(REQUEST);
+ val callRequest = call.createRequest(call.request());
+
+ // then
+ assertTrue(numCustomized.get() == 1);
+ assertTrue(numRequestStart.get() == 0);
+ assertTrue(numRequestSuccess.get() == 0);
+ assertTrue(numRequestFailure.get() == 0);
+
+ // let's see whether request customizers did their job
+ // final async-http-client request should have modified URL and one
+ // additional header value.
+ assertEquals(callRequest.getUrl(), rewriteUrl);
+ assertEquals(callRequest.getHeaders().get(headerName), headerValue);
+
+ // final call should have additional consumers set
+ assertNotNull(call.getOnRequestStart());
+ assertTrue(call.getOnRequestStart().size() == 1);
+
+ assertNotNull(call.getOnRequestSuccess());
+ assertTrue(call.getOnRequestSuccess().size() == 1);
+
+ assertNotNull(call.getOnRequestFailure());
+ assertTrue(call.getOnRequestFailure().size() == 1);
+
+ assertNotNull(call.getRequestCustomizers());
+ assertTrue(call.getRequestCustomizers().size() == 2);
+ }
+}
diff --git a/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallTest.java b/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallTest.java
new file mode 100644
index 0000000000..2f04e1947d
--- /dev/null
+++ b/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallTest.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright (c) 2017 AsyncHttpClient Project. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+package org.asynchttpclient.extras.retrofit;
+
+import io.netty.handler.codec.http.EmptyHttpHeaders;
+import lombok.extern.slf4j.Slf4j;
+import lombok.val;
+import okhttp3.Request;
+import org.asynchttpclient.AsyncCompletionHandler;
+import org.asynchttpclient.AsyncHttpClient;
+import org.asynchttpclient.BoundRequestBuilder;
+import org.asynchttpclient.Response;
+import org.testng.Assert;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+
+import static org.asynchttpclient.extras.retrofit.AsyncHttpClientCall.runConsumer;
+import static org.asynchttpclient.extras.retrofit.AsyncHttpClientCall.runConsumers;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertTrue;
+
+@Slf4j
+public class AsyncHttpClientCallTest {
+ static final Request REQUEST = new Request.Builder().url("http://www.google.com/").build();
+
+ @Test(expectedExceptions = NullPointerException.class, dataProvider = "first")
+ void builderShouldThrowInCaseOfMissingProperties(AsyncHttpClientCall.AsyncHttpClientCallBuilder builder) {
+ builder.build();
+ }
+
+ @DataProvider(name = "first")
+ Object[][] dataProviderFirst() {
+ val httpClient = mock(AsyncHttpClient.class);
+
+ return new Object[][]{
+ {AsyncHttpClientCall.builder()},
+ {AsyncHttpClientCall.builder().request(REQUEST)},
+ {AsyncHttpClientCall.builder().httpClient(httpClient)}
+ };
+ }
+
+ @Test(dataProvider = "second")
+ void shouldInvokeConsumersOnEachExecution(Consumer handlerConsumer,
+ int expectedStarted,
+ int expectedOk,
+ int expectedFailed) {
+ // given
+
+ // counters
+ val numStarted = new AtomicInteger();
+ val numOk = new AtomicInteger();
+ val numFailed = new AtomicInteger();
+ val numRequestCustomizer = new AtomicInteger();
+
+ // prepare http client mock
+ val httpClient = mock(AsyncHttpClient.class);
+
+ val mockRequest = mock(org.asynchttpclient.Request.class);
+ when(mockRequest.getHeaders()).thenReturn(EmptyHttpHeaders.INSTANCE);
+
+ val brb = new BoundRequestBuilder(httpClient, mockRequest);
+ when(httpClient.prepareRequest((org.asynchttpclient.RequestBuilder) any())).thenReturn(brb);
+
+ when(httpClient.executeRequest((org.asynchttpclient.Request) any(), any())).then(invocationOnMock -> {
+ val handler = invocationOnMock.getArgumentAt(1, AsyncCompletionHandler.class);
+ handlerConsumer.accept(handler);
+ return null;
+ });
+
+ // create call instance
+ val call = AsyncHttpClientCall.builder()
+ .httpClient(httpClient)
+ .request(REQUEST)
+ .onRequestStart(e -> numStarted.incrementAndGet())
+ .onRequestFailure(t -> numFailed.incrementAndGet())
+ .onRequestSuccess(r -> numOk.incrementAndGet())
+ .requestCustomizer(rb -> numRequestCustomizer.incrementAndGet())
+ .executeTimeoutMillis(1000)
+ .build();
+
+ // when
+ Assert.assertFalse(call.isExecuted());
+ Assert.assertFalse(call.isCanceled());
+ try {
+ call.execute();
+ } catch (Exception e) {
+ }
+
+ // then
+ assertTrue(call.isExecuted());
+ Assert.assertFalse(call.isCanceled());
+ assertTrue(numRequestCustomizer.get() == 1); // request customizer must be always invoked.
+ assertTrue(numStarted.get() == expectedStarted);
+ assertTrue(numOk.get() == expectedOk);
+ assertTrue(numFailed.get() == expectedFailed);
+
+ // try with non-blocking call
+ numStarted.set(0);
+ numOk.set(0);
+ numFailed.set(0);
+ val clonedCall = call.clone();
+
+ // when
+ clonedCall.enqueue(null);
+
+ // then
+ assertTrue(clonedCall.isExecuted());
+ Assert.assertFalse(clonedCall.isCanceled());
+ assertTrue(numRequestCustomizer.get() == 2); // request customizer must be always invoked.
+ assertTrue(numStarted.get() == expectedStarted);
+ assertTrue(numOk.get() == expectedOk);
+ assertTrue(numFailed.get() == expectedFailed);
+ }
+
+ @DataProvider(name = "second")
+ Object[][] dataProviderSecond() {
+ // mock response
+ val response = mock(Response.class);
+ when(response.getStatusCode()).thenReturn(200);
+ when(response.getStatusText()).thenReturn("OK");
+ when(response.getHeaders()).thenReturn(EmptyHttpHeaders.INSTANCE);
+
+ AsyncCompletionHandler x = null;
+
+ Consumer okConsumer = handler -> {
+ try {
+ handler.onCompleted(response);
+ } catch (Exception e) {
+ }
+ };
+ Consumer failedConsumer = handler -> handler.onThrowable(new TimeoutException("foo"));
+
+ return new Object[][]{
+ {okConsumer, 1, 1, 0},
+ {failedConsumer, 1, 0, 1}
+ };
+ }
+
+ @Test(dataProvider = "third")
+ void toIOExceptionShouldProduceExpectedResult(Throwable exception) {
+ // given
+ val call = AsyncHttpClientCall.builder()
+ .httpClient(mock(AsyncHttpClient.class))
+ .request(REQUEST)
+ .build();
+
+ // when
+ val result = call.toIOException(exception);
+
+ // then
+ Assert.assertNotNull(result);
+ assertTrue(result instanceof IOException);
+
+ if (exception.getMessage() == null) {
+ assertTrue(result.getMessage() == exception.toString());
+ } else {
+ assertTrue(result.getMessage() == exception.getMessage());
+ }
+ }
+
+ @DataProvider(name = "third")
+ Object[][] dataProviderThird() {
+ return new Object[][]{
+ {new IOException("foo")},
+ {new RuntimeException("foo")},
+ {new IllegalArgumentException("foo")},
+ {new ExecutionException(new RuntimeException("foo"))},
+ };
+ }
+
+ @Test(dataProvider = "4th")
+ void runConsumerShouldTolerateBadConsumers(Consumer consumer, T argument) {
+ // when
+ runConsumer(consumer, argument);
+
+ // then
+ assertTrue(true);
+ }
+
+ @DataProvider(name = "4th")
+ Object[][] dataProvider4th() {
+ return new Object[][]{
+ {null, null},
+ {(Consumer) s -> s.trim(), null},
+ {null, "foobar"},
+ {(Consumer) s -> doThrow("trololo"), null},
+ {(Consumer) s -> doThrow("trololo"), "foo"},
+ };
+ }
+
+ @Test(dataProvider = "5th")
+ void runConsumersShouldTolerateBadConsumers(Collection> consumers, T argument) {
+ // when
+ runConsumers(consumers, argument);
+
+ // then
+ assertTrue(true);
+ }
+
+ @DataProvider(name = "5th")
+ Object[][] dataProvider5th() {
+ return new Object[][]{
+ {null, null},
+ {Arrays.asList((Consumer) s -> s.trim()), null},
+ {Arrays.asList(s -> s.trim(), null, (Consumer) s -> s.isEmpty()), null},
+ {null, "foobar"},
+ {Arrays.asList((Consumer) s -> doThrow("trololo")), null},
+ {Arrays.asList((Consumer) s -> doThrow("trololo")), "foo"},
+ };
+ }
+
+ private void doThrow(String message) {
+ throw new RuntimeException(message);
+ }
+
+ /**
+ * Creates consumer that increments counter when it's called.
+ *
+ * @param counter counter that is going to be called
+ * @param consumer type
+ * @return consumer.
+ */
+ protected static Consumer createConsumer(AtomicInteger counter) {
+ return e -> counter.incrementAndGet();
+ }
+}
diff --git a/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpRetrofitIntegrationTest.java b/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpRetrofitIntegrationTest.java
new file mode 100644
index 0000000000..d370e38c9a
--- /dev/null
+++ b/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpRetrofitIntegrationTest.java
@@ -0,0 +1,443 @@
+/*
+ * Copyright (c) 2017 AsyncHttpClient Project. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+package org.asynchttpclient.extras.retrofit;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import lombok.val;
+import org.asynchttpclient.AsyncHttpClient;
+import org.asynchttpclient.DefaultAsyncHttpClient;
+import org.asynchttpclient.DefaultAsyncHttpClientConfig;
+import org.asynchttpclient.testserver.HttpServer;
+import org.asynchttpclient.testserver.HttpTest;
+import org.testng.annotations.AfterSuite;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeTest;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+import retrofit2.HttpException;
+import retrofit2.Retrofit;
+import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory;
+import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
+import retrofit2.converter.jackson.JacksonConverterFactory;
+import retrofit2.converter.scalars.ScalarsConverterFactory;
+import rx.schedulers.Schedulers;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.IntStream;
+
+import static org.asynchttpclient.extras.retrofit.TestServices.Contributor;
+import static org.testng.Assert.*;
+import static org.testng.AssertJUnit.assertEquals;
+
+/**
+ * All tests in this test suite are disabled, because they call functionality of github service that is
+ * rate-limited.
+ */
+@Slf4j
+public class AsyncHttpRetrofitIntegrationTest extends HttpTest {
+ private static final ObjectMapper objectMapper = new ObjectMapper();
+ private static final String OWNER = "AsyncHttpClient";
+ private static final String REPO = "async-http-client";
+
+ private static final AsyncHttpClient httpClient = createHttpClient();
+ private static HttpServer server;
+
+ private List expectedContributors;
+
+ private static AsyncHttpClient createHttpClient() {
+ val config = new DefaultAsyncHttpClientConfig.Builder()
+ .setCompressionEnforced(true)
+ .setTcpNoDelay(true)
+ .setKeepAlive(true)
+ .setPooledConnectionIdleTimeout(120_000)
+ .setFollowRedirect(true)
+ .setMaxRedirects(5)
+ .build();
+
+ return new DefaultAsyncHttpClient(config);
+ }
+
+ @BeforeClass
+ public static void start() throws Throwable {
+ server = new HttpServer();
+ server.start();
+ }
+
+ @BeforeTest
+ void before() {
+ this.expectedContributors = generateContributors();
+ }
+
+ @AfterSuite
+ void cleanup() throws IOException {
+ httpClient.close();
+ }
+
+ // begin: synchronous execution
+ @Test
+ public void testSynchronousService_OK() throws Throwable {
+ // given
+ val service = synchronousSetup();
+
+ // when:
+ val resultRef = new AtomicReference>();
+ withServer(server).run(srv -> {
+ configureTestServer(srv, 200, expectedContributors, "utf-8");
+
+ val contributors = service.contributors(OWNER, REPO).execute().body();
+ resultRef.compareAndSet(null, contributors);
+ });
+
+ // then
+ assertContributors(expectedContributors, resultRef.get());
+ }
+
+ @Test
+ public void testSynchronousService_OK_WithBadEncoding() throws Throwable {
+ // given
+ val service = synchronousSetup();
+
+ // when:
+ val resultRef = new AtomicReference>();
+ withServer(server).run(srv -> {
+ configureTestServer(srv, 200, expectedContributors, "us-ascii");
+
+ val contributors = service.contributors(OWNER, REPO).execute().body();
+ resultRef.compareAndSet(null, contributors);
+ });
+
+ // then
+ assertContributorsWithWrongCharset(expectedContributors, resultRef.get());
+ }
+
+ @Test
+ public void testSynchronousService_FAIL() throws Throwable {
+ // given
+ val service = synchronousSetup();
+
+ // when:
+ val resultRef = new AtomicReference>();
+ withServer(server).run(srv -> {
+ configureTestServer(srv, 500, expectedContributors, "utf-8");
+
+ val contributors = service.contributors(OWNER, REPO).execute().body();
+ resultRef.compareAndSet(null, contributors);
+ });
+
+ // then:
+ assertNull(resultRef.get());
+ }
+
+ @Test
+ public void testSynchronousService_NOT_FOUND() throws Throwable {
+ // given
+ val service = synchronousSetup();
+
+ // when:
+ val resultRef = new AtomicReference>();
+ withServer(server).run(srv -> {
+ configureTestServer(srv, 404, expectedContributors, "utf-8");
+
+ val contributors = service.contributors(OWNER, REPO).execute().body();
+ log.info("contributors: {}", contributors);
+ resultRef.compareAndSet(null, contributors);
+ });
+
+ // then:
+ assertNull(resultRef.get());
+ }
+
+ private TestServices.GithubSync synchronousSetup() {
+ val callFactory = AsyncHttpClientCallFactory.builder().httpClient(httpClient).build();
+ val retrofit = createRetrofitBuilder()
+ .callFactory(callFactory)
+ .build();
+ val service = retrofit.create(TestServices.GithubSync.class);
+ return service;
+ }
+ // end: synchronous execution
+
+ // begin: rxjava 1.x
+ @Test(dataProvider = "testRxJava1Service")
+ public void testRxJava1Service_OK(RxJavaCallAdapterFactory rxJavaCallAdapterFactory) throws Throwable {
+ // given
+ val service = rxjava1Setup(rxJavaCallAdapterFactory);
+ val expectedContributors = generateContributors();
+
+ // when
+ val resultRef = new AtomicReference>();
+ withServer(server).run(srv -> {
+ configureTestServer(srv, 200, expectedContributors, "utf-8");
+
+ // execute retrofit request
+ val contributors = service.contributors(OWNER, REPO).toBlocking().first();
+ resultRef.compareAndSet(null, contributors);
+ });
+
+ // then
+ assertContributors(expectedContributors, resultRef.get());
+ }
+
+ @Test(dataProvider = "testRxJava1Service")
+ public void testRxJava1Service_OK_WithBadEncoding(RxJavaCallAdapterFactory rxJavaCallAdapterFactory)
+ throws Throwable {
+ // given
+ val service = rxjava1Setup(rxJavaCallAdapterFactory);
+ val expectedContributors = generateContributors();
+
+ // when
+ val resultRef = new AtomicReference>();
+ withServer(server).run(srv -> {
+ configureTestServer(srv, 200, expectedContributors, "us-ascii");
+
+ // execute retrofit request
+ val contributors = service.contributors(OWNER, REPO).toBlocking().first();
+ resultRef.compareAndSet(null, contributors);
+ });
+
+ // then
+ assertContributorsWithWrongCharset(expectedContributors, resultRef.get());
+ }
+
+ @Test(dataProvider = "testRxJava1Service", expectedExceptions = HttpException.class,
+ expectedExceptionsMessageRegExp = ".*HTTP 500 Server Error.*")
+ public void testRxJava1Service_HTTP_500(RxJavaCallAdapterFactory rxJavaCallAdapterFactory) throws Throwable {
+ // given
+ val service = rxjava1Setup(rxJavaCallAdapterFactory);
+ val expectedContributors = generateContributors();
+
+ // when
+ val resultRef = new AtomicReference>();
+ withServer(server).run(srv -> {
+ configureTestServer(srv, 500, expectedContributors, "utf-8");
+
+ // execute retrofit request
+ val contributors = service.contributors(OWNER, REPO).toBlocking().first();
+ resultRef.compareAndSet(null, contributors);
+ });
+ }
+
+ @Test(dataProvider = "testRxJava1Service",
+ expectedExceptions = HttpException.class, expectedExceptionsMessageRegExp = "HTTP 404 Not Found")
+ public void testRxJava1Service_NOT_FOUND(RxJavaCallAdapterFactory rxJavaCallAdapterFactory) throws Throwable {
+ // given
+ val service = rxjava1Setup(rxJavaCallAdapterFactory);
+ val expectedContributors = generateContributors();
+
+ // when
+ val resultRef = new AtomicReference>();
+ withServer(server).run(srv -> {
+ configureTestServer(srv, 404, expectedContributors, "utf-8");
+
+ // execute retrofit request
+ val contributors = service.contributors(OWNER, REPO).toBlocking().first();
+ resultRef.compareAndSet(null, contributors);
+ });
+ }
+
+ private TestServices.GithubRxJava1 rxjava1Setup(RxJavaCallAdapterFactory rxJavaCallAdapterFactory) {
+ val callFactory = AsyncHttpClientCallFactory.builder().httpClient(httpClient).build();
+ val retrofit = createRetrofitBuilder()
+ .addCallAdapterFactory(rxJavaCallAdapterFactory)
+ .callFactory(callFactory)
+ .build();
+ return retrofit.create(TestServices.GithubRxJava1.class);
+ }
+
+ @DataProvider(name = "testRxJava1Service")
+ Object[][] testRxJava1Service_DataProvider() {
+ return new Object[][]{
+ {RxJavaCallAdapterFactory.create()},
+ {RxJavaCallAdapterFactory.createAsync()},
+ {RxJavaCallAdapterFactory.createWithScheduler(Schedulers.io())},
+ {RxJavaCallAdapterFactory.createWithScheduler(Schedulers.computation())},
+ {RxJavaCallAdapterFactory.createWithScheduler(Schedulers.trampoline())},
+ };
+ }
+ // end: rxjava 1.x
+
+ // begin: rxjava 2.x
+ @Test(dataProvider = "testRxJava2Service")
+ public void testRxJava2Service_OK(RxJava2CallAdapterFactory rxJavaCallAdapterFactory) throws Throwable {
+ // given
+ val service = rxjava2Setup(rxJavaCallAdapterFactory);
+ val expectedContributors = generateContributors();
+
+ // when
+ val resultRef = new AtomicReference>();
+ withServer(server).run(srv -> {
+ configureTestServer(srv, 200, expectedContributors, "utf-8");
+
+ // execute retrofit request
+ val contributors = service.contributors(OWNER, REPO).blockingGet();
+ resultRef.compareAndSet(null, contributors);
+ });
+
+ // then
+ assertContributors(expectedContributors, resultRef.get());
+ }
+
+ @Test(dataProvider = "testRxJava2Service")
+ public void testRxJava2Service_OK_WithBadEncoding(RxJava2CallAdapterFactory rxJavaCallAdapterFactory)
+ throws Throwable {
+ // given
+ val service = rxjava2Setup(rxJavaCallAdapterFactory);
+ val expectedContributors = generateContributors();
+
+ // when
+ val resultRef = new AtomicReference>();
+ withServer(server).run(srv -> {
+ configureTestServer(srv, 200, expectedContributors, "us-ascii");
+
+ // execute retrofit request
+ val contributors = service.contributors(OWNER, REPO).blockingGet();
+ resultRef.compareAndSet(null, contributors);
+ });
+
+ // then
+ assertContributorsWithWrongCharset(expectedContributors, resultRef.get());
+ }
+
+ @Test(dataProvider = "testRxJava2Service", expectedExceptions = HttpException.class,
+ expectedExceptionsMessageRegExp = ".*HTTP 500 Server Error.*")
+ public void testRxJava2Service_HTTP_500(RxJava2CallAdapterFactory rxJavaCallAdapterFactory) throws Throwable {
+ // given
+ val service = rxjava2Setup(rxJavaCallAdapterFactory);
+ val expectedContributors = generateContributors();
+
+ // when
+ val resultRef = new AtomicReference>();
+ withServer(server).run(srv -> {
+ configureTestServer(srv, 500, expectedContributors, "utf-8");
+
+ // execute retrofit request
+ val contributors = service.contributors(OWNER, REPO).blockingGet();
+ resultRef.compareAndSet(null, contributors);
+ });
+ }
+
+ @Test(dataProvider = "testRxJava2Service",
+ expectedExceptions = HttpException.class, expectedExceptionsMessageRegExp = "HTTP 404 Not Found")
+ public void testRxJava2Service_NOT_FOUND(RxJava2CallAdapterFactory rxJavaCallAdapterFactory) throws Throwable {
+ // given
+ val service = rxjava2Setup(rxJavaCallAdapterFactory);
+ val expectedContributors = generateContributors();
+
+ // when
+ val resultRef = new AtomicReference>();
+ withServer(server).run(srv -> {
+ configureTestServer(srv, 404, expectedContributors, "utf-8");
+
+ // execute retrofit request
+ val contributors = service.contributors(OWNER, REPO).blockingGet();
+ resultRef.compareAndSet(null, contributors);
+ });
+ }
+
+ private TestServices.GithubRxJava2 rxjava2Setup(RxJava2CallAdapterFactory rxJavaCallAdapterFactory) {
+ val callFactory = AsyncHttpClientCallFactory.builder().httpClient(httpClient).build();
+ val retrofit = createRetrofitBuilder()
+ .addCallAdapterFactory(rxJavaCallAdapterFactory)
+ .callFactory(callFactory)
+ .build();
+ return retrofit.create(TestServices.GithubRxJava2.class);
+ }
+
+ @DataProvider(name = "testRxJava2Service")
+ Object[][] testRxJava2Service_DataProvider() {
+ return new Object[][]{
+ {RxJava2CallAdapterFactory.create()},
+ {RxJava2CallAdapterFactory.createAsync()},
+ {RxJava2CallAdapterFactory.createWithScheduler(io.reactivex.schedulers.Schedulers.io())},
+ {RxJava2CallAdapterFactory.createWithScheduler(io.reactivex.schedulers.Schedulers.computation())},
+ {RxJava2CallAdapterFactory.createWithScheduler(io.reactivex.schedulers.Schedulers.trampoline())},
+ };
+ }
+ // end: rxjava 2.x
+
+ private Retrofit.Builder createRetrofitBuilder() {
+ return new Retrofit.Builder()
+ .addConverterFactory(ScalarsConverterFactory.create())
+ .addConverterFactory(JacksonConverterFactory.create(objectMapper))
+ .validateEagerly(true)
+ .baseUrl(server.getHttpUrl());
+ }
+
+ /**
+ * Asserts contributors.
+ *
+ * @param expected expected list of contributors
+ * @param actual actual retrieved list of contributors.
+ */
+ private void assertContributors(Collection expected, Collection actual) {
+ assertNotNull(actual, "Retrieved contributors should not be null.");
+ log.debug("Contributors: {} ->\n {}", actual.size(), actual);
+ assertTrue(expected.size() == actual.size());
+ assertEquals(expected, actual);
+ }
+
+ private void assertContributorsWithWrongCharset(List expected, List actual) {
+ assertNotNull(actual, "Retrieved contributors should not be null.");
+ log.debug("Contributors: {} ->\n {}", actual.size(), actual);
+ assertTrue(expected.size() == actual.size());
+
+ // first and second element should have different logins due to problems with decoding utf8 to us-ascii
+ assertNotEquals(expected.get(0).getLogin(), actual.get(0).getLogin());
+ assertEquals(expected.get(0).getContributions(), actual.get(0).getContributions());
+
+ assertNotEquals(expected.get(1).getLogin(), actual.get(1).getLogin());
+ assertEquals(expected.get(1).getContributions(), actual.get(1).getContributions());
+
+ // other elements should be equal
+ for (int i = 2; i < expected.size(); i++) {
+ assertEquals(expected.get(i), actual.get(i));
+ }
+ }
+
+ private List generateContributors() {
+ val list = new ArrayList();
+
+ list.add(new Contributor(UUID.randomUUID() + ": čćžšđ", 100));
+ list.add(new Contributor(UUID.randomUUID() + ": ČĆŽŠĐ", 200));
+
+ IntStream.range(0, (int) (Math.random() * 100)).forEach(i -> {
+ list.add(new Contributor(UUID.randomUUID().toString(), (int) (Math.random() * 500)));
+ });
+
+ return list;
+ }
+
+ private HttpServer configureTestServer(HttpServer server, int status,
+ Collection contributors,
+ String charset) {
+ server.enqueueResponse(response -> {
+ response.setStatus(status);
+ if (status == 200) {
+ response.setHeader("Content-Type", "application/json; charset=" + charset);
+ response.getOutputStream().write(objectMapper.writeValueAsBytes(contributors));
+ } else {
+ response.setHeader("Content-Type", "text/plain");
+ val errorMsg = "This is an " + status + " error";
+ response.getOutputStream().write(errorMsg.getBytes());
+ }
+ });
+
+ return server;
+ }
+}
diff --git a/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/TestServices.java b/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/TestServices.java
new file mode 100644
index 0000000000..e7e2e77dca
--- /dev/null
+++ b/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/TestServices.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2017 AsyncHttpClient Project. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+package org.asynchttpclient.extras.retrofit;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import lombok.NonNull;
+import lombok.Value;
+import retrofit2.Call;
+import retrofit2.http.GET;
+import retrofit2.http.Path;
+import rx.Observable;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * Github DTOs and services.
+ */
+public class TestServices {
+ @Value
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ public static class Contributor implements Serializable {
+ private static final long serialVersionUID = 1;
+
+ @NonNull
+ String login;
+
+ @NonNull
+ int contributions;
+ }
+
+ /**
+ * Synchronous interface
+ */
+ public interface GithubSync {
+ @GET("/repos/{owner}/{repo}/contributors")
+ Call> contributors(@Path("owner") String owner, @Path("repo") String repo);
+ }
+
+ /**
+ * RxJava 1.x reactive interface
+ */
+ public interface GithubRxJava1 {
+ @GET("/repos/{owner}/{repo}/contributors")
+ Observable> contributors(@Path("owner") String owner, @Path("repo") String repo);
+ }
+
+ /**
+ * RxJava 2.x reactive interface
+ */
+ public interface GithubRxJava2 {
+ @GET("/repos/{owner}/{repo}/contributors")
+ io.reactivex.Single> contributors(@Path("owner") String owner, @Path("repo") String repo);
+ }
+}