diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index a35115c7e02..2d359c0d663 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -102,14 +102,15 @@ dependencies { testCompile "org.springframework:spring-test:${springVersion}" testCompile "org.slf4j:slf4j-jcl:1.7.12" testCompile "org.slf4j:jul-to-slf4j:1.7.12" - testCompile("log4j:log4j:1.2.16") + testCompile "log4j:log4j:1.2.16" testCompile("org.mockito:mockito-core:1.10.19") { exclude group: 'org.hamcrest', module: 'hamcrest-core' } - testCompile("org.hamcrest:hamcrest-all:1.3") + testCompile "org.hamcrest:hamcrest-all:1.3" + testCompile "com.squareup.okhttp3:mockwebserver:3.0.1" // Needed to run Javadoc without error - optional("org.apache.httpcomponents:httpclient:4.5.1") + optional "org.apache.httpcomponents:httpclient:4.5.1" } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultWebResponse.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultWebResponse.java new file mode 100644 index 00000000000..328d1198e2e --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultWebResponse.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.client.reactive; + +import java.util.List; + +import reactor.core.publisher.Mono; + +import org.springframework.core.codec.Decoder; +import org.springframework.http.client.reactive.ClientHttpResponse; + +/** + * Default implementation of the {@link WebResponse} interface + * + * @author Brian Clozel + */ +public class DefaultWebResponse implements WebResponse { + + private final Mono clientResponse; + + private final List> messageDecoders; + + + public DefaultWebResponse(Mono clientResponse, List> messageDecoders) { + this.clientResponse = clientResponse; + this.messageDecoders = messageDecoders; + } + + @Override + public Mono getClientResponse() { + return this.clientResponse; + } + + @Override + public List> getMessageDecoders() { + return this.messageDecoders; + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebClient.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebClient.java new file mode 100644 index 00000000000..d733acc8037 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebClient.java @@ -0,0 +1,139 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.client.reactive; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; + +import reactor.core.publisher.Mono; + +import org.springframework.core.codec.Decoder; +import org.springframework.core.codec.Encoder; +import org.springframework.core.codec.support.ByteBufferDecoder; +import org.springframework.core.codec.support.ByteBufferEncoder; +import org.springframework.core.codec.support.JacksonJsonDecoder; +import org.springframework.core.codec.support.JacksonJsonEncoder; +import org.springframework.core.codec.support.JsonObjectDecoder; +import org.springframework.core.codec.support.StringDecoder; +import org.springframework.core.codec.support.StringEncoder; +import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.core.io.buffer.DefaultDataBufferAllocator; +import org.springframework.http.HttpStatus; +import org.springframework.http.client.reactive.ClientHttpRequest; +import org.springframework.http.client.reactive.ClientHttpRequestFactory; +import org.springframework.http.client.reactive.ClientHttpResponse; + +/** + * Reactive Web client supporting the HTTP/1.1 protocol + * + *

Here is a simple example of a GET request: + *

+ * WebClient client = new WebClient(new ReactorHttpClientRequestFactory());
+ * Mono<String> result = client
+ * 		.perform(HttpRequestBuilders.get("http://example.org/resource")
+ * 			.accept(MediaType.TEXT_PLAIN))
+ * 		.extract(WebResponseExtractors.body(String.class));
+ * 
+ * + *

This Web client relies on + *

    + *
  • a {@link ClientHttpRequestFactory} that drives the underlying library (e.g. Reactor-Net, RxNetty...)
  • + *
  • an {@link HttpRequestBuilder} which create a Web request with a builder API (see {@link HttpRequestBuilders})
  • + *
  • an {@link WebResponseExtractor} which extracts the relevant part of the server response + * with the composition API of choice (see {@link WebResponseExtractors}
  • + *
+ * + * @author Brian Clozel + * @see HttpRequestBuilders + * @see WebResponseExtractors + */ +public final class WebClient { + + private ClientHttpRequestFactory requestFactory; + + private List> messageEncoders; + + private List> messageDecoders; + + /** + * Create a {@code ReactiveRestClient} instance, using the {@link ClientHttpRequestFactory} + * implementation given as an argument to drive the underlying HTTP client implementation. + * + * Register by default the following Encoders and Decoders: + *
    + *
  • {@link ByteBufferEncoder} / {@link ByteBufferDecoder}
  • + *
  • {@link StringEncoder} / {@link StringDecoder}
  • + *
  • {@link JacksonJsonEncoder} / {@link JacksonJsonDecoder}
  • + *
+ * + * @param requestFactory the {@code ClientHttpRequestFactory} to use + */ + public WebClient(ClientHttpRequestFactory requestFactory) { + this.requestFactory = requestFactory; + DataBufferAllocator allocator = new DefaultDataBufferAllocator(); + this.messageEncoders = Arrays.asList(new ByteBufferEncoder(allocator), new StringEncoder(allocator), + new JacksonJsonEncoder(allocator)); + this.messageDecoders = Arrays.asList(new ByteBufferDecoder(), new StringDecoder(allocator), + new JacksonJsonDecoder(new JsonObjectDecoder(allocator))); + } + + /** + * Set the list of {@link Encoder}s to use for encoding messages + */ + public void setMessageEncoders(List> messageEncoders) { + this.messageEncoders = messageEncoders; + } + + /** + * Set the list of {@link Decoder}s to use for decoding messages + */ + public void setMessageDecoders(List> messageDecoders) { + this.messageDecoders = messageDecoders; + } + + /** + * Perform the actual HTTP request/response exchange + * + *

Pulling demand from the exposed {@code Flux} will result in: + *

    + *
  • building the actual HTTP request using the provided {@code RequestBuilder}
  • + *
  • encoding the HTTP request body with the configured {@code Encoder}s
  • + *
  • returning the response with a publisher of the body
  • + *
+ */ + public WebResponseActions perform(DefaultHttpRequestBuilder builder) { + + ClientHttpRequest request = builder.setMessageEncoders(messageEncoders).build(requestFactory); + final Mono clientResponse = request.execute() + .log("org.springframework.http.client.reactive"); + + return new WebResponseActions() { + @Override + public void doWithStatus(Consumer consumer) { + // TODO: implement + } + + @Override + public T extract(WebResponseExtractor extractor) { + return extractor.extract(new DefaultWebResponse(clientResponse, messageDecoders)); + } + + }; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponse.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponse.java new file mode 100644 index 00000000000..6a92847dd19 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponse.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.client.reactive; + +import java.util.List; + +import reactor.core.publisher.Mono; + +import org.springframework.core.codec.Decoder; +import org.springframework.http.client.reactive.ClientHttpResponse; + +/** + * Result of a {@code ClientHttpRequest} sent to a remote server by the {@code WebClient} + * + *

Contains all the required information to extract relevant information from the raw response. + * + * @author Brian Clozel + */ +public interface WebResponse { + + /** + * Return the raw response received by the {@code WebClient} + */ + Mono getClientResponse(); + + /** + * Return the configured list of {@link Decoder}s that can be used to decode the raw response body + */ + List> getMessageDecoders(); +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponseActions.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponseActions.java new file mode 100644 index 00000000000..e18e2decdfc --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponseActions.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.client.reactive; + +import java.util.function.Consumer; + +import org.springframework.http.HttpStatus; + +/** + * Allows applying actions, such as extractors, on the result of an executed + * {@link WebClient} request. + * + * @author Brian Clozel + */ +public interface WebResponseActions { + + /** + * Apply synchronous operations once the HTTP response status + * has been received. + */ + void doWithStatus(Consumer consumer); + + /** + * Perform an extraction of the response body into a higher level representation. + * + *

+	 * static imports: HttpRequestBuilders.*, HttpResponseExtractors.*
+	 *
+	 * webClient
+	 *   .perform(get(baseUrl.toString()).accept(MediaType.TEXT_PLAIN))
+	 *   .extract(response(String.class));
+	 * 
+ */ + T extract(WebResponseExtractor extractor); +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponseExtractor.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponseExtractor.java new file mode 100644 index 00000000000..2bb1592447e --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponseExtractor.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.client.reactive; + +/** + * A {@code WebResponseExtractor} extracts the relevant part of a + * raw {@link org.springframework.http.client.reactive.ClientHttpResponse}, + * optionally decoding the response body and using a target composition API. + * + *

See static factory methods in {@link WebResponseExtractors}. + * + * @author Brian Clozel + */ +public interface WebResponseExtractor { + + T extract(WebResponse webResponse); +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponseExtractors.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponseExtractors.java new file mode 100644 index 00000000000..9c24c76d35b --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponseExtractors.java @@ -0,0 +1,123 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.client.reactive; + +import java.nio.charset.Charset; +import java.util.List; +import java.util.Optional; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.core.ResolvableType; +import org.springframework.core.codec.Decoder; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.reactive.ClientHttpResponse; + +/** + * Static factory methods for {@link WebResponseExtractor} + * based on the {@link Flux} and {@link Mono} API. + * + * @author Brian Clozel + */ +public class WebResponseExtractors { + + private static final Charset UTF_8 = Charset.forName("UTF-8"); + + private static final Object[] HINTS = new Object[] {UTF_8}; + + /** + * Extract the response body and decode it, returning it as a {@code Mono} + */ + public static WebResponseExtractor> body(Class sourceClass) { + + ResolvableType resolvableType = ResolvableType.forClass(sourceClass); + //noinspection unchecked + return webResponse -> (Mono) webResponse.getClientResponse() + .flatMap(resp -> decodeResponseBody(resp, resolvableType, webResponse.getMessageDecoders())) + .next(); + } + + /** + * Extract the response body and decode it, returning it as a {@code Flux} + */ + public static WebResponseExtractor> bodyStream(Class sourceClass) { + + ResolvableType resolvableType = ResolvableType.forClass(sourceClass); + return webResponse -> webResponse.getClientResponse() + .flatMap(resp -> decodeResponseBody(resp, resolvableType, webResponse.getMessageDecoders())); + } + + /** + * Extract the full response body as a {@code ResponseEntity} + * with its body decoded as a single type {@code T} + */ + public static WebResponseExtractor>> response(Class sourceClass) { + + ResolvableType resolvableType = ResolvableType.forClass(sourceClass); + return webResponse -> webResponse.getClientResponse() + .then(response -> + Mono.when( + decodeResponseBody(response, resolvableType, webResponse.getMessageDecoders()).next(), + Mono.just(response.getHeaders()), + Mono.just(response.getStatusCode()))) + .map(tuple -> { + //noinspection unchecked + return new ResponseEntity<>((T) tuple.getT1(), tuple.getT2(), tuple.getT3()); + }); + } + + /** + * Extract the full response body as a {@code ResponseEntity} + * with its body decoded as a {@code Flux} + */ + public static WebResponseExtractor>>> responseStream(Class sourceClass) { + ResolvableType resolvableType = ResolvableType.forClass(sourceClass); + return webResponse -> webResponse.getClientResponse() + .map(response -> new ResponseEntity<>( + decodeResponseBody(response, resolvableType, webResponse.getMessageDecoders()), + response.getHeaders(), response.getStatusCode())); + } + + /** + * Extract the response headers as an {@code HttpHeaders} instance + */ + public static WebResponseExtractor> headers() { + return webResponse -> webResponse.getClientResponse().map(resp -> resp.getHeaders()); + } + + protected static Flux decodeResponseBody(ClientHttpResponse response, ResolvableType responseType, + List> messageDecoders) { + + MediaType contentType = response.getHeaders().getContentType(); + Optional> decoder = resolveDecoder(messageDecoders, responseType, contentType); + if (!decoder.isPresent()) { + return Flux.error(new IllegalStateException("Could not decode response body of type '" + contentType + + "' with target type '" + responseType.toString() + "'")); + } + //noinspection unchecked + return (Flux) decoder.get().decode(response.getBody(), responseType, contentType, HINTS); + } + + + protected static Optional> resolveDecoder(List> messageDecoders, ResolvableType type, + MediaType mediaType) { + return messageDecoders.stream().filter(e -> e.canDecode(type, mediaType)).findFirst(); + } +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/WebClientIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/WebClientIntegrationTests.java new file mode 100644 index 00000000000..d77ac035ddd --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/WebClientIntegrationTests.java @@ -0,0 +1,249 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.client.reactive; + +import static org.junit.Assert.*; +import static org.springframework.web.client.reactive.HttpRequestBuilders.*; +import static org.springframework.web.client.reactive.WebResponseExtractors.*; + +import okhttp3.HttpUrl; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.hamcrest.Matchers; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.test.TestSubscriber; +import reactor.fn.Consumer; + +import org.springframework.core.codec.support.Pojo; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.reactive.ReactorHttpClientRequestFactory; + +/** + * @author Brian Clozel + */ +public class WebClientIntegrationTests { + + private MockWebServer server; + + private WebClient webClient; + + @Before + public void setup() { + this.server = new MockWebServer(); + this.webClient = new WebClient(new ReactorHttpClientRequestFactory()); + } + + @Test + public void shouldGetHeaders() throws Exception { + + HttpUrl baseUrl = server.url("/greeting?name=Spring"); + this.server.enqueue(new MockResponse().setHeader("Content-Type", "text/plain").setBody("Hello Spring!")); + + Mono result = this.webClient + .perform(get(baseUrl.toString())) + .extract(headers()); + + TestSubscriber ts = new TestSubscriber(); + result.subscribe(ts); + ts.awaitAndAssertValuesWith( + httpHeaders -> { + assertEquals(MediaType.TEXT_PLAIN, httpHeaders.getContentType()); + assertEquals(13L, httpHeaders.getContentLength()); + } + ).assertComplete(); + + RecordedRequest request = server.takeRequest(); + assertEquals("*/*", request.getHeader(HttpHeaders.ACCEPT)); + assertEquals("/greeting?name=Spring", request.getPath()); + } + + @Test + public void shouldGetPlainTextResponseAsObject() throws Exception { + + HttpUrl baseUrl = server.url("/greeting?name=Spring"); + this.server.enqueue(new MockResponse().setBody("Hello Spring!")); + + Mono result = this.webClient + .perform(get(baseUrl.toString()) + .header("X-Test-Header", "testvalue")) + .extract(body(String.class)); + + + TestSubscriber ts = new TestSubscriber(); + result.subscribe(ts); + ts.awaitAndAssertValues("Hello Spring!").assertComplete(); + + RecordedRequest request = server.takeRequest(); + assertEquals("testvalue", request.getHeader("X-Test-Header")); + assertEquals("*/*", request.getHeader(HttpHeaders.ACCEPT)); + assertEquals("/greeting?name=Spring", request.getPath()); + } + + @Test + public void shouldGetPlainTextResponse() throws Exception { + + HttpUrl baseUrl = server.url("/greeting?name=Spring"); + this.server.enqueue(new MockResponse().setHeader("Content-Type", "text/plain").setBody("Hello Spring!")); + + Mono> result = this.webClient + .perform(get(baseUrl.toString()) + .accept(MediaType.TEXT_PLAIN)) + .extract(response(String.class)); + + TestSubscriber> ts = new TestSubscriber(); + result.subscribe(ts); + ts.awaitAndAssertValuesWith(new Consumer>() { + @Override + public void accept(ResponseEntity response) { + assertEquals(200, response.getStatusCode().value()); + assertEquals(MediaType.TEXT_PLAIN, response.getHeaders().getContentType()); + assertEquals("Hello Spring!", response.getBody()); + } + }); + RecordedRequest request = server.takeRequest(); + assertEquals("/greeting?name=Spring", request.getPath()); + assertEquals("text/plain", request.getHeader(HttpHeaders.ACCEPT)); + } + + @Test + public void shouldGetJsonAsMonoOfPojo() throws Exception { + + HttpUrl baseUrl = server.url("/pojo"); + this.server.enqueue(new MockResponse().setHeader("Content-Type", "application/json") + .setBody("{\"bar\":\"barbar\",\"foo\":\"foofoo\"}")); + + Mono result = this.webClient + .perform(get(baseUrl.toString()) + .accept(MediaType.APPLICATION_JSON)) + .extract(body(Pojo.class)); + + TestSubscriber ts = new TestSubscriber(); + result.subscribe(ts); + ts.awaitAndAssertValuesWith(p -> assertEquals("barbar", p.getBar())).assertComplete(); + RecordedRequest request = server.takeRequest(); + assertEquals("/pojo", request.getPath()); + assertEquals("application/json", request.getHeader(HttpHeaders.ACCEPT)); + } + + @Test + public void shouldGetJsonAsFluxOfPojos() throws Exception { + + HttpUrl baseUrl = server.url("/pojos"); + this.server.enqueue(new MockResponse().setHeader("Content-Type", "application/json") + .setBody("[{\"bar\":\"bar1\",\"foo\":\"foo1\"},{\"bar\":\"bar2\",\"foo\":\"foo2\"}]")); + + Flux result = this.webClient + .perform(get(baseUrl.toString()) + .accept(MediaType.APPLICATION_JSON)) + .extract(bodyStream(Pojo.class)); + + TestSubscriber ts = new TestSubscriber(); + result.subscribe(ts); + ts.awaitAndAssertValuesWith( + p -> assertThat(p.getBar(), Matchers.is("bar1")), + p -> assertThat(p.getBar(), Matchers.is("bar2")) + ).assertValueCount(2).assertComplete(); + RecordedRequest request = server.takeRequest(); + assertEquals("/pojos", request.getPath()); + assertEquals("application/json", request.getHeader(HttpHeaders.ACCEPT)); + } + + @Test + public void shouldGetJsonAsResponseOfPojosStream() throws Exception { + + HttpUrl baseUrl = server.url("/pojos"); + this.server.enqueue(new MockResponse().setHeader("Content-Type", "application/json") + .setBody("[{\"bar\":\"bar1\",\"foo\":\"foo1\"},{\"bar\":\"bar2\",\"foo\":\"foo2\"}]")); + + Mono>> result = this.webClient + .perform(get(baseUrl.toString()) + .accept(MediaType.APPLICATION_JSON)) + .extract(responseStream(Pojo.class)); + + TestSubscriber>> ts = new TestSubscriber(); + result.subscribe(ts); + ts.awaitAndAssertValuesWith( + response -> { + assertEquals(200, response.getStatusCode().value()); + assertEquals(MediaType.APPLICATION_JSON, response.getHeaders().getContentType()); + } + ).assertComplete(); + RecordedRequest request = server.takeRequest(); + assertEquals("/pojos", request.getPath()); + assertEquals("application/json", request.getHeader(HttpHeaders.ACCEPT)); + } + + @Test + public void shouldPostPojoAsJson() throws Exception { + + HttpUrl baseUrl = server.url("/pojo/capitalize"); + this.server.enqueue(new MockResponse().setBody("{\"bar\":\"BARBAR\",\"foo\":\"FOOFOO\"}")); + + Pojo spring = new Pojo("foofoo", "barbar"); + Mono result = this.webClient + .perform(post(baseUrl.toString()) + .content(spring) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .extract(body(Pojo.class)); + + TestSubscriber ts = new TestSubscriber(); + result.subscribe(ts); + ts.awaitAndAssertValuesWith(p -> assertEquals("BARBAR", p.getBar())).assertComplete(); + + RecordedRequest request = server.takeRequest(); + assertEquals("/pojo/capitalize", request.getPath()); + assertEquals("{\"foo\":\"foofoo\",\"bar\":\"barbar\"}", request.getBody().readUtf8()); + assertEquals("chunked", request.getHeader(HttpHeaders.TRANSFER_ENCODING)); + assertEquals("application/json", request.getHeader(HttpHeaders.ACCEPT)); + assertEquals("application/json", request.getHeader(HttpHeaders.CONTENT_TYPE)); + } + + @Test + public void shouldGetErrorWhen404() throws Exception { + + HttpUrl baseUrl = server.url("/greeting?name=Spring"); + this.server.enqueue(new MockResponse().setResponseCode(404)); + + Mono result = this.webClient + .perform(get(baseUrl.toString())) + .extract(body(String.class)); + + + TestSubscriber ts = new TestSubscriber(); + result.subscribe(ts); + // TODO: error message should be converted to a ClientException + ts.await().assertError(); + + RecordedRequest request = server.takeRequest(); + assertEquals("*/*", request.getHeader(HttpHeaders.ACCEPT)); + assertEquals("/greeting?name=Spring", request.getPath()); + } + + @After + public void tearDown() throws Exception { + this.server.shutdown(); + } + +}