From e28607f7477f8c4e38db56094461127a9c9a81b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brane=20F=2E=20Gra=C4=8Dnar?= Date: Thu, 8 Jun 2017 01:22:35 +0200 Subject: [PATCH 1/6] Added retrofit2 module. --- extras/pom.xml | 1 + extras/retrofit2/README.md | 57 ++++ extras/retrofit2/pom.xml | 63 ++++ .../extras/retrofit/AsyncHttpClientCall.java | 305 ++++++++++++++++++ .../retrofit/AsyncHttpClientCallFactory.java | 70 ++++ .../AsyncHttpClientCallFactoryTest.java | 73 +++++ .../retrofit/AsyncHttpClientCallTest.java | 216 +++++++++++++ .../AsyncHttpRetrofitIntegrationTest.java | 175 ++++++++++ .../extras/retrofit/TestServices.java | 70 ++++ 9 files changed, 1030 insertions(+) create mode 100644 extras/retrofit2/README.md create mode 100644 extras/retrofit2/pom.xml create mode 100644 extras/retrofit2/src/main/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCall.java create mode 100644 extras/retrofit2/src/main/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallFactory.java create mode 100644 extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallFactoryTest.java create mode 100644 extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallTest.java create mode 100644 extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpRetrofitIntegrationTest.java create mode 100644 extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/TestServices.java 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..32bfa18f02 --- /dev/null +++ b/extras/retrofit2/src/main/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCall.java @@ -0,0 +1,305 @@ +/* + * 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.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.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; + + /** + * Request customizer that is being invoked just before async-http-client request is being built. + */ + Consumer requestCustomizer; + + /** + * Consumer that gets called just before actual HTTP request is being fired. + */ + Consumer onRequestStart; + + /** + * Consumer that gets called when HTTP request finishes with an exception. + */ + Consumer onRequestFailure; + + /** + * Consumer that gets called when HTTP request finishes successfully. + */ + Consumer 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; + runConsumer(this.onRequestStart, this.request); + getHttpClient().executeRequest(asyncHttpClientRequest, new AsyncCompletionHandler() { + @Override + public void onThrowable(Throwable t) { + runConsumer(me.onRequestFailure, t); + future.completeExceptionally(t); + } + + @Override + public Response onCompleted(org.asynchttpclient.Response response) throws Exception { + val okHttpResponse = toOkhttpResponse(response); + runConsumer(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) + runConsumer(this.requestCustomizer, requestBuilder); + + return requestBuilder.build(); + } + + /** + * Safely runs specified consumer. + * + * @param consumer consumer (may be null) + * @param argument consumer argument + * @param consumer type. + */ + protected 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); + } + } + + private void throwAlreadyExecuted() { + throw new IllegalStateException("This call has already been executed."); + } +} \ No newline at end of file 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..cc1f01ae7c --- /dev/null +++ b/extras/retrofit2/src/main/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallFactory.java @@ -0,0 +1,70 @@ +/* + * 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.Value; +import okhttp3.Call; +import okhttp3.Request; +import okhttp3.Response; +import org.asynchttpclient.AsyncHttpClient; +import org.asynchttpclient.RequestBuilder; + +import java.util.function.Consumer; + +/** + * {@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; + + /** + * Consumer that gets called just before actual HTTP request is being fired. + */ + Consumer onRequestStart; + + /** + * Consumer that gets called when HTTP request finishes with an exception. + */ + Consumer onRequestFailure; + + /** + * Consumer that gets called when HTTP request finishes successfully. + */ + Consumer onRequestSuccess; + + /** + *

Request customizer that is being invoked just before async-http-client request is being built.

+ *

NOTE: You should NOT keep reference to request builder or related collections.

+ */ + Consumer requestCustomizer; + + @Override + public Call newCall(Request request) { + return AsyncHttpClientCall.builder() + .httpClient(httpClient) + .onRequestStart(getOnRequestStart()) + .onRequestSuccess(getOnRequestSuccess()) + .onRequestFailure(getOnRequestFailure()) + .requestCustomizer(getRequestCustomizer()) + .request(request) + .build(); + } +} \ No newline at end of file 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..04e97300f7 --- /dev/null +++ b/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallFactoryTest.java @@ -0,0 +1,73 @@ +/* + * 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.val; +import okhttp3.Request; +import okhttp3.Response; +import org.asynchttpclient.AsyncHttpClient; +import org.asynchttpclient.RequestBuilder; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.function.Consumer; + +import static org.mockito.Mockito.mock; + +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(); + Consumer onRequestFailure = createConsumer(); + Consumer onRequestSuccess = createConsumer(); + Consumer requestCustomizer = createConsumer(); + + // when + val factory = AsyncHttpClientCallFactory.builder() + .httpClient(httpClient) + .onRequestStart(onRequestStart) + .onRequestFailure(onRequestFailure) + .onRequestSuccess(onRequestSuccess) + .requestCustomizer(requestCustomizer) + .build(); + + // then + Assert.assertTrue(factory.getHttpClient() == httpClient); + Assert.assertTrue(factory.getOnRequestStart() == onRequestStart); + Assert.assertTrue(factory.getOnRequestFailure() == onRequestFailure); + Assert.assertTrue(factory.getOnRequestSuccess() == onRequestSuccess); + Assert.assertTrue(factory.getRequestCustomizer() == requestCustomizer); + + // when + val call = (AsyncHttpClientCall) factory.newCall(request); + + // then + Assert.assertNotNull(call); + Assert.assertTrue(call.request() == request); + Assert.assertTrue(call.getHttpClient() == httpClient); + Assert.assertTrue(call.getOnRequestStart() == onRequestStart); + Assert.assertTrue(call.getOnRequestFailure() == onRequestFailure); + Assert.assertTrue(call.getOnRequestSuccess() == onRequestSuccess); + Assert.assertTrue(call.getRequestCustomizer() == requestCustomizer); + } + + private Consumer createConsumer() { + return e -> { + }; + } + +} \ No newline at end of file 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..1b66ee3356 --- /dev/null +++ b/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallTest.java @@ -0,0 +1,216 @@ +/* + * 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.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@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 + Assert.assertTrue(call.isExecuted()); + Assert.assertFalse(call.isCanceled()); + Assert.assertTrue(numRequestCustomizer.get() == 1); // request customizer must be always invoked. + Assert.assertTrue(numStarted.get() == expectedStarted); + Assert.assertTrue(numOk.get() == expectedOk); + Assert.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 + Assert.assertTrue(clonedCall.isExecuted()); + Assert.assertFalse(clonedCall.isCanceled()); + Assert.assertTrue(numRequestCustomizer.get() == 2); // request customizer must be always invoked. + Assert.assertTrue(numStarted.get() == expectedStarted); + Assert.assertTrue(numOk.get() == expectedOk); + Assert.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); + Assert.assertTrue(result instanceof IOException); + + if (exception.getMessage() == null) { + Assert.assertTrue(result.getMessage() == exception.toString()); + } else { + Assert.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) { + // given + val call = AsyncHttpClientCall.builder() + .httpClient(mock(AsyncHttpClient.class)) + .request(REQUEST) + .build(); + + // when + call.runConsumer(consumer, argument); + + // then + Assert.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"}, + }; + } + + private void doThrow(String message) { + throw new RuntimeException(message); + } +} \ No newline at end of file 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..b1a0297154 --- /dev/null +++ b/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpRetrofitIntegrationTest.java @@ -0,0 +1,175 @@ +/* + * 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 org.asynchttpclient.AsyncHttpClient; +import org.asynchttpclient.DefaultAsyncHttpClient; +import org.asynchttpclient.DefaultAsyncHttpClientConfig; +import org.testng.Assert; +import org.testng.annotations.AfterSuite; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; +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.Collection; + +/** + * All tests in this test suite are disabled, because they call functionality of github service that is + * rate-limited. + */ +@Slf4j +public class AsyncHttpRetrofitIntegrationTest { + private static final String OWNER = "AsyncHttpClient"; + private static final String REPO = "async-http-client"; + + protected static AsyncHttpClient httpClient = createHttpClient(); + + 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); + } + + @AfterSuite + void cleanup() throws IOException { + httpClient.close(); + } + + @Test(enabled = false) + public void testSynchronousService() throws IOException { + // given + val callFactory = AsyncHttpClientCallFactory.builder().httpClient(httpClient).build(); + val retrofit = createRetrofitBuilder() + .callFactory(callFactory) + .build(); + val service = retrofit.create(TestServices.GithubSync.class); + + // when: + val contributors = service.contributors(OWNER, REPO).execute().body(); + + // then + assertContributors(contributors); + } + + @Test(enabled = false, dataProvider = "testRxJava1Service") + public void testRxJava1Service(RxJavaCallAdapterFactory rxJavaCallAdapterFactory) { + // given + val callFactory = AsyncHttpClientCallFactory.builder().httpClient(httpClient).build(); + val retrofit = createRetrofitBuilder() + .addCallAdapterFactory(rxJavaCallAdapterFactory) + .callFactory(callFactory) + .build(); + val service = retrofit.create(TestServices.GithubRxJava1.class); + + // when + val contributors = service.contributors(OWNER, REPO) + .subscribeOn(Schedulers.io()) + .toBlocking() + .first(); + + // then + assertContributors(contributors); + } + + @DataProvider(name = "testRxJava1Service") + Object[][] testRxJava1ServiceDataProvider() { + return new Object[][]{ + {RxJavaCallAdapterFactory.create()}, + {RxJavaCallAdapterFactory.createAsync()}, + {RxJavaCallAdapterFactory.createWithScheduler(Schedulers.io())}, + {RxJavaCallAdapterFactory.createWithScheduler(Schedulers.computation())}, + {RxJavaCallAdapterFactory.createWithScheduler(Schedulers.trampoline())}, + }; + } + + @Test(enabled = false, dataProvider = "testRxJava2Service") + public void testRxJava2Service(RxJava2CallAdapterFactory rxJava2CallAdapterFactory) { + // given + val callFactory = AsyncHttpClientCallFactory.builder().httpClient(httpClient).build(); + val retrofit = createRetrofitBuilder() + .addCallAdapterFactory(rxJava2CallAdapterFactory) + .callFactory(callFactory) + .build(); + val service = retrofit.create(TestServices.GithubRxJava2.class); + + // when + val contributors = service.contributors(OWNER, REPO) + .subscribeOn(io.reactivex.schedulers.Schedulers.computation()) + .blockingGet(); + + // then + assertContributors(contributors); + } + + @DataProvider(name = "testRxJava2Service") + Object[][] testRxJava2ServiceDataProvider() { + 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())}, + }; + } + + @Test(enabled = false) + void testGoogle() throws IOException { + // given + val callFactory = AsyncHttpClientCallFactory.builder().httpClient(httpClient).build(); + val retrofit = createRetrofitBuilder() + .callFactory(callFactory) + .baseUrl("http://www.google.com/") + .build(); + val service = retrofit.create(TestServices.Google.class); + + // when + val result = service.getHomePage().execute().body(); + log.debug("Received: {} bytes of output", result.length()); + + // then + Assert.assertNotNull(result, "Response body should not be null."); + Assert.assertTrue(result.length() > 1_000, "Response body is too short."); + } + + private Retrofit.Builder createRetrofitBuilder() { + return new Retrofit.Builder() + .addConverterFactory(ScalarsConverterFactory.create()) + .addConverterFactory(JacksonConverterFactory.create()) + .validateEagerly(true) + .baseUrl("https://api.github.com/"); + } + + private void assertContributors(Collection contributors) { + Assert.assertNotNull(contributors, "Contributors should not be null."); + log.info("Contributors: {} ->\n {}", contributors.size(), contributors); + Assert.assertTrue(contributors.size() >= 30, "There should be at least 30 contributors."); + contributors.forEach(e -> { + Assert.assertNotNull(e, "Contributor element should not be null"); + }); + } +} 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..6377c75274 --- /dev/null +++ b/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/TestServices.java @@ -0,0 +1,70 @@ +/* + * 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; + } + + public interface Google { + @GET("/") + Call getHomePage(); + } + + /** + * 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); + } +} From 1e357adf53519e6153decd754fdd36dce4370707 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brane=20F=2E=20Gra=C4=8Dnar?= Date: Sat, 10 Jun 2017 10:14:06 +0200 Subject: [PATCH 2/6] Added newlines at the end of files. --- .../asynchttpclient/extras/retrofit/AsyncHttpClientCall.java | 2 +- .../extras/retrofit/AsyncHttpClientCallFactory.java | 2 +- .../extras/retrofit/AsyncHttpClientCallFactoryTest.java | 3 +-- .../extras/retrofit/AsyncHttpClientCallTest.java | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) 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 index 32bfa18f02..d9e02ad483 100644 --- a/extras/retrofit2/src/main/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCall.java +++ b/extras/retrofit2/src/main/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCall.java @@ -302,4 +302,4 @@ protected void runConsumer(Consumer consumer, T argument) { private void throwAlreadyExecuted() { throw new IllegalStateException("This call has already been executed."); } -} \ No newline at end of file +} 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 index cc1f01ae7c..441040a371 100644 --- a/extras/retrofit2/src/main/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallFactory.java +++ b/extras/retrofit2/src/main/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallFactory.java @@ -67,4 +67,4 @@ public Call newCall(Request request) { .request(request) .build(); } -} \ No newline at end of file +} 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 index 04e97300f7..830218307d 100644 --- a/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallFactoryTest.java +++ b/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallFactoryTest.java @@ -69,5 +69,4 @@ private Consumer createConsumer() { return e -> { }; } - -} \ No newline at end of file +} 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 index 1b66ee3356..573d920efa 100644 --- a/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallTest.java +++ b/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallTest.java @@ -213,4 +213,4 @@ Object[][] dataProvider4th() { private void doThrow(String message) { throw new RuntimeException(message); } -} \ No newline at end of file +} From 3b2ccedd56bbf31980c592e6e7fa759c8ad6e282 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brane=20F=2E=20Gra=C4=8Dnar?= Date: Sat, 10 Jun 2017 10:51:39 +0200 Subject: [PATCH 3/6] Added static method runConsumers() and cleanup --- .../extras/retrofit/AsyncHttpClientCall.java | 17 ++++- .../retrofit/AsyncHttpClientCallTest.java | 74 +++++++++++++------ 2 files changed, 68 insertions(+), 23 deletions(-) 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 index d9e02ad483..c643d09f92 100644 --- a/extras/retrofit2/src/main/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCall.java +++ b/extras/retrofit2/src/main/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCall.java @@ -33,6 +33,7 @@ import org.asynchttpclient.RequestBuilder; import java.io.IOException; +import java.util.Collection; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -289,7 +290,7 @@ protected org.asynchttpclient.Request createRequest(@NonNull Request request) { * @param argument consumer argument * @param consumer type. */ - protected void runConsumer(Consumer consumer, T argument) { + protected static void runConsumer(Consumer consumer, T argument) { try { if (consumer != null) { consumer.accept(argument); @@ -299,6 +300,20 @@ protected void runConsumer(Consumer consumer, T argument) { } } + /** + * 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/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallTest.java b/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallTest.java index 573d920efa..2f04e1947d 100644 --- a/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallTest.java +++ b/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallTest.java @@ -25,14 +25,19 @@ 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 { @@ -102,12 +107,12 @@ void shouldInvokeConsumersOnEachExecution(Consumer handl } // then - Assert.assertTrue(call.isExecuted()); + assertTrue(call.isExecuted()); Assert.assertFalse(call.isCanceled()); - Assert.assertTrue(numRequestCustomizer.get() == 1); // request customizer must be always invoked. - Assert.assertTrue(numStarted.get() == expectedStarted); - Assert.assertTrue(numOk.get() == expectedOk); - Assert.assertTrue(numFailed.get() == expectedFailed); + 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); @@ -119,12 +124,12 @@ void shouldInvokeConsumersOnEachExecution(Consumer handl clonedCall.enqueue(null); // then - Assert.assertTrue(clonedCall.isExecuted()); + assertTrue(clonedCall.isExecuted()); Assert.assertFalse(clonedCall.isCanceled()); - Assert.assertTrue(numRequestCustomizer.get() == 2); // request customizer must be always invoked. - Assert.assertTrue(numStarted.get() == expectedStarted); - Assert.assertTrue(numOk.get() == expectedOk); - Assert.assertTrue(numFailed.get() == expectedFailed); + 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") @@ -164,12 +169,12 @@ void toIOExceptionShouldProduceExpectedResult(Throwable exception) { // then Assert.assertNotNull(result); - Assert.assertTrue(result instanceof IOException); + assertTrue(result instanceof IOException); if (exception.getMessage() == null) { - Assert.assertTrue(result.getMessage() == exception.toString()); + assertTrue(result.getMessage() == exception.toString()); } else { - Assert.assertTrue(result.getMessage() == exception.getMessage()); + assertTrue(result.getMessage() == exception.getMessage()); } } @@ -185,20 +190,13 @@ Object[][] dataProviderThird() { @Test(dataProvider = "4th") void runConsumerShouldTolerateBadConsumers(Consumer consumer, T argument) { - // given - val call = AsyncHttpClientCall.builder() - .httpClient(mock(AsyncHttpClient.class)) - .request(REQUEST) - .build(); - // when - call.runConsumer(consumer, argument); + runConsumer(consumer, argument); // then - Assert.assertTrue(true); + assertTrue(true); } - @DataProvider(name = "4th") Object[][] dataProvider4th() { return new Object[][]{ @@ -210,7 +208,39 @@ Object[][] dataProvider4th() { }; } + @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(); + } } From 5ac6b778a4551dc1630dbcc981050e1f362f2491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brane=20F=2E=20Gra=C4=8Dnar?= Date: Sat, 10 Jun 2017 10:53:15 +0200 Subject: [PATCH 4/6] Introduced concept of call builder customizers, cleanup --- .../retrofit/AsyncHttpClientCallFactory.java | 43 +++++-------- .../AsyncHttpClientCallFactoryTest.java | 62 ++++++++++++------- 2 files changed, 54 insertions(+), 51 deletions(-) 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 index 441040a371..0376628b7e 100644 --- a/extras/retrofit2/src/main/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallFactory.java +++ b/extras/retrofit2/src/main/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallFactory.java @@ -14,15 +14,18 @@ import lombok.Builder; import lombok.NonNull; +import lombok.Singular; import lombok.Value; +import lombok.val; import okhttp3.Call; import okhttp3.Request; -import okhttp3.Response; import org.asynchttpclient.AsyncHttpClient; -import org.asynchttpclient.RequestBuilder; +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} */ @@ -36,35 +39,21 @@ public class AsyncHttpClientCallFactory implements Call.Factory { AsyncHttpClient httpClient; /** - * Consumer that gets called just before actual HTTP request is being fired. - */ - Consumer onRequestStart; - - /** - * Consumer that gets called when HTTP request finishes with an exception. - */ - Consumer onRequestFailure; - - /** - * Consumer that gets called when HTTP request finishes successfully. - */ - Consumer onRequestSuccess; - - /** - *

Request customizer that is being invoked just before async-http-client request is being built.

- *

NOTE: You should NOT keep reference to request builder or related collections.

+ * List of {@link Call} builder customizers that are invoked just before creating it. */ - Consumer requestCustomizer; + @Singular("callCustomizer") + List> callCustomizers; @Override public Call newCall(Request request) { - return AsyncHttpClientCall.builder() + val callBuilder = AsyncHttpClientCall.builder() .httpClient(httpClient) - .onRequestStart(getOnRequestStart()) - .onRequestSuccess(getOnRequestSuccess()) - .onRequestFailure(getOnRequestFailure()) - .requestCustomizer(getRequestCustomizer()) - .request(request) - .build(); + .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 index 830218307d..1d6c73a3e5 100644 --- a/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallFactoryTest.java +++ b/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallFactoryTest.java @@ -20,9 +20,12 @@ import org.testng.Assert; import org.testng.annotations.Test; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; +import static org.asynchttpclient.extras.retrofit.AsyncHttpClientCallTest.createConsumer; import static org.mockito.Mockito.mock; +import static org.testng.Assert.assertTrue; public class AsyncHttpClientCallFactoryTest { @Test @@ -31,42 +34,53 @@ void newCallShouldProduceExpectedResult() { val request = new Request.Builder().url("http://www.google.com/").build(); val httpClient = mock(AsyncHttpClient.class); - Consumer onRequestStart = createConsumer(); - Consumer onRequestFailure = createConsumer(); - Consumer onRequestSuccess = createConsumer(); - Consumer requestCustomizer = createConsumer(); + Consumer onRequestStart = createConsumer(new AtomicInteger()); + Consumer onRequestFailure = createConsumer(new AtomicInteger()); + Consumer onRequestSuccess = createConsumer(new AtomicInteger()); + Consumer requestCustomizer = createConsumer(new AtomicInteger()); - // when + // 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) - .onRequestStart(onRequestStart) - .onRequestFailure(onRequestFailure) - .onRequestSuccess(onRequestSuccess) - .requestCustomizer(requestCustomizer) + .callCustomizer(callBuilderConsumer1) + .callCustomizer(callBuilderConsumer2) .build(); // then - Assert.assertTrue(factory.getHttpClient() == httpClient); - Assert.assertTrue(factory.getOnRequestStart() == onRequestStart); - Assert.assertTrue(factory.getOnRequestFailure() == onRequestFailure); - Assert.assertTrue(factory.getOnRequestSuccess() == onRequestSuccess); - Assert.assertTrue(factory.getRequestCustomizer() == requestCustomizer); + 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 Assert.assertNotNull(call); - Assert.assertTrue(call.request() == request); - Assert.assertTrue(call.getHttpClient() == httpClient); - Assert.assertTrue(call.getOnRequestStart() == onRequestStart); - Assert.assertTrue(call.getOnRequestFailure() == onRequestFailure); - Assert.assertTrue(call.getOnRequestSuccess() == onRequestSuccess); - Assert.assertTrue(call.getRequestCustomizer() == requestCustomizer); - } + assertTrue(customizer1Called.get() == 1); + assertTrue(customizer2Called.get() == 1); - private Consumer createConsumer() { - return e -> { - }; + assertTrue(call.request() == request); + assertTrue(call.getHttpClient() == httpClient); + assertTrue(call.getOnRequestStart() == onRequestStart); + assertTrue(call.getOnRequestFailure() == onRequestFailure); + assertTrue(call.getOnRequestSuccess() == onRequestSuccess); + assertTrue(call.getRequestCustomizer() == requestCustomizer); } } From 8744424aded41e0aba56f62e64f1dc1e30865f14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brane=20F=2E=20Gra=C4=8Dnar?= Date: Mon, 12 Jun 2017 13:14:21 +0200 Subject: [PATCH 5/6] Refactored integration tests. --- .../AsyncHttpClientCallFactoryTest.java | 64 ++- .../AsyncHttpRetrofitIntegrationTest.java | 390 +++++++++++++++--- .../extras/retrofit/TestServices.java | 5 - 3 files changed, 390 insertions(+), 69 deletions(-) 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 index 1d6c73a3e5..0d8c6228dd 100644 --- a/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallFactoryTest.java +++ b/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallFactoryTest.java @@ -17,15 +17,17 @@ import okhttp3.Response; import org.asynchttpclient.AsyncHttpClient; import org.asynchttpclient.RequestBuilder; -import org.testng.Assert; 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.assertTrue; +import static org.testng.Assert.*; public class AsyncHttpClientCallFactoryTest { @Test @@ -72,7 +74,7 @@ void newCallShouldProduceExpectedResult() { val call = (AsyncHttpClientCall) factory.newCall(request); // then - Assert.assertNotNull(call); + assertNotNull(call); assertTrue(customizer1Called.get() == 1); assertTrue(customizer2Called.get() == 1); @@ -83,4 +85,60 @@ void newCallShouldProduceExpectedResult() { assertTrue(call.getOnRequestSuccess() == onRequestSuccess); assertTrue(call.getRequestCustomizer() == 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) + .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()); + assertNotNull(call.getOnRequestSuccess()); + assertNotNull(call.getOnRequestFailure()); + assertNotNull(call.getRequestCustomizer()); + } } 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 index b1a0297154..d370e38c9a 100644 --- a/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpRetrofitIntegrationTest.java +++ b/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpRetrofitIntegrationTest.java @@ -12,15 +12,20 @@ */ 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.testng.Assert; +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; @@ -29,18 +34,31 @@ 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 { +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"; - protected static AsyncHttpClient httpClient = createHttpClient(); + private static final AsyncHttpClient httpClient = createHttpClient(); + private static HttpServer server; + + private List expectedContributors; private static AsyncHttpClient createHttpClient() { val config = new DefaultAsyncHttpClientConfig.Builder() @@ -55,49 +73,195 @@ private static AsyncHttpClient createHttpClient() { 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(); } - @Test(enabled = false) - public void testSynchronousService() throws IOException { + // 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 - // when: - val contributors = service.contributors(OWNER, REPO).execute().body(); + // 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(contributors); + assertContributors(expectedContributors, resultRef.get()); } - @Test(enabled = false, dataProvider = "testRxJava1Service") - public void testRxJava1Service(RxJavaCallAdapterFactory rxJavaCallAdapterFactory) { + @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(); - val service = retrofit.create(TestServices.GithubRxJava1.class); - - // when - val contributors = service.contributors(OWNER, REPO) - .subscribeOn(Schedulers.io()) - .toBlocking() - .first(); - - // then - assertContributors(contributors); + return retrofit.create(TestServices.GithubRxJava1.class); } @DataProvider(name = "testRxJava1Service") - Object[][] testRxJava1ServiceDataProvider() { + Object[][] testRxJava1Service_DataProvider() { return new Object[][]{ {RxJavaCallAdapterFactory.create()}, {RxJavaCallAdapterFactory.createAsync()}, @@ -106,70 +270,174 @@ Object[][] testRxJava1ServiceDataProvider() { {RxJavaCallAdapterFactory.createWithScheduler(Schedulers.trampoline())}, }; } + // end: rxjava 1.x - @Test(enabled = false, dataProvider = "testRxJava2Service") - public void testRxJava2Service(RxJava2CallAdapterFactory rxJava2CallAdapterFactory) { + // begin: rxjava 2.x + @Test(dataProvider = "testRxJava2Service") + public void testRxJava2Service_OK(RxJava2CallAdapterFactory rxJavaCallAdapterFactory) throws Throwable { // given - val callFactory = AsyncHttpClientCallFactory.builder().httpClient(httpClient).build(); - val retrofit = createRetrofitBuilder() - .addCallAdapterFactory(rxJava2CallAdapterFactory) - .callFactory(callFactory) - .build(); - val service = retrofit.create(TestServices.GithubRxJava2.class); + val service = rxjava2Setup(rxJavaCallAdapterFactory); + val expectedContributors = generateContributors(); // when - val contributors = service.contributors(OWNER, REPO) - .subscribeOn(io.reactivex.schedulers.Schedulers.computation()) - .blockingGet(); + 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(contributors); + assertContributors(expectedContributors, resultRef.get()); } - @DataProvider(name = "testRxJava2Service") - Object[][] testRxJava2ServiceDataProvider() { - 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())}, - }; + @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(enabled = false) - void testGoogle() throws IOException { + @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) - .baseUrl("http://www.google.com/") .build(); - val service = retrofit.create(TestServices.Google.class); - - // when - val result = service.getHomePage().execute().body(); - log.debug("Received: {} bytes of output", result.length()); + return retrofit.create(TestServices.GithubRxJava2.class); + } - // then - Assert.assertNotNull(result, "Response body should not be null."); - Assert.assertTrue(result.length() > 1_000, "Response body is too short."); + @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()) + .addConverterFactory(JacksonConverterFactory.create(objectMapper)) .validateEagerly(true) - .baseUrl("https://api.github.com/"); + .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 void assertContributors(Collection contributors) { - Assert.assertNotNull(contributors, "Contributors should not be null."); - log.info("Contributors: {} ->\n {}", contributors.size(), contributors); - Assert.assertTrue(contributors.size() >= 30, "There should be at least 30 contributors."); - contributors.forEach(e -> { - Assert.assertNotNull(e, "Contributor element should not be null"); + 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 index 6377c75274..e7e2e77dca 100644 --- a/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/TestServices.java +++ b/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/TestServices.java @@ -39,11 +39,6 @@ public static class Contributor implements Serializable { int contributions; } - public interface Google { - @GET("/") - Call getHomePage(); - } - /** * Synchronous interface */ From 2bba0b9c4aad6b5d25cc33fc9289ff7c97ba7e7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brane=20F=2E=20Gra=C4=8Dnar?= Date: Mon, 12 Jun 2017 13:25:15 +0200 Subject: [PATCH 6/6] Added multiple consumers support for each Call event type. --- .../extras/retrofit/AsyncHttpClientCall.java | 30 +++++++++++-------- .../AsyncHttpClientCallFactoryTest.java | 24 +++++++++++---- 2 files changed, 36 insertions(+), 18 deletions(-) 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 index c643d09f92..00cb272b16 100644 --- a/extras/retrofit2/src/main/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCall.java +++ b/extras/retrofit2/src/main/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCall.java @@ -16,6 +16,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NonNull; +import lombok.Singular; import lombok.SneakyThrows; import lombok.Value; import lombok.extern.slf4j.Slf4j; @@ -34,6 +35,7 @@ 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; @@ -76,24 +78,28 @@ class AsyncHttpClientCall implements Cloneable, okhttp3.Call { Request request; /** - * Request customizer that is being invoked just before async-http-client request is being built. + * List of consumers that get called just before actual async-http-client request is being built. */ - Consumer requestCustomizer; + @Singular("requestCustomizer") + List> requestCustomizers; /** - * Consumer that gets called just before actual HTTP request is being fired. + * List of consumers that get called just before actual HTTP request is being fired. */ - Consumer onRequestStart; + @Singular("onRequestStart") + List> onRequestStart; /** - * Consumer that gets called when HTTP request finishes with an exception. + * List of consumers that get called when HTTP request finishes with an exception. */ - Consumer onRequestFailure; + @Singular("onRequestFailure") + List> onRequestFailure; /** - * Consumer that gets called when HTTP request finishes successfully. + * List of consumers that get called when HTTP request finishes successfully. */ - Consumer onRequestSuccess; + @Singular("onRequestSuccess") + List> onRequestSuccess; /** * Tells whether call has been executed. @@ -191,18 +197,18 @@ protected CompletableFuture executeHttpRequest() { // execute the request. val me = this; - runConsumer(this.onRequestStart, this.request); + runConsumers(this.onRequestStart, this.request); getHttpClient().executeRequest(asyncHttpClientRequest, new AsyncCompletionHandler() { @Override public void onThrowable(Throwable t) { - runConsumer(me.onRequestFailure, t); + runConsumers(me.onRequestFailure, t); future.completeExceptionally(t); } @Override public Response onCompleted(org.asynchttpclient.Response response) throws Exception { val okHttpResponse = toOkhttpResponse(response); - runConsumer(me.onRequestSuccess, okHttpResponse); + runConsumers(me.onRequestSuccess, okHttpResponse); future.complete(okHttpResponse); return okHttpResponse; } @@ -278,7 +284,7 @@ protected org.asynchttpclient.Request createRequest(@NonNull Request request) { } // customize the request builder (external customizer can change the request url for example) - runConsumer(this.requestCustomizer, requestBuilder); + runConsumers(this.requestCustomizers, requestBuilder); return requestBuilder.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 index 0d8c6228dd..58eef1c91c 100644 --- a/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallFactoryTest.java +++ b/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallFactoryTest.java @@ -12,6 +12,7 @@ */ package org.asynchttpclient.extras.retrofit; +import lombok.extern.slf4j.Slf4j; import lombok.val; import okhttp3.Request; import okhttp3.Response; @@ -29,6 +30,7 @@ import static org.mockito.Mockito.mock; import static org.testng.Assert.*; +@Slf4j public class AsyncHttpClientCallFactoryTest { @Test void newCallShouldProduceExpectedResult() { @@ -80,10 +82,11 @@ void newCallShouldProduceExpectedResult() { assertTrue(call.request() == request); assertTrue(call.getHttpClient() == httpClient); - assertTrue(call.getOnRequestStart() == onRequestStart); - assertTrue(call.getOnRequestFailure() == onRequestFailure); - assertTrue(call.getOnRequestSuccess() == onRequestSuccess); - assertTrue(call.getRequestCustomizer() == requestCustomizer); + + 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 @@ -107,7 +110,9 @@ void shouldApplyAllConsumersToCallBeingConstructed() throws IOException { }; Consumer callCustomizer = callBuilder -> { - callBuilder.requestCustomizer(requestCustomizer) + callBuilder + .requestCustomizer(requestCustomizer) + .requestCustomizer(rb -> log.warn("I'm customizing: {}", rb)) .onRequestSuccess(createConsumer(numRequestSuccess)) .onRequestFailure(createConsumer(numRequestFailure)) .onRequestStart(createConsumer(numRequestStart)); @@ -137,8 +142,15 @@ void shouldApplyAllConsumersToCallBeingConstructed() throws IOException { // 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()); - assertNotNull(call.getRequestCustomizer()); + assertTrue(call.getOnRequestFailure().size() == 1); + + assertNotNull(call.getRequestCustomizers()); + assertTrue(call.getRequestCustomizers().size() == 2); } }