Move WebClient to web.reactive.function.client
This commit is contained in:
@@ -1,318 +0,0 @@
|
||||
/*
|
||||
* 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.net.URI;
|
||||
import java.nio.charset.Charset;
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.client.reactive.ClientHttpRequest;
|
||||
import org.springframework.http.codec.BodyInserter;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.util.DefaultUriTemplateHandler;
|
||||
import org.springframework.web.util.UriTemplateHandler;
|
||||
|
||||
/**
|
||||
* Represents a typed, immutable, client-side HTTP request, as executed by the {@link WebClient}.
|
||||
* Instances of this interface are created via static builder methods:
|
||||
* {@link #method(HttpMethod, String, Object...)}, {@link #GET(String, Object...)}, etc.
|
||||
*
|
||||
* @param <T> the type of the body that this request contains
|
||||
* @author Brian Clozel
|
||||
* @author Arjen Poutsma
|
||||
* @since 5.0
|
||||
*/
|
||||
public interface ClientRequest<T> {
|
||||
|
||||
// Instance methods
|
||||
|
||||
/**
|
||||
* Return the HTTP method.
|
||||
*/
|
||||
HttpMethod method();
|
||||
|
||||
/**
|
||||
* Return the request URI.
|
||||
*/
|
||||
URI url();
|
||||
|
||||
/**
|
||||
* Return the headers of this request.
|
||||
*/
|
||||
HttpHeaders headers();
|
||||
|
||||
/**
|
||||
* Return the cookies of this request.
|
||||
*/
|
||||
MultiValueMap<String, String> cookies();
|
||||
|
||||
/**
|
||||
* Return the body inserter of this request.
|
||||
*/
|
||||
BodyInserter<T, ? super ClientHttpRequest> inserter();
|
||||
|
||||
/**
|
||||
* Writes this request to the given {@link ClientHttpRequest}.
|
||||
*
|
||||
* @param request the client http request to write to
|
||||
* @param strategies the strategies to use when writing
|
||||
* @return {@code Mono<Void>} to indicate when writing is complete
|
||||
*/
|
||||
Mono<Void> writeTo(ClientHttpRequest request, WebClientStrategies strategies);
|
||||
|
||||
// Static builder methods
|
||||
|
||||
/**
|
||||
* Create a builder with the method, URI, headers, and cookies of the given request.
|
||||
*
|
||||
* @param other the request to copy the method, URI, headers, and cookies from
|
||||
* @return the created builder
|
||||
*/
|
||||
static BodyBuilder from(ClientRequest<?> other) {
|
||||
Assert.notNull(other, "'other' must not be null");
|
||||
return new DefaultClientRequestBuilder(other.method(), other.url())
|
||||
.headers(other.headers())
|
||||
.cookies(other.cookies());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a builder with the given method and url.
|
||||
* @param method the HTTP method (GET, POST, etc)
|
||||
* @param url the URL
|
||||
* @return the created builder
|
||||
*/
|
||||
static BodyBuilder method(HttpMethod method, URI url) {
|
||||
return new DefaultClientRequestBuilder(method, url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a builder with the given method and url template.
|
||||
* @param method the HTTP method (GET, POST, etc)
|
||||
* @param urlTemplate the URL template
|
||||
* @param uriVariables optional variables to expand the template
|
||||
* @return the created builder
|
||||
*/
|
||||
static BodyBuilder method(HttpMethod method, String urlTemplate, Object... uriVariables) {
|
||||
UriTemplateHandler templateHandler = new DefaultUriTemplateHandler();
|
||||
URI url = templateHandler.expand(urlTemplate, uriVariables);
|
||||
return new DefaultClientRequestBuilder(method, url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an HTTP GET builder with the given url template.
|
||||
* @param urlTemplate the URL template
|
||||
* @param uriVariables optional variables to expand the template
|
||||
* @return the created builder
|
||||
*/
|
||||
static HeadersBuilder<?> GET(String urlTemplate, Object... uriVariables) {
|
||||
return method(HttpMethod.GET, urlTemplate, uriVariables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an HTTP HEAD builder with the given url template.
|
||||
* @param urlTemplate the URL template
|
||||
* @param uriVariables optional variables to expand the template
|
||||
* @return the created builder
|
||||
*/
|
||||
static HeadersBuilder<?> HEAD(String urlTemplate, Object... uriVariables) {
|
||||
return method(HttpMethod.HEAD, urlTemplate, uriVariables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an HTTP POST builder with the given url template.
|
||||
* @param urlTemplate the URL template
|
||||
* @param uriVariables optional variables to expand the template
|
||||
* @return the created builder
|
||||
*/
|
||||
static BodyBuilder POST(String urlTemplate, Object... uriVariables) {
|
||||
return method(HttpMethod.POST, urlTemplate, uriVariables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an HTTP PUT builder with the given url template.
|
||||
* @param urlTemplate the URL template
|
||||
* @param uriVariables optional variables to expand the template
|
||||
* @return the created builder
|
||||
*/
|
||||
static BodyBuilder PUT(String urlTemplate, Object... uriVariables) {
|
||||
return method(HttpMethod.PUT, urlTemplate, uriVariables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an HTTP PATCH builder with the given url template.
|
||||
* @param urlTemplate the URL template
|
||||
* @param uriVariables optional variables to expand the template
|
||||
* @return the created builder
|
||||
*/
|
||||
static BodyBuilder PATCH(String urlTemplate, Object... uriVariables) {
|
||||
return method(HttpMethod.PATCH, urlTemplate, uriVariables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an HTTP DELETE builder with the given url template.
|
||||
* @param urlTemplate the URL template
|
||||
* @param uriVariables optional variables to expand the template
|
||||
* @return the created builder
|
||||
*/
|
||||
static HeadersBuilder<?> DELETE(String urlTemplate, Object... uriVariables) {
|
||||
return method(HttpMethod.DELETE, urlTemplate, uriVariables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an HTTP OPTIONS builder with the given url template.
|
||||
* @param urlTemplate the URL template
|
||||
* @param uriVariables optional variables to expand the template
|
||||
* @return the created builder
|
||||
*/
|
||||
static HeadersBuilder<?> OPTIONS(String urlTemplate, Object... uriVariables) {
|
||||
return method(HttpMethod.OPTIONS, urlTemplate, uriVariables);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Defines a builder that adds headers to the request.
|
||||
*
|
||||
* @param <B> the builder subclass
|
||||
*/
|
||||
interface HeadersBuilder<B extends HeadersBuilder<B>> {
|
||||
|
||||
/**
|
||||
* Add the given, single header value under the given name.
|
||||
* @param headerName the header name
|
||||
* @param headerValues the header value(s)
|
||||
* @return this builder
|
||||
* @see HttpHeaders#add(String, String)
|
||||
*/
|
||||
B header(String headerName, String... headerValues);
|
||||
|
||||
/**
|
||||
* Copy the given headers into the entity's headers map.
|
||||
*
|
||||
* @param headers the existing HttpHeaders to copy from
|
||||
* @return this builder
|
||||
*/
|
||||
B headers(HttpHeaders headers);
|
||||
|
||||
/**
|
||||
* Set the list of acceptable {@linkplain MediaType media types}, as
|
||||
* specified by the {@code Accept} header.
|
||||
* @param acceptableMediaTypes the acceptable media types
|
||||
* @return this builder
|
||||
*/
|
||||
B accept(MediaType... acceptableMediaTypes);
|
||||
|
||||
/**
|
||||
* Set the list of acceptable {@linkplain Charset charsets}, as specified
|
||||
* by the {@code Accept-Charset} header.
|
||||
* @param acceptableCharsets the acceptable charsets
|
||||
* @return this builder
|
||||
*/
|
||||
B acceptCharset(Charset... acceptableCharsets);
|
||||
|
||||
/**
|
||||
* Set the value of the {@code If-Modified-Since} header.
|
||||
* <p>The date should be specified as the number of milliseconds since
|
||||
* January 1, 1970 GMT.
|
||||
* @param ifModifiedSince the new value of the header
|
||||
* @return this builder
|
||||
*/
|
||||
B ifModifiedSince(ZonedDateTime ifModifiedSince);
|
||||
|
||||
/**
|
||||
* Set the values of the {@code If-None-Match} header.
|
||||
* @param ifNoneMatches the new value of the header
|
||||
* @return this builder
|
||||
*/
|
||||
B ifNoneMatch(String... ifNoneMatches);
|
||||
|
||||
/**
|
||||
* Add a cookie with the given name and value.
|
||||
* @param name the cookie name
|
||||
* @param value the cookie value
|
||||
* @return this builder
|
||||
*/
|
||||
B cookie(String name, String value);
|
||||
|
||||
/**
|
||||
* Copy the given cookies into the entity's cookies map.
|
||||
*
|
||||
* @param cookies the existing cookies to copy from
|
||||
* @return this builder
|
||||
*/
|
||||
B cookies(MultiValueMap<String, String> cookies);
|
||||
|
||||
/**
|
||||
* Builds the request entity with no body.
|
||||
* @return the request entity
|
||||
*/
|
||||
ClientRequest<Void> build();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Defines a builder that adds a body to the request entity.
|
||||
*/
|
||||
interface BodyBuilder extends HeadersBuilder<BodyBuilder> {
|
||||
|
||||
|
||||
/**
|
||||
* Set the length of the body in bytes, as specified by the
|
||||
* {@code Content-Length} header.
|
||||
* @param contentLength the content length
|
||||
* @return this builder
|
||||
* @see HttpHeaders#setContentLength(long)
|
||||
*/
|
||||
BodyBuilder contentLength(long contentLength);
|
||||
|
||||
/**
|
||||
* Set the {@linkplain MediaType media type} of the body, as specified
|
||||
* by the {@code Content-Type} header.
|
||||
* @param contentType the content type
|
||||
* @return this builder
|
||||
* @see HttpHeaders#setContentType(MediaType)
|
||||
*/
|
||||
BodyBuilder contentType(MediaType contentType);
|
||||
|
||||
/**
|
||||
* Set the body of the request to the given {@code BodyInserter} and return it.
|
||||
* @param inserter the {@code BodyInserter} that writes to the request
|
||||
* @param <T> the type contained in the body
|
||||
* @return the built request
|
||||
*/
|
||||
<T> ClientRequest<T> body(BodyInserter<T, ? super ClientHttpRequest> inserter);
|
||||
|
||||
/**
|
||||
* Set the body of the request to the given {@code Publisher} and return it.
|
||||
* @param publisher the {@code Publisher} to write to the request
|
||||
* @param elementClass the class of elements contained in the publisher
|
||||
* @param <T> the type of the elements contained in the publisher
|
||||
* @param <S> the type of the {@code Publisher}
|
||||
* @return the built request
|
||||
*/
|
||||
<T, S extends Publisher<T>> ClientRequest<S> body(S publisher, Class<T> elementClass);
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
/*
|
||||
* 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 java.util.Optional;
|
||||
import java.util.OptionalLong;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.client.reactive.ClientHttpResponse;
|
||||
import org.springframework.http.codec.BodyExtractor;
|
||||
|
||||
/**
|
||||
* Represents an HTTP response, as returned by the {@link WebClient}.
|
||||
* Access to headers and body is offered by {@link Headers} and
|
||||
* {@link #body(BodyExtractor)}, {@link #bodyToMono(Class)}, {@link #bodyToFlux(Class)}
|
||||
* respectively.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Arjen Poutsma
|
||||
* @since 5.0
|
||||
*/
|
||||
public interface ClientResponse {
|
||||
|
||||
/**
|
||||
* Return the status code of this response.
|
||||
*/
|
||||
HttpStatus statusCode();
|
||||
|
||||
/**
|
||||
* Return the headers of this response.
|
||||
*/
|
||||
Headers headers();
|
||||
|
||||
/**
|
||||
* Extract the body with the given {@code BodyExtractor}. Unlike {@link #bodyToMono(Class)} and
|
||||
* {@link #bodyToFlux(Class)}; this method does not check for a 4xx or 5xx status code before
|
||||
* extracting the body.
|
||||
* @param extractor the {@code BodyExtractor} that reads from the response
|
||||
* @param <T> the type of the body returned
|
||||
* @return the extracted body
|
||||
*/
|
||||
<T> T body(BodyExtractor<T, ? super ClientHttpResponse> extractor);
|
||||
|
||||
/**
|
||||
* Extract the body to a {@code Mono}. If the response has status code 4xx or 5xx, the
|
||||
* {@code Mono} will contain a {@link WebClientException}.
|
||||
* @param elementClass the class of element in the {@code Mono}
|
||||
* @param <T> the element type
|
||||
* @return a mono containing the body, or a {@link WebClientException} if the status code is
|
||||
* 4xx or 5xx
|
||||
*/
|
||||
<T> Mono<T> bodyToMono(Class<? extends T> elementClass);
|
||||
|
||||
/**
|
||||
* Extract the body to a {@code Flux}. If the response has status code 4xx or 5xx, the
|
||||
* {@code Flux} will contain a {@link WebClientException}.
|
||||
* @param elementClass the class of element in the {@code Flux}
|
||||
* @param <T> the element type
|
||||
* @return a flux containing the body, or a {@link WebClientException} if the status code is
|
||||
* 4xx or 5xx
|
||||
*/
|
||||
<T> Flux<T> bodyToFlux(Class<? extends T> elementClass);
|
||||
|
||||
|
||||
/**
|
||||
* Represents the headers of the HTTP response.
|
||||
* @see ClientResponse#headers()
|
||||
*/
|
||||
interface Headers {
|
||||
|
||||
/**
|
||||
* Return the length of the body in bytes, as specified by the
|
||||
* {@code Content-Length} header.
|
||||
*/
|
||||
OptionalLong contentLength();
|
||||
|
||||
/**
|
||||
* Return the {@linkplain MediaType media type} of the body, as specified
|
||||
* by the {@code Content-Type} header.
|
||||
*/
|
||||
Optional<MediaType> contentType();
|
||||
|
||||
/**
|
||||
* Return the header value(s), if any, for the header of the given name.
|
||||
* <p>Return an empty list if no header values are found.
|
||||
*
|
||||
* @param headerName the header name
|
||||
*/
|
||||
List<String> header(String headerName);
|
||||
|
||||
/**
|
||||
* Return the headers as a {@link HttpHeaders} instance.
|
||||
*/
|
||||
HttpHeaders asHttpHeaders();
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,226 +0,0 @@
|
||||
/*
|
||||
* 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.net.URI;
|
||||
import java.nio.charset.Charset;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Arrays;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.http.HttpCookie;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.client.reactive.ClientHttpRequest;
|
||||
import org.springframework.http.codec.BodyInserter;
|
||||
import org.springframework.http.codec.BodyInserters;
|
||||
import org.springframework.http.codec.HttpMessageWriter;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link ClientRequest.BodyBuilder}.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @since 5.0
|
||||
*/
|
||||
class DefaultClientRequestBuilder implements ClientRequest.BodyBuilder {
|
||||
|
||||
private final HttpMethod method;
|
||||
|
||||
private final URI url;
|
||||
|
||||
private final HttpHeaders headers = new HttpHeaders();
|
||||
|
||||
private final MultiValueMap<String, String> cookies = new LinkedMultiValueMap<>();
|
||||
|
||||
|
||||
public DefaultClientRequestBuilder(HttpMethod method, URI url) {
|
||||
this.method = method;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientRequest.BodyBuilder header(String headerName, String... headerValues) {
|
||||
for (String headerValue : headerValues) {
|
||||
this.headers.add(headerName, headerValue);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientRequest.BodyBuilder headers(HttpHeaders headers) {
|
||||
if (headers != null) {
|
||||
this.headers.putAll(headers);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientRequest.BodyBuilder accept(MediaType... acceptableMediaTypes) {
|
||||
this.headers.setAccept(Arrays.asList(acceptableMediaTypes));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientRequest.BodyBuilder acceptCharset(Charset... acceptableCharsets) {
|
||||
this.headers.setAcceptCharset(Arrays.asList(acceptableCharsets));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientRequest.BodyBuilder ifModifiedSince(ZonedDateTime ifModifiedSince) {
|
||||
ZonedDateTime gmt = ifModifiedSince.withZoneSameInstant(ZoneId.of("GMT"));
|
||||
String headerValue = DateTimeFormatter.RFC_1123_DATE_TIME.format(gmt);
|
||||
this.headers.set(HttpHeaders.IF_MODIFIED_SINCE, headerValue);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientRequest.BodyBuilder ifNoneMatch(String... ifNoneMatches) {
|
||||
this.headers.setIfNoneMatch(Arrays.asList(ifNoneMatches));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientRequest.BodyBuilder cookie(String name, String value) {
|
||||
this.cookies.add(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientRequest.BodyBuilder cookies(MultiValueMap<String, String> cookies) {
|
||||
if (cookies != null) {
|
||||
this.cookies.putAll(cookies);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientRequest<Void> build() {
|
||||
return body(BodyInserters.empty());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientRequest.BodyBuilder contentLength(long contentLength) {
|
||||
this.headers.setContentLength(contentLength);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientRequest.BodyBuilder contentType(MediaType contentType) {
|
||||
this.headers.setContentType(contentType);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> ClientRequest<T> body(BodyInserter<T, ? super ClientHttpRequest> inserter) {
|
||||
Assert.notNull(inserter, "'inserter' must not be null");
|
||||
return new BodyInserterRequest<T>(this.method, this.url, this.headers, this.cookies,
|
||||
inserter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T, S extends Publisher<T>> ClientRequest<S> body(S publisher, Class<T> elementClass) {
|
||||
return body(BodyInserters.fromPublisher(publisher, elementClass));
|
||||
}
|
||||
|
||||
private static class BodyInserterRequest<T> implements ClientRequest<T> {
|
||||
|
||||
private final HttpMethod method;
|
||||
|
||||
private final URI url;
|
||||
|
||||
private final HttpHeaders headers;
|
||||
|
||||
private final MultiValueMap<String, String> cookies;
|
||||
|
||||
private final BodyInserter<T, ? super ClientHttpRequest> inserter;
|
||||
|
||||
public BodyInserterRequest(HttpMethod method, URI url, HttpHeaders headers,
|
||||
MultiValueMap<String, String> cookies,
|
||||
BodyInserter<T, ? super ClientHttpRequest> inserter) {
|
||||
this.method = method;
|
||||
this.url = url;
|
||||
this.headers = HttpHeaders.readOnlyHttpHeaders(headers);
|
||||
this.cookies = CollectionUtils.unmodifiableMultiValueMap(cookies);
|
||||
this.inserter = inserter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpMethod method() {
|
||||
return this.method;
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI url() {
|
||||
return this.url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders headers() {
|
||||
return this.headers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MultiValueMap<String, String> cookies() {
|
||||
return this.cookies;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BodyInserter<T, ? super ClientHttpRequest> inserter() {
|
||||
return this.inserter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> writeTo(ClientHttpRequest request, WebClientStrategies strategies) {
|
||||
HttpHeaders requestHeaders = request.getHeaders();
|
||||
if (!this.headers.isEmpty()) {
|
||||
this.headers.entrySet().stream()
|
||||
.filter(entry -> !requestHeaders.containsKey(entry.getKey()))
|
||||
.forEach(entry -> requestHeaders
|
||||
.put(entry.getKey(), entry.getValue()));
|
||||
}
|
||||
MultiValueMap<String, HttpCookie> requestCookies = request.getCookies();
|
||||
if (!this.cookies.isEmpty()) {
|
||||
this.cookies.entrySet().forEach(entry -> {
|
||||
String name = entry.getKey();
|
||||
entry.getValue().forEach(value -> {
|
||||
HttpCookie cookie = new HttpCookie(name, value);
|
||||
requestCookies.add(name, cookie);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return this.inserter.insert(request, new BodyInserter.Context() {
|
||||
@Override
|
||||
public Supplier<Stream<HttpMessageWriter<?>>> messageWriters() {
|
||||
return strategies.messageWriters();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
/*
|
||||
* 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.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalLong;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.client.reactive.ClientHttpResponse;
|
||||
import org.springframework.http.codec.BodyExtractor;
|
||||
import org.springframework.http.codec.BodyExtractors;
|
||||
import org.springframework.http.codec.HttpMessageReader;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link ClientResponse}.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @since 5.0
|
||||
*/
|
||||
class DefaultClientResponse implements ClientResponse {
|
||||
|
||||
private final ClientHttpResponse response;
|
||||
|
||||
private final Headers headers;
|
||||
|
||||
private final WebClientStrategies strategies;
|
||||
|
||||
|
||||
public DefaultClientResponse(ClientHttpResponse response, WebClientStrategies strategies) {
|
||||
this.response = response;
|
||||
this.strategies = strategies;
|
||||
this.headers = new DefaultHeaders();
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpStatus statusCode() {
|
||||
return this.response.getStatusCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Headers headers() {
|
||||
return this.headers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T body(BodyExtractor<T, ? super ClientHttpResponse> extractor) {
|
||||
return extractor.extract(this.response, new BodyExtractor.Context() {
|
||||
@Override
|
||||
public Supplier<Stream<HttpMessageReader<?>>> messageReaders() {
|
||||
return strategies.messageReaders();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Mono<T> bodyToMono(Class<? extends T> elementClass) {
|
||||
return bodyToPublisher(BodyExtractors.toMono(elementClass), Mono::error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Flux<T> bodyToFlux(Class<? extends T> elementClass) {
|
||||
return bodyToPublisher(BodyExtractors.toFlux(elementClass), Flux::error);
|
||||
}
|
||||
|
||||
private <T extends Publisher<?>> T bodyToPublisher(
|
||||
BodyExtractor<T, ? super ClientHttpResponse> extractor,
|
||||
Function<WebClientException, T> errorFunction) {
|
||||
|
||||
HttpStatus status = statusCode();
|
||||
if (status.is4xxClientError() || status.is5xxServerError()) {
|
||||
WebClientException ex = new WebClientException(
|
||||
"ClientResponse has erroneous status code: " + status.value() +
|
||||
" " + status.getReasonPhrase());
|
||||
return errorFunction.apply(ex);
|
||||
}
|
||||
else {
|
||||
return body(extractor);
|
||||
}
|
||||
}
|
||||
|
||||
private class DefaultHeaders implements Headers {
|
||||
|
||||
private HttpHeaders delegate() {
|
||||
return response.getHeaders();
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionalLong contentLength() {
|
||||
return toOptionalLong(delegate().getContentLength());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<MediaType> contentType() {
|
||||
return Optional.ofNullable(delegate().getContentType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> header(String headerName) {
|
||||
List<String> headerValues = delegate().get(headerName);
|
||||
return headerValues != null ? headerValues : Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders asHttpHeaders() {
|
||||
return HttpHeaders.readOnlyHttpHeaders(delegate());
|
||||
}
|
||||
|
||||
private OptionalLong toOptionalLong(long value) {
|
||||
return value != -1 ? OptionalLong.of(value) : OptionalLong.empty();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
/*
|
||||
* 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.logging.Level;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link WebClient.Builder}.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @since 5.0
|
||||
*/
|
||||
class DefaultWebClientBuilder implements WebClient.Builder {
|
||||
|
||||
private ClientHttpConnector clientHttpConnector;
|
||||
|
||||
private WebClientStrategies strategies = WebClientStrategies.withDefaults();
|
||||
|
||||
private ExchangeFilterFunction filter = new NoOpFilter();
|
||||
|
||||
|
||||
public DefaultWebClientBuilder(ClientHttpConnector clientHttpConnector) {
|
||||
this.clientHttpConnector = clientHttpConnector;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebClient.Builder strategies(WebClientStrategies strategies) {
|
||||
Assert.notNull(strategies, "'strategies' must not be null");
|
||||
this.strategies = strategies;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebClient.Builder filter(ExchangeFilterFunction filter) {
|
||||
Assert.notNull(filter, "'filter' must not be null");
|
||||
this.filter = filter.andThen(this.filter);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebClient build() {
|
||||
return new DefaultWebClient(this.clientHttpConnector, this.strategies, this.filter);
|
||||
}
|
||||
|
||||
private final static class DefaultWebClient implements WebClient {
|
||||
|
||||
private final ClientHttpConnector clientHttpConnector;
|
||||
|
||||
private final WebClientStrategies strategies;
|
||||
|
||||
private final ExchangeFilterFunction filter;
|
||||
|
||||
public DefaultWebClient(
|
||||
ClientHttpConnector clientHttpConnector,
|
||||
WebClientStrategies strategies,
|
||||
ExchangeFilterFunction filter) {
|
||||
this.clientHttpConnector = clientHttpConnector;
|
||||
this.strategies = strategies;
|
||||
this.filter = filter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ClientResponse> exchange(ClientRequest<?> request) {
|
||||
Assert.notNull(request, "'request' must not be null");
|
||||
|
||||
return this.filter.filter(request, this::exchangeInternal);
|
||||
}
|
||||
|
||||
private Mono<ClientResponse> exchangeInternal(ClientRequest<?> request) {
|
||||
return this.clientHttpConnector
|
||||
.connect(request.method(), request.url(),
|
||||
clientHttpRequest -> request
|
||||
.writeTo(clientHttpRequest, this.strategies))
|
||||
.log("org.springframework.web.client.reactive", Level.FINE)
|
||||
.map(clientHttpResponse -> new DefaultClientResponse(clientHttpResponse,
|
||||
this.strategies));
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebClient filter(ExchangeFilterFunction filter) {
|
||||
Assert.notNull(filter, "'filter' must not be null");
|
||||
|
||||
ExchangeFilterFunction composedFilter = filter.andThen(this.filter);
|
||||
|
||||
return new DefaultWebClient(this.clientHttpConnector, this.strategies, composedFilter);
|
||||
}
|
||||
}
|
||||
|
||||
private class NoOpFilter implements ExchangeFilterFunction {
|
||||
|
||||
@Override
|
||||
public Mono<ClientResponse> filter(ClientRequest<?> request, ExchangeFunction next) {
|
||||
return next.exchange(request);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
/*
|
||||
* 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.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.codec.ByteArrayDecoder;
|
||||
import org.springframework.core.codec.ByteArrayEncoder;
|
||||
import org.springframework.core.codec.ByteBufferDecoder;
|
||||
import org.springframework.core.codec.ByteBufferEncoder;
|
||||
import org.springframework.core.codec.CharSequenceEncoder;
|
||||
import org.springframework.core.codec.Decoder;
|
||||
import org.springframework.core.codec.Encoder;
|
||||
import org.springframework.core.codec.StringDecoder;
|
||||
import org.springframework.http.codec.DecoderHttpMessageReader;
|
||||
import org.springframework.http.codec.EncoderHttpMessageWriter;
|
||||
import org.springframework.http.codec.HttpMessageReader;
|
||||
import org.springframework.http.codec.HttpMessageWriter;
|
||||
import org.springframework.http.codec.ResourceHttpMessageWriter;
|
||||
import org.springframework.http.codec.json.Jackson2JsonDecoder;
|
||||
import org.springframework.http.codec.json.Jackson2JsonEncoder;
|
||||
import org.springframework.http.codec.xml.Jaxb2XmlDecoder;
|
||||
import org.springframework.http.codec.xml.Jaxb2XmlEncoder;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link WebClientStrategies.Builder}.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @since 5.0
|
||||
*/
|
||||
class DefaultWebClientStrategiesBuilder implements WebClientStrategies.Builder {
|
||||
|
||||
private static final boolean jackson2Present =
|
||||
ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper",
|
||||
DefaultWebClientStrategiesBuilder.class.getClassLoader()) &&
|
||||
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator",
|
||||
DefaultWebClientStrategiesBuilder.class.getClassLoader());
|
||||
|
||||
private static final boolean jaxb2Present =
|
||||
ClassUtils.isPresent("javax.xml.bind.Binder",
|
||||
DefaultWebClientStrategiesBuilder.class.getClassLoader());
|
||||
|
||||
|
||||
private final List<HttpMessageReader<?>> messageReaders = new ArrayList<>();
|
||||
|
||||
private final List<HttpMessageWriter<?>> messageWriters = new ArrayList<>();
|
||||
|
||||
|
||||
public void defaultConfiguration() {
|
||||
messageReader(new DecoderHttpMessageReader<>(new ByteArrayDecoder()));
|
||||
messageReader(new DecoderHttpMessageReader<>(new ByteBufferDecoder()));
|
||||
messageReader(new DecoderHttpMessageReader<>(new StringDecoder(false)));
|
||||
|
||||
messageWriter(new EncoderHttpMessageWriter<>(new ByteArrayEncoder()));
|
||||
messageWriter(new EncoderHttpMessageWriter<>(new ByteBufferEncoder()));
|
||||
messageWriter(new EncoderHttpMessageWriter<>(new CharSequenceEncoder()));
|
||||
messageWriter(new ResourceHttpMessageWriter());
|
||||
|
||||
if (jaxb2Present) {
|
||||
messageReader(new DecoderHttpMessageReader<>(new Jaxb2XmlDecoder()));
|
||||
messageWriter(new EncoderHttpMessageWriter<>(new Jaxb2XmlEncoder()));
|
||||
}
|
||||
if (jackson2Present) {
|
||||
messageReader(new DecoderHttpMessageReader<>(new Jackson2JsonDecoder()));
|
||||
messageWriter(new EncoderHttpMessageWriter<>(new Jackson2JsonEncoder()));
|
||||
}
|
||||
}
|
||||
|
||||
public void applicationContext(ApplicationContext applicationContext) {
|
||||
applicationContext.getBeansOfType(HttpMessageReader.class).values().forEach(this::messageReader);
|
||||
applicationContext.getBeansOfType(HttpMessageWriter.class).values().forEach(this::messageWriter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebClientStrategies.Builder messageReader(HttpMessageReader<?> messageReader) {
|
||||
Assert.notNull(messageReader, "'messageReader' must not be null");
|
||||
this.messageReaders.add(messageReader);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebClientStrategies.Builder decoder(Decoder<?> decoder) {
|
||||
Assert.notNull(decoder, "'decoder' must not be null");
|
||||
return messageReader(new DecoderHttpMessageReader<>(decoder));
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebClientStrategies.Builder messageWriter(HttpMessageWriter<?> messageWriter) {
|
||||
Assert.notNull(messageWriter, "'messageWriter' must not be null");
|
||||
this.messageWriters.add(messageWriter);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebClientStrategies.Builder encoder(Encoder<?> encoder) {
|
||||
Assert.notNull(encoder, "'encoder' must not be null");
|
||||
return messageWriter(new EncoderHttpMessageWriter<>(encoder));
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebClientStrategies build() {
|
||||
return new DefaultWebClientStrategies(this.messageReaders, this.messageWriters);
|
||||
}
|
||||
|
||||
private static class DefaultWebClientStrategies implements WebClientStrategies {
|
||||
|
||||
private final List<HttpMessageReader<?>> messageReaders;
|
||||
|
||||
private final List<HttpMessageWriter<?>> messageWriters;
|
||||
|
||||
public DefaultWebClientStrategies(
|
||||
List<HttpMessageReader<?>> messageReaders,
|
||||
List<HttpMessageWriter<?>> messageWriters) {
|
||||
this.messageReaders = unmodifiableCopy(messageReaders);
|
||||
this.messageWriters = unmodifiableCopy(messageWriters);
|
||||
}
|
||||
|
||||
private static <T> List<T> unmodifiableCopy(List<? extends T> list) {
|
||||
return Collections.unmodifiableList(new ArrayList<>(list));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Supplier<Stream<HttpMessageReader<?>>> messageReaders() {
|
||||
return this.messageReaders::stream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Supplier<Stream<HttpMessageWriter<?>>> messageWriters() {
|
||||
return this.messageWriters::stream;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
/*
|
||||
* 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.Function;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Represents a function that filters an {@linkplain ExchangeFunction exchange function}.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @since 5.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ExchangeFilterFunction {
|
||||
|
||||
/**
|
||||
* Apply this filter to the given request and exchange function. The given
|
||||
* {@linkplain ExchangeFunction exchange function} represents the next entity in the
|
||||
* chain, and can be {@linkplain ExchangeFunction#exchange(ClientRequest) invoked} in order
|
||||
* to proceed to the exchange, or not invoked to block the chain.
|
||||
*
|
||||
* @param request the request
|
||||
* @param next the next exchange function in the chain
|
||||
* @return the filtered response
|
||||
*/
|
||||
Mono<ClientResponse> filter(ClientRequest<?> request, ExchangeFunction next);
|
||||
|
||||
/**
|
||||
* Return a composed filter function that first applies this filter, and then applies the
|
||||
* {@code after} filter.
|
||||
* @param after the filter to apply after this filter is applied
|
||||
* @return a composed filter that first applies this function and then applies the
|
||||
* {@code after} function
|
||||
*/
|
||||
default ExchangeFilterFunction andThen(ExchangeFilterFunction after) {
|
||||
Assert.notNull(after, "'after' must not be null");
|
||||
return (request, next) -> {
|
||||
ExchangeFunction nextExchange = exchangeRequest -> after.filter(exchangeRequest, next);
|
||||
return filter(request, nextExchange);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply this filter to the given exchange function, resulting in a filtered exchange function.
|
||||
* @param exchange the exchange function to filter
|
||||
* @return the filtered exchange function
|
||||
*/
|
||||
default ExchangeFunction apply(ExchangeFunction exchange) {
|
||||
Assert.notNull(exchange, "'exchange' must not be null");
|
||||
return request -> this.filter(request, exchange);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapt the given request processor function to a filter function that only operates on the
|
||||
* {@code ClientRequest}.
|
||||
* @param requestProcessor the request processor
|
||||
* @return the filter adaptation of the request processor
|
||||
*/
|
||||
static ExchangeFilterFunction ofRequestProcessor(Function<ClientRequest<?>,
|
||||
Mono<ClientRequest<?>>> requestProcessor) {
|
||||
|
||||
Assert.notNull(requestProcessor, "'requestProcessor' must not be null");
|
||||
return (request, next) -> requestProcessor.apply(request).then(next::exchange);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapt the given response processor function to a filter function that only operates on the
|
||||
* {@code ClientResponse}.
|
||||
* @param responseProcessor the response processor
|
||||
* @return the filter adaptation of the request processor
|
||||
*/
|
||||
static ExchangeFilterFunction ofResponseProcessor(Function<ClientResponse,
|
||||
Mono<ClientResponse>> responseProcessor) {
|
||||
|
||||
Assert.notNull(responseProcessor, "'responseProcessor' must not be null");
|
||||
return (request, next) -> next.exchange(request).then(responseProcessor);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
/*
|
||||
* 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.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Implementations of {@link ExchangeFilterFunction} that provide various useful request filter
|
||||
* operations, such as basic authentication, error handling, etc.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @author Arjen Poutsma
|
||||
* @since 5.0
|
||||
*/
|
||||
public abstract class ExchangeFilterFunctions {
|
||||
|
||||
/**
|
||||
* Return a filter that adds an Authorization header for HTTP Basic Authentication.
|
||||
* @param username the username to use
|
||||
* @param password the password to use
|
||||
* @return the {@link ExchangeFilterFunction} that adds the Authorization header
|
||||
*/
|
||||
public static ExchangeFilterFunction basicAuthentication(String username, String password) {
|
||||
Assert.notNull(username, "'username' must not be null");
|
||||
Assert.notNull(password, "'password' must not be null");
|
||||
|
||||
return ExchangeFilterFunction.ofRequestProcessor(
|
||||
clientRequest -> {
|
||||
String authorization = authorization(username, password);
|
||||
ClientRequest<?> authorizedRequest = ClientRequest.from(clientRequest)
|
||||
.header(HttpHeaders.AUTHORIZATION, authorization)
|
||||
.body(clientRequest.inserter());
|
||||
return Mono.just(authorizedRequest);
|
||||
});
|
||||
}
|
||||
|
||||
private static String authorization(String username, String password) {
|
||||
String credentials = username + ":" + password;
|
||||
byte[] credentialBytes = credentials.getBytes(StandardCharsets.ISO_8859_1);
|
||||
byte[] encodedBytes = Base64.getEncoder().encode(credentialBytes);
|
||||
String encodedCredentials = new String(encodedBytes, StandardCharsets.ISO_8859_1);
|
||||
return "Basic " + encodedCredentials;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
/*
|
||||
* 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 reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* Represents a function that exchanges a {@linkplain ClientRequest request} for a (delayed)
|
||||
* {@linkplain ClientResponse}.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @since 5.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ExchangeFunction {
|
||||
|
||||
/**
|
||||
* Exchange the given request for a response mono.
|
||||
* @param request the request to exchange
|
||||
* @return the response, wrapped in a {@code Mono}
|
||||
*/
|
||||
Mono<ClientResponse> exchange(ClientRequest<?> request);
|
||||
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
/*
|
||||
* 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 reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Reactive Web client supporting the HTTP/1.1 protocol. Main entry point is through the
|
||||
* {@link #exchange(ClientRequest)} method.
|
||||
*
|
||||
* <p>For example:
|
||||
* <pre class="code">
|
||||
* WebClient client = WebClient.create(new ReactorClientHttpConnector());
|
||||
* ClientRequest<Void> request = ClientRequest.GET("http://example.com/resource").build();
|
||||
*
|
||||
* Mono<String> result = client
|
||||
* .exchange(request)
|
||||
* .then(response -> response.bodyToMono(String.class));
|
||||
* </pre>
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Arjen Poutsma
|
||||
* @since 5.0
|
||||
*/
|
||||
public interface WebClient extends ExchangeFunction {
|
||||
|
||||
/**
|
||||
* Exchange the given request for a response mono. Invoking this method performs the actual
|
||||
* HTTP request/response exchange.
|
||||
* @param request the request to exchange
|
||||
* @return the response, wrapped in a {@code Mono}
|
||||
*/
|
||||
@Override
|
||||
Mono<ClientResponse> exchange(ClientRequest<?> request);
|
||||
|
||||
/**
|
||||
* Filters this client with the given {@code ExchangeFilterFunction}, resulting in a filtered
|
||||
* {@code WebClient}.
|
||||
* @param filterFunction the filter to apply to this client
|
||||
* @return the filtered client
|
||||
* @see ExchangeFilterFunction#apply(ExchangeFunction)
|
||||
*/
|
||||
WebClient filter(ExchangeFilterFunction filterFunction);
|
||||
|
||||
|
||||
/**
|
||||
* Create a new instance of {@code WebClient} with the given connector. This method uses
|
||||
* {@linkplain WebClientStrategies#withDefaults() default strategies}.
|
||||
* @param connector the connector to create connections
|
||||
* @return the created client
|
||||
*/
|
||||
static WebClient create(ClientHttpConnector connector) {
|
||||
return builder(connector).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a builder for a {@code WebClient}.
|
||||
* @param connector the connector to create connections
|
||||
* @return a web client builder
|
||||
*/
|
||||
static Builder builder(ClientHttpConnector connector) {
|
||||
Assert.notNull(connector, "'connector' must not be null");
|
||||
return new DefaultWebClientBuilder(connector);
|
||||
}
|
||||
|
||||
/**
|
||||
* A mutable builder for a {@link WebClient}.
|
||||
*/
|
||||
interface Builder {
|
||||
|
||||
/**
|
||||
* Replaces the default strategies with the ones provided by the given
|
||||
* {@code WebClientStrategies}.
|
||||
* @param strategies the strategies to use
|
||||
* @return this builder
|
||||
*/
|
||||
Builder strategies(WebClientStrategies strategies);
|
||||
|
||||
/**
|
||||
* Adds a filter function <strong>before</strong> the currently registered filters (if any).
|
||||
* @param filter the filter to add
|
||||
* @return this builder
|
||||
*/
|
||||
Builder filter(ExchangeFilterFunction filter);
|
||||
|
||||
/**
|
||||
* Builds the {@code WebClient}.
|
||||
* @return the built client
|
||||
*/
|
||||
WebClient build();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
/*
|
||||
* 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 org.springframework.core.NestedRuntimeException;
|
||||
|
||||
/**
|
||||
* Exception published by {@link WebClient} in case of errors.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @since 5.0
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class WebClientException extends NestedRuntimeException {
|
||||
|
||||
/**
|
||||
* Construct a new instance of {@code WebClientException} with the given message.
|
||||
* @param msg the message
|
||||
*/
|
||||
public WebClientException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new instance of {@code WebClientException} with the given message and
|
||||
* exception.
|
||||
* @param msg the message
|
||||
* @param ex the exception
|
||||
*/
|
||||
public WebClientException(String msg, Throwable ex) {
|
||||
super(msg, ex);
|
||||
}
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
/*
|
||||
* 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.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.codec.Decoder;
|
||||
import org.springframework.core.codec.Encoder;
|
||||
import org.springframework.http.codec.HttpMessageReader;
|
||||
import org.springframework.http.codec.HttpMessageWriter;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Defines the strategies to be used by the {@link WebClient}. An instance of
|
||||
* this class is immutable; instances are typically created through the mutable {@link Builder}:
|
||||
* either through {@link #builder()} to set up default strategies, or {@link #empty()} to start from
|
||||
* scratch. Alternatively, {@code WebClientStrategies} instances can be created through
|
||||
* {@link #of(Supplier, Supplier)}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Arjen Poutsma
|
||||
* @since 5.0
|
||||
*/
|
||||
public interface WebClientStrategies {
|
||||
|
||||
// Instance methods
|
||||
|
||||
/**
|
||||
* Supply a {@linkplain Stream stream} of {@link HttpMessageReader}s to be used for request
|
||||
* body conversion.
|
||||
* @return the stream of message readers
|
||||
*/
|
||||
Supplier<Stream<HttpMessageReader<?>>> messageReaders();
|
||||
|
||||
/**
|
||||
* Supply a {@linkplain Stream stream} of {@link HttpMessageWriter}s to be used for response
|
||||
* body conversion.
|
||||
* @return the stream of message writers
|
||||
*/
|
||||
Supplier<Stream<HttpMessageWriter<?>>> messageWriters();
|
||||
|
||||
|
||||
// Static methods
|
||||
|
||||
/**
|
||||
* Return a new {@code WebClientStrategies} with default initialization.
|
||||
* @return the new {@code WebClientStrategies}
|
||||
*/
|
||||
static WebClientStrategies withDefaults() {
|
||||
return builder().build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@code WebClientStrategies} based on the given
|
||||
* {@linkplain ApplicationContext application context}.
|
||||
* The returned supplier will search for all {@link HttpMessageReader}, and
|
||||
* {@link HttpMessageWriter} instances in the given application context and return them for
|
||||
* {@link #messageReaders()}, and {@link #messageWriters()} respectively.
|
||||
* @param applicationContext the application context to base the strategies on
|
||||
* @return the new {@code WebClientStrategies}
|
||||
*/
|
||||
static WebClientStrategies of(ApplicationContext applicationContext) {
|
||||
return builder(applicationContext).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@code WebClientStrategies} described by the given supplier functions.
|
||||
* All provided supplier function parameters can be {@code null} to indicate an empty
|
||||
* stream is to be returned.
|
||||
* @param messageReaders the supplier function for {@link HttpMessageReader} instances (can be {@code null})
|
||||
* @param messageWriters the supplier function for {@link HttpMessageWriter} instances (can be {@code null})
|
||||
* @return the new {@code WebClientStrategies}
|
||||
*/
|
||||
static WebClientStrategies of(Supplier<Stream<HttpMessageReader<?>>> messageReaders,
|
||||
Supplier<Stream<HttpMessageWriter<?>>> messageWriters) {
|
||||
|
||||
return new WebClientStrategies() {
|
||||
@Override
|
||||
public Supplier<Stream<HttpMessageReader<?>>> messageReaders() {
|
||||
return checkForNull(messageReaders);
|
||||
}
|
||||
@Override
|
||||
public Supplier<Stream<HttpMessageWriter<?>>> messageWriters() {
|
||||
return checkForNull(messageWriters);
|
||||
}
|
||||
private <T> Supplier<Stream<T>> checkForNull(Supplier<Stream<T>> supplier) {
|
||||
return supplier != null ? supplier : Stream::empty;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// Builder methods
|
||||
|
||||
/**
|
||||
* Return a mutable builder for a {@code WebClientStrategies} with default initialization.
|
||||
* @return the builder
|
||||
*/
|
||||
static Builder builder() {
|
||||
DefaultWebClientStrategiesBuilder builder = new DefaultWebClientStrategiesBuilder();
|
||||
builder.defaultConfiguration();
|
||||
return builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a mutable builder based on the given {@linkplain ApplicationContext application context}.
|
||||
* The returned builder will search for all {@link HttpMessageReader}, and
|
||||
* {@link HttpMessageWriter} instances in the given application context and return them for
|
||||
* {@link #messageReaders()}, and {@link #messageWriters()}.
|
||||
* @param applicationContext the application context to base the strategies on
|
||||
* @return the builder
|
||||
*/
|
||||
static Builder builder(ApplicationContext applicationContext) {
|
||||
Assert.notNull(applicationContext, "ApplicationContext must not be null");
|
||||
DefaultWebClientStrategiesBuilder builder = new DefaultWebClientStrategiesBuilder();
|
||||
builder.applicationContext(applicationContext);
|
||||
return builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a mutable, empty builder for a {@code WebClientStrategies}.
|
||||
* @return the builder
|
||||
*/
|
||||
static Builder empty() {
|
||||
return new DefaultWebClientStrategiesBuilder();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A mutable builder for a {@link WebClientStrategies}.
|
||||
*/
|
||||
interface Builder {
|
||||
|
||||
/**
|
||||
* Add the given message reader to this builder.
|
||||
* @param messageReader the message reader to add
|
||||
* @return this builder
|
||||
*/
|
||||
Builder messageReader(HttpMessageReader<?> messageReader);
|
||||
|
||||
/**
|
||||
* Add the given decoder to this builder. This is a convenient alternative to adding a
|
||||
* {@link org.springframework.http.codec.DecoderHttpMessageReader} that wraps the given
|
||||
* decoder.
|
||||
* @param decoder the decoder to add
|
||||
* @return this builder
|
||||
*/
|
||||
Builder decoder(Decoder<?> decoder);
|
||||
|
||||
/**
|
||||
* Add the given message writer to this builder.
|
||||
* @param messageWriter the message writer to add
|
||||
* @return this builder
|
||||
*/
|
||||
Builder messageWriter(HttpMessageWriter<?> messageWriter);
|
||||
|
||||
/**
|
||||
* Add the given encoder to this builder. This is a convenient alternative to adding a
|
||||
* {@link org.springframework.http.codec.EncoderHttpMessageWriter} that wraps the given
|
||||
* encoder.
|
||||
* @param encoder the encoder to add
|
||||
* @return this builder
|
||||
*/
|
||||
Builder encoder(Encoder<?> encoder);
|
||||
|
||||
/**
|
||||
* Builds the {@link WebClientStrategies}.
|
||||
* @return the built strategies
|
||||
*/
|
||||
WebClientStrategies build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
/**
|
||||
* Provides a reactive {@link org.springframework.web.client.reactive.WebClient}
|
||||
* that builds on top of the
|
||||
* {@code org.springframework.http.client.reactive} reactive HTTP adapter layer.
|
||||
*/
|
||||
package org.springframework.web.client.reactive;
|
||||
@@ -1,138 +0,0 @@
|
||||
/*
|
||||
* 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.http.server.reactive;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferFactory;
|
||||
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
|
||||
import org.springframework.http.codec.BodyExtractors;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.client.reactive.ClientRequest;
|
||||
import org.springframework.web.client.reactive.WebClient;
|
||||
|
||||
/**
|
||||
* @author Sebastien Deleuze
|
||||
*/
|
||||
public class FlushingIntegrationTests extends AbstractHttpHandlerIntegrationTests {
|
||||
|
||||
private WebClient webClient;
|
||||
|
||||
@Before
|
||||
public void setup() throws Exception {
|
||||
super.setup();
|
||||
this.webClient = WebClient.create(new ReactorClientHttpConnector());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void writeAndFlushWith() throws Exception {
|
||||
ClientRequest<Void> request = ClientRequest.GET("http://localhost:" + port + "/write-and-flush").build();
|
||||
Mono<String> result = this.webClient
|
||||
.exchange(request)
|
||||
.flatMap(response -> response.body(BodyExtractors.toFlux(String.class)))
|
||||
.takeUntil(s -> s.endsWith("data1"))
|
||||
.reduce((s1, s2) -> s1 + s2);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.expectNext("data0data1")
|
||||
.expectComplete()
|
||||
.verify(Duration.ofSeconds(5L));
|
||||
}
|
||||
|
||||
@Test // SPR-14991
|
||||
public void writeAndAutoFlushOnComplete() {
|
||||
ClientRequest<Void> request = ClientRequest.GET("http://localhost:" + port + "/write-and-complete").build();
|
||||
Mono<String> result = this.webClient
|
||||
.exchange(request)
|
||||
.flatMap(response -> response.bodyToFlux(String.class))
|
||||
.reduce((s1, s2) -> s1 + s2);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.consumeNextWith(value -> Assert.isTrue(value.length() == 200000))
|
||||
.expectComplete()
|
||||
.verify(Duration.ofSeconds(5L));
|
||||
}
|
||||
|
||||
@Test // SPR-14992
|
||||
public void writeAndAutoFlushBeforeComplete() {
|
||||
ClientRequest<Void> request = ClientRequest.GET("http://localhost:" + port + "/write-and-never-complete").build();
|
||||
Flux<String> result = this.webClient
|
||||
.exchange(request)
|
||||
.flatMap(response -> response.bodyToFlux(String.class));
|
||||
|
||||
StepVerifier.create(result)
|
||||
.expectNextMatches(s -> s.startsWith("0123456789"))
|
||||
.thenCancel()
|
||||
.verify(Duration.ofSeconds(5L));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HttpHandler createHttpHandler() {
|
||||
return new FlushingHandler();
|
||||
}
|
||||
|
||||
private static class FlushingHandler implements HttpHandler {
|
||||
|
||||
@Override
|
||||
public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response) {
|
||||
String path = request.getURI().getPath();
|
||||
if (path.endsWith("write-and-flush")) {
|
||||
Flux<Publisher<DataBuffer>> responseBody = Flux
|
||||
.intervalMillis(50)
|
||||
.map(l -> toDataBuffer("data" + l, response.bufferFactory()))
|
||||
.take(2)
|
||||
.map(Flux::just);
|
||||
responseBody = responseBody.concatWith(Flux.never());
|
||||
return response.writeAndFlushWith(responseBody);
|
||||
}
|
||||
else if (path.endsWith("write-and-complete")) {
|
||||
Flux<DataBuffer> responseBody = Flux
|
||||
.just("0123456789")
|
||||
.repeat(20000)
|
||||
.map(value -> toDataBuffer(value, response.bufferFactory()));
|
||||
return response.writeWith(responseBody);
|
||||
}
|
||||
else if (path.endsWith("write-and-never-complete")) {
|
||||
Flux<DataBuffer> responseBody = Flux
|
||||
.just("0123456789")
|
||||
.repeat(20000)
|
||||
.map(value -> toDataBuffer(value, response.bufferFactory()))
|
||||
.mergeWith(Flux.never());
|
||||
return response.writeWith(responseBody);
|
||||
}
|
||||
return response.writeWith(Flux.empty());
|
||||
}
|
||||
|
||||
private DataBuffer toDataBuffer(String value, DataBufferFactory factory) {
|
||||
byte[] data = (value).getBytes(StandardCharsets.UTF_8);
|
||||
DataBuffer buffer = factory.allocateBuffer(data.length);
|
||||
buffer.write(data);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,211 +0,0 @@
|
||||
/*
|
||||
* 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.net.URI;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.Charset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.codec.CharSequenceEncoder;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.client.reactive.ClientHttpRequest;
|
||||
import org.springframework.http.codec.BodyInserter;
|
||||
import org.springframework.http.codec.EncoderHttpMessageWriter;
|
||||
import org.springframework.http.codec.HttpMessageWriter;
|
||||
import org.springframework.web.client.reactive.test.MockClientHttpRequest;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* @author Arjen Poutsma
|
||||
*/
|
||||
public class DefaultClientRequestBuilderTests {
|
||||
|
||||
@Test
|
||||
public void from() throws Exception {
|
||||
ClientRequest<Void> other = ClientRequest.GET("http://example.com")
|
||||
.header("foo", "bar")
|
||||
.cookie("baz", "qux").build();
|
||||
ClientRequest<Void> result = ClientRequest.from(other).build();
|
||||
assertEquals(new URI("http://example.com"), result.url());
|
||||
assertEquals(HttpMethod.GET, result.method());
|
||||
assertEquals("bar", result.headers().getFirst("foo"));
|
||||
assertEquals("qux", result.cookies().getFirst("baz"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void method() throws Exception {
|
||||
URI url = new URI("http://example.com");
|
||||
ClientRequest<Void> result = ClientRequest.method(HttpMethod.DELETE, url).build();
|
||||
assertEquals(url, result.url());
|
||||
assertEquals(HttpMethod.DELETE, result.method());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void GET() throws Exception {
|
||||
URI url = new URI("http://example.com");
|
||||
ClientRequest<Void> result = ClientRequest.GET(url.toString()).build();
|
||||
assertEquals(url, result.url());
|
||||
assertEquals(HttpMethod.GET, result.method());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void HEAD() throws Exception {
|
||||
URI url = new URI("http://example.com");
|
||||
ClientRequest<Void> result = ClientRequest.HEAD(url.toString()).build();
|
||||
assertEquals(url, result.url());
|
||||
assertEquals(HttpMethod.HEAD, result.method());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void POST() throws Exception {
|
||||
URI url = new URI("http://example.com");
|
||||
ClientRequest<Void> result = ClientRequest.POST(url.toString()).build();
|
||||
assertEquals(url, result.url());
|
||||
assertEquals(HttpMethod.POST, result.method());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void PUT() throws Exception {
|
||||
URI url = new URI("http://example.com");
|
||||
ClientRequest<Void> result = ClientRequest.PUT(url.toString()).build();
|
||||
assertEquals(url, result.url());
|
||||
assertEquals(HttpMethod.PUT, result.method());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void PATCH() throws Exception {
|
||||
URI url = new URI("http://example.com");
|
||||
ClientRequest<Void> result = ClientRequest.PATCH(url.toString()).build();
|
||||
assertEquals(url, result.url());
|
||||
assertEquals(HttpMethod.PATCH, result.method());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void DELETE() throws Exception {
|
||||
URI url = new URI("http://example.com");
|
||||
ClientRequest<Void> result = ClientRequest.DELETE(url.toString()).build();
|
||||
assertEquals(url, result.url());
|
||||
assertEquals(HttpMethod.DELETE, result.method());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void OPTIONS() throws Exception {
|
||||
URI url = new URI("http://example.com");
|
||||
ClientRequest<Void> result = ClientRequest.OPTIONS(url.toString()).build();
|
||||
assertEquals(url, result.url());
|
||||
assertEquals(HttpMethod.OPTIONS, result.method());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void accept() throws Exception {
|
||||
MediaType json = MediaType.APPLICATION_JSON;
|
||||
ClientRequest<Void> result = ClientRequest.GET("http://example.com").accept(json).build();
|
||||
assertEquals(Collections.singletonList(json), result.headers().getAccept());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptCharset() throws Exception {
|
||||
Charset charset = Charset.defaultCharset();
|
||||
ClientRequest<Void> result = ClientRequest.GET("http://example.com")
|
||||
.acceptCharset(charset).build();
|
||||
assertEquals(Collections.singletonList(charset), result.headers().getAcceptCharset());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ifModifiedSince() throws Exception {
|
||||
ZonedDateTime now = ZonedDateTime.now();
|
||||
ClientRequest<Void> result = ClientRequest.GET("http://example.com")
|
||||
.ifModifiedSince(now).build();
|
||||
assertEquals(now.toInstant().toEpochMilli()/1000, result.headers().getIfModifiedSince()/1000);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ifNoneMatch() throws Exception {
|
||||
ClientRequest<Void> result = ClientRequest.GET("http://example.com")
|
||||
.ifNoneMatch("\"v2.7\"", "\"v2.8\"").build();
|
||||
assertEquals(Arrays.asList("\"v2.7\"", "\"v2.8\""), result.headers().getIfNoneMatch());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cookie() throws Exception {
|
||||
ClientRequest<Void> result = ClientRequest.GET("http://example.com")
|
||||
.cookie("foo", "bar").build();
|
||||
assertEquals("bar", result.cookies().getFirst("foo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void build() throws Exception {
|
||||
ClientRequest<Void> result = ClientRequest.GET("http://example.com")
|
||||
.header("MyKey", "MyValue")
|
||||
.cookie("foo", "bar")
|
||||
.build();
|
||||
|
||||
MockClientHttpRequest request = new MockClientHttpRequest();
|
||||
WebClientStrategies strategies = mock(WebClientStrategies.class);
|
||||
|
||||
result.writeTo(request, strategies).block();
|
||||
|
||||
assertEquals("MyValue", request.getHeaders().getFirst("MyKey"));
|
||||
assertEquals("bar", request.getCookies().getFirst("foo").getValue());
|
||||
assertNull(request.getBody());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bodyInserter() throws Exception {
|
||||
String body = "foo";
|
||||
BodyInserter<String, ClientHttpRequest> inserter =
|
||||
(response, strategies) -> {
|
||||
byte[] bodyBytes = body.getBytes(UTF_8);
|
||||
ByteBuffer byteBuffer = ByteBuffer.wrap(bodyBytes);
|
||||
DataBuffer buffer = new DefaultDataBufferFactory().wrap(byteBuffer);
|
||||
|
||||
return response.writeWith(Mono.just(buffer));
|
||||
};
|
||||
|
||||
ClientRequest<String> result = ClientRequest.POST("http://example.com")
|
||||
.body(inserter);
|
||||
|
||||
MockClientHttpRequest request = new MockClientHttpRequest();
|
||||
|
||||
List<HttpMessageWriter<?>> messageWriters = new ArrayList<>();
|
||||
messageWriters.add(new EncoderHttpMessageWriter<CharSequence>(new CharSequenceEncoder()));
|
||||
|
||||
WebClientStrategies strategies = mock(WebClientStrategies.class);
|
||||
when(strategies.messageWriters()).thenReturn(messageWriters::stream);
|
||||
|
||||
result.writeTo(request, strategies).block();
|
||||
assertNotNull(request.getBody());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,198 +0,0 @@
|
||||
/*
|
||||
* 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.net.InetSocketAddress;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalLong;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.core.codec.StringDecoder;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DefaultDataBuffer;
|
||||
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpRange;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.client.reactive.ClientHttpResponse;
|
||||
import org.springframework.http.codec.DecoderHttpMessageReader;
|
||||
import org.springframework.http.codec.HttpMessageReader;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.http.codec.BodyExtractors.toMono;
|
||||
|
||||
/**
|
||||
* @author Arjen Poutsma
|
||||
*/
|
||||
public class DefaultClientResponseTests {
|
||||
|
||||
private ClientHttpResponse mockResponse;
|
||||
|
||||
private WebClientStrategies mockWebClientStrategies;
|
||||
|
||||
private DefaultClientResponse defaultClientResponse;
|
||||
|
||||
|
||||
@Before
|
||||
public void createMocks() {
|
||||
mockResponse = mock(ClientHttpResponse.class);
|
||||
mockWebClientStrategies = mock(WebClientStrategies.class);
|
||||
|
||||
defaultClientResponse = new DefaultClientResponse(mockResponse, mockWebClientStrategies);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void statusCode() throws Exception {
|
||||
HttpStatus status = HttpStatus.CONTINUE;
|
||||
when(mockResponse.getStatusCode()).thenReturn(status);
|
||||
|
||||
assertEquals(status, defaultClientResponse.statusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void header() throws Exception {
|
||||
HttpHeaders httpHeaders = new HttpHeaders();
|
||||
long contentLength = 42L;
|
||||
httpHeaders.setContentLength(contentLength);
|
||||
MediaType contentType = MediaType.TEXT_PLAIN;
|
||||
httpHeaders.setContentType(contentType);
|
||||
InetSocketAddress host = InetSocketAddress.createUnresolved("localhost", 80);
|
||||
httpHeaders.setHost(host);
|
||||
List<HttpRange> range = Collections.singletonList(HttpRange.createByteRange(0, 42));
|
||||
httpHeaders.setRange(range);
|
||||
|
||||
when(mockResponse.getHeaders()).thenReturn(httpHeaders);
|
||||
|
||||
ClientResponse.Headers headers = defaultClientResponse.headers();
|
||||
assertEquals(OptionalLong.of(contentLength), headers.contentLength());
|
||||
assertEquals(Optional.of(contentType), headers.contentType());
|
||||
assertEquals(httpHeaders, headers.asHttpHeaders());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void body() throws Exception {
|
||||
DefaultDataBufferFactory factory = new DefaultDataBufferFactory();
|
||||
DefaultDataBuffer dataBuffer =
|
||||
factory.wrap(ByteBuffer.wrap("foo".getBytes(StandardCharsets.UTF_8)));
|
||||
Flux<DataBuffer> body = Flux.just(dataBuffer);
|
||||
|
||||
HttpHeaders httpHeaders = new HttpHeaders();
|
||||
httpHeaders.setContentType(MediaType.TEXT_PLAIN);
|
||||
when(mockResponse.getHeaders()).thenReturn(httpHeaders);
|
||||
when(mockResponse.getBody()).thenReturn(body);
|
||||
|
||||
Set<HttpMessageReader<?>> messageReaders = Collections
|
||||
.singleton(new DecoderHttpMessageReader<String>(new StringDecoder()));
|
||||
when(mockWebClientStrategies.messageReaders()).thenReturn(messageReaders::stream);
|
||||
|
||||
Mono<String> resultMono = defaultClientResponse.body(toMono(String.class));
|
||||
assertEquals("foo", resultMono.block());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bodyToMono() throws Exception {
|
||||
DefaultDataBufferFactory factory = new DefaultDataBufferFactory();
|
||||
DefaultDataBuffer dataBuffer =
|
||||
factory.wrap(ByteBuffer.wrap("foo".getBytes(StandardCharsets.UTF_8)));
|
||||
Flux<DataBuffer> body = Flux.just(dataBuffer);
|
||||
|
||||
HttpHeaders httpHeaders = new HttpHeaders();
|
||||
httpHeaders.setContentType(MediaType.TEXT_PLAIN);
|
||||
when(mockResponse.getHeaders()).thenReturn(httpHeaders);
|
||||
when(mockResponse.getStatusCode()).thenReturn(HttpStatus.OK);
|
||||
when(mockResponse.getBody()).thenReturn(body);
|
||||
|
||||
Set<HttpMessageReader<?>> messageReaders = Collections
|
||||
.singleton(new DecoderHttpMessageReader<String>(new StringDecoder()));
|
||||
when(mockWebClientStrategies.messageReaders()).thenReturn(messageReaders::stream);
|
||||
|
||||
Mono<String> resultMono = defaultClientResponse.bodyToMono(String.class);
|
||||
assertEquals("foo", resultMono.block());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bodyToMonoError() throws Exception {
|
||||
HttpHeaders httpHeaders = new HttpHeaders();
|
||||
httpHeaders.setContentType(MediaType.TEXT_PLAIN);
|
||||
when(mockResponse.getHeaders()).thenReturn(httpHeaders);
|
||||
when(mockResponse.getStatusCode()).thenReturn(HttpStatus.NOT_FOUND);
|
||||
|
||||
Set<HttpMessageReader<?>> messageReaders = Collections
|
||||
.singleton(new DecoderHttpMessageReader<String>(new StringDecoder()));
|
||||
when(mockWebClientStrategies.messageReaders()).thenReturn(messageReaders::stream);
|
||||
|
||||
Mono<String> resultMono = defaultClientResponse.bodyToMono(String.class);
|
||||
|
||||
StepVerifier.create(resultMono)
|
||||
.expectError(WebClientException.class)
|
||||
.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bodyToFlux() throws Exception {
|
||||
DefaultDataBufferFactory factory = new DefaultDataBufferFactory();
|
||||
DefaultDataBuffer dataBuffer =
|
||||
factory.wrap(ByteBuffer.wrap("foo".getBytes(StandardCharsets.UTF_8)));
|
||||
Flux<DataBuffer> body = Flux.just(dataBuffer);
|
||||
|
||||
HttpHeaders httpHeaders = new HttpHeaders();
|
||||
httpHeaders.setContentType(MediaType.TEXT_PLAIN);
|
||||
when(mockResponse.getHeaders()).thenReturn(httpHeaders);
|
||||
when(mockResponse.getStatusCode()).thenReturn(HttpStatus.OK);
|
||||
when(mockResponse.getBody()).thenReturn(body);
|
||||
|
||||
Set<HttpMessageReader<?>> messageReaders = Collections
|
||||
.singleton(new DecoderHttpMessageReader<String>(new StringDecoder()));
|
||||
when(mockWebClientStrategies.messageReaders()).thenReturn(messageReaders::stream);
|
||||
|
||||
Flux<String> resultFlux = defaultClientResponse.bodyToFlux(String.class);
|
||||
Mono<List<String>> result = resultFlux.collectList();
|
||||
assertEquals(Collections.singletonList("foo"), result.block());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bodyToFluxError() throws Exception {
|
||||
HttpHeaders httpHeaders = new HttpHeaders();
|
||||
httpHeaders.setContentType(MediaType.TEXT_PLAIN);
|
||||
when(mockResponse.getHeaders()).thenReturn(httpHeaders);
|
||||
when(mockResponse.getStatusCode()).thenReturn(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
|
||||
Set<HttpMessageReader<?>> messageReaders = Collections
|
||||
.singleton(new DecoderHttpMessageReader<String>(new StringDecoder()));
|
||||
when(mockWebClientStrategies.messageReaders()).thenReturn(messageReaders::stream);
|
||||
|
||||
Flux<String> resultFlux = defaultClientResponse.bodyToFlux(String.class);
|
||||
StepVerifier.create(resultFlux)
|
||||
.expectError(WebClientException.class)
|
||||
.verify();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
/*
|
||||
* 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 org.junit.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* @author Arjen Poutsma
|
||||
*/
|
||||
public class ExchangeFilterFunctionsTests {
|
||||
|
||||
@Test
|
||||
public void andThen() throws Exception {
|
||||
ClientRequest<Void> request = ClientRequest.GET("http://example.com").build();
|
||||
ClientResponse response = mock(ClientResponse.class);
|
||||
ExchangeFunction exchange = r -> Mono.just(response);
|
||||
|
||||
boolean[] filtersInvoked = new boolean[2];
|
||||
ExchangeFilterFunction filter1 = (r, n) -> {
|
||||
assertFalse(filtersInvoked[0]);
|
||||
assertFalse(filtersInvoked[1]);
|
||||
filtersInvoked[0] = true;
|
||||
assertFalse(filtersInvoked[1]);
|
||||
return n.exchange(r);
|
||||
};
|
||||
ExchangeFilterFunction filter2 = (r, n) -> {
|
||||
assertTrue(filtersInvoked[0]);
|
||||
assertFalse(filtersInvoked[1]);
|
||||
filtersInvoked[1] = true;
|
||||
return n.exchange(r);
|
||||
};
|
||||
ExchangeFilterFunction filter = filter1.andThen(filter2);
|
||||
|
||||
|
||||
ClientResponse result = filter.filter(request, exchange).block();
|
||||
assertEquals(response, result);
|
||||
|
||||
assertTrue(filtersInvoked[0]);
|
||||
assertTrue(filtersInvoked[1]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void apply() throws Exception {
|
||||
ClientRequest<Void> request = ClientRequest.GET("http://example.com").build();
|
||||
ClientResponse response = mock(ClientResponse.class);
|
||||
ExchangeFunction exchange = r -> Mono.just(response);
|
||||
|
||||
boolean[] filterInvoked = new boolean[1];
|
||||
ExchangeFilterFunction filter = (r, n) -> {
|
||||
assertFalse(filterInvoked[0]);
|
||||
filterInvoked[0] = true;
|
||||
return n.exchange(r);
|
||||
};
|
||||
|
||||
ExchangeFunction filteredExchange = filter.apply(exchange);
|
||||
ClientResponse result = filteredExchange.exchange(request).block();
|
||||
assertEquals(response, result);
|
||||
assertTrue(filterInvoked[0]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void basicAuthentication() throws Exception {
|
||||
ClientRequest<Void> request = ClientRequest.GET("http://example.com").build();
|
||||
ClientResponse response = mock(ClientResponse.class);
|
||||
|
||||
ExchangeFunction exchange = r -> {
|
||||
assertTrue(r.headers().containsKey(HttpHeaders.AUTHORIZATION));
|
||||
assertTrue(r.headers().getFirst(HttpHeaders.AUTHORIZATION).startsWith("Basic "));
|
||||
return Mono.just(response);
|
||||
};
|
||||
|
||||
ExchangeFilterFunction auth = ExchangeFilterFunctions.basicAuthentication("foo", "bar");
|
||||
assertFalse(request.headers().containsKey(HttpHeaders.AUTHORIZATION));
|
||||
ClientResponse result = auth.filter(request, exchange).block();
|
||||
assertEquals(response, result);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,332 +0,0 @@
|
||||
/*
|
||||
* 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.time.Duration;
|
||||
|
||||
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.test.StepVerifier;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
|
||||
import org.springframework.http.codec.BodyExtractors;
|
||||
import org.springframework.http.codec.BodyInserters;
|
||||
import org.springframework.http.codec.Pojo;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.springframework.http.codec.BodyExtractors.toFlux;
|
||||
import static org.springframework.http.codec.BodyExtractors.toMono;
|
||||
|
||||
/**
|
||||
* {@link WebClient} integration tests with the {@code Flux} and {@code Mono} API.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
public class WebClientIntegrationTests {
|
||||
|
||||
private MockWebServer server;
|
||||
|
||||
private WebClient webClient;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
this.server = new MockWebServer();
|
||||
this.webClient = WebClient.create(new ReactorClientHttpConnector());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void headers() throws Exception {
|
||||
HttpUrl baseUrl = server.url("/greeting?name=Spring");
|
||||
this.server.enqueue(new MockResponse().setHeader("Content-Type", "text/plain").setBody("Hello Spring!"));
|
||||
|
||||
ClientRequest<Void> request = ClientRequest.GET(baseUrl.toString()).build();
|
||||
Mono<HttpHeaders> result = this.webClient
|
||||
.exchange(request)
|
||||
.map(response -> response.headers().asHttpHeaders());
|
||||
|
||||
StepVerifier.create(result)
|
||||
.consumeNextWith(
|
||||
httpHeaders -> {
|
||||
assertEquals(MediaType.TEXT_PLAIN, httpHeaders.getContentType());
|
||||
assertEquals(13L, httpHeaders.getContentLength());
|
||||
})
|
||||
.expectComplete()
|
||||
.verify(Duration.ofSeconds(3));
|
||||
|
||||
RecordedRequest recordedRequest = server.takeRequest();
|
||||
assertEquals(1, server.getRequestCount());
|
||||
assertEquals("*/*", recordedRequest.getHeader(HttpHeaders.ACCEPT));
|
||||
assertEquals("/greeting?name=Spring", recordedRequest.getPath());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void plainText() throws Exception {
|
||||
HttpUrl baseUrl = server.url("/greeting?name=Spring");
|
||||
this.server.enqueue(new MockResponse().setBody("Hello Spring!"));
|
||||
|
||||
ClientRequest<Void> request = ClientRequest.GET(baseUrl.toString())
|
||||
.header("X-Test-Header", "testvalue")
|
||||
.build();
|
||||
|
||||
Mono<String> result = this.webClient
|
||||
.exchange(request)
|
||||
.then(response -> response.body(toMono(String.class)));
|
||||
|
||||
StepVerifier.create(result)
|
||||
.expectNext("Hello Spring!")
|
||||
.expectComplete()
|
||||
.verify(Duration.ofSeconds(3));
|
||||
|
||||
RecordedRequest recordedRequest = server.takeRequest();
|
||||
assertEquals(1, server.getRequestCount());
|
||||
assertEquals("testvalue", recordedRequest.getHeader("X-Test-Header"));
|
||||
assertEquals("*/*", recordedRequest.getHeader(HttpHeaders.ACCEPT));
|
||||
assertEquals("/greeting?name=Spring", recordedRequest.getPath());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void jsonString() throws Exception {
|
||||
HttpUrl baseUrl = server.url("/json");
|
||||
String content = "{\"bar\":\"barbar\",\"foo\":\"foofoo\"}";
|
||||
this.server.enqueue(new MockResponse().setHeader("Content-Type", "application/json")
|
||||
.setBody(content));
|
||||
|
||||
ClientRequest<Void> request = ClientRequest.GET(baseUrl.toString())
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.build();
|
||||
|
||||
Mono<String> result = this.webClient
|
||||
.exchange(request)
|
||||
.then(response -> response.body(toMono(String.class)));
|
||||
|
||||
StepVerifier.create(result)
|
||||
.expectNext(content)
|
||||
.expectComplete()
|
||||
.verify(Duration.ofSeconds(3));
|
||||
|
||||
RecordedRequest recordedRequest = server.takeRequest();
|
||||
assertEquals(1, server.getRequestCount());
|
||||
assertEquals("/json", recordedRequest.getPath());
|
||||
assertEquals("application/json", recordedRequest.getHeader(HttpHeaders.ACCEPT));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void jsonPojoMono() throws Exception {
|
||||
HttpUrl baseUrl = server.url("/pojo");
|
||||
this.server.enqueue(new MockResponse().setHeader("Content-Type", "application/json")
|
||||
.setBody("{\"bar\":\"barbar\",\"foo\":\"foofoo\"}"));
|
||||
|
||||
ClientRequest<Void> request = ClientRequest.GET(baseUrl.toString())
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.build();
|
||||
|
||||
Mono<Pojo> result = this.webClient
|
||||
.exchange(request)
|
||||
.then(response -> response.body(toMono(Pojo.class)));
|
||||
|
||||
StepVerifier.create(result)
|
||||
.consumeNextWith(p -> assertEquals("barbar", p.getBar()))
|
||||
.expectComplete()
|
||||
.verify(Duration.ofSeconds(3));
|
||||
|
||||
RecordedRequest recordedRequest = server.takeRequest();
|
||||
assertEquals(1, server.getRequestCount());
|
||||
assertEquals("/pojo", recordedRequest.getPath());
|
||||
assertEquals("application/json", recordedRequest.getHeader(HttpHeaders.ACCEPT));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void jsonPojoFlux() 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\"}]"));
|
||||
|
||||
ClientRequest<Void> request = ClientRequest.GET(baseUrl.toString())
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.build();
|
||||
|
||||
Flux<Pojo> result = this.webClient
|
||||
.exchange(request)
|
||||
.flatMap(response -> response.body(toFlux(Pojo.class)));
|
||||
|
||||
StepVerifier.create(result)
|
||||
.consumeNextWith(p -> assertThat(p.getBar(), Matchers.is("bar1")))
|
||||
.consumeNextWith(p -> assertThat(p.getBar(), Matchers.is("bar2")))
|
||||
.expectComplete()
|
||||
.verify(Duration.ofSeconds(3));
|
||||
|
||||
RecordedRequest recordedRequest = server.takeRequest();
|
||||
assertEquals(1, server.getRequestCount());
|
||||
assertEquals("/pojos", recordedRequest.getPath());
|
||||
assertEquals("application/json", recordedRequest.getHeader(HttpHeaders.ACCEPT));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void postJsonPojo() throws Exception {
|
||||
HttpUrl baseUrl = server.url("/pojo/capitalize");
|
||||
this.server.enqueue(new MockResponse()
|
||||
.setHeader("Content-Type", "application/json")
|
||||
.setBody("{\"bar\":\"BARBAR\",\"foo\":\"FOOFOO\"}"));
|
||||
|
||||
Pojo spring = new Pojo("foofoo", "barbar");
|
||||
ClientRequest<Pojo> request = ClientRequest.POST(baseUrl.toString())
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(BodyInserters.fromObject(spring));
|
||||
|
||||
Mono<Pojo> result = this.webClient
|
||||
.exchange(request)
|
||||
.then(response -> response.body(BodyExtractors.toMono(Pojo.class)));
|
||||
|
||||
StepVerifier.create(result)
|
||||
.consumeNextWith(p -> assertEquals("BARBAR", p.getBar()))
|
||||
.expectComplete()
|
||||
.verify(Duration.ofSeconds(3));
|
||||
|
||||
RecordedRequest recordedRequest = server.takeRequest();
|
||||
assertEquals(1, server.getRequestCount());
|
||||
assertEquals("/pojo/capitalize", recordedRequest.getPath());
|
||||
assertEquals("{\"foo\":\"foofoo\",\"bar\":\"barbar\"}", recordedRequest.getBody().readUtf8());
|
||||
assertEquals("chunked", recordedRequest.getHeader(HttpHeaders.TRANSFER_ENCODING));
|
||||
assertEquals("application/json", recordedRequest.getHeader(HttpHeaders.ACCEPT));
|
||||
assertEquals("application/json", recordedRequest.getHeader(HttpHeaders.CONTENT_TYPE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cookies() throws Exception {
|
||||
HttpUrl baseUrl = server.url("/test");
|
||||
this.server.enqueue(new MockResponse()
|
||||
.setHeader("Content-Type", "text/plain").setBody("test"));
|
||||
|
||||
ClientRequest<Void> request = ClientRequest.GET(baseUrl.toString())
|
||||
.cookie("testkey", "testvalue")
|
||||
.build();
|
||||
|
||||
Mono<String> result = this.webClient
|
||||
.exchange(request)
|
||||
.then(response -> response.body(toMono(String.class)));
|
||||
|
||||
StepVerifier.create(result)
|
||||
.expectNext("test")
|
||||
.expectComplete()
|
||||
.verify(Duration.ofSeconds(3));
|
||||
|
||||
RecordedRequest recordedRequest = server.takeRequest();
|
||||
assertEquals(1, server.getRequestCount());
|
||||
assertEquals("/test", recordedRequest.getPath());
|
||||
assertEquals("testkey=testvalue", recordedRequest.getHeader(HttpHeaders.COOKIE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void notFound() throws Exception {
|
||||
HttpUrl baseUrl = server.url("/greeting?name=Spring");
|
||||
this.server.enqueue(new MockResponse().setResponseCode(404)
|
||||
.setHeader("Content-Type", "text/plain").setBody("Not Found"));
|
||||
|
||||
ClientRequest<Void> request = ClientRequest.GET(baseUrl.toString()).build();
|
||||
|
||||
Mono<ClientResponse> result = this.webClient
|
||||
.exchange(request);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.consumeNextWith(response -> {
|
||||
assertEquals(HttpStatus.NOT_FOUND, response.statusCode());
|
||||
})
|
||||
.expectComplete()
|
||||
.verify(Duration.ofSeconds(3));
|
||||
|
||||
RecordedRequest recordedRequest = server.takeRequest();
|
||||
assertEquals(1, server.getRequestCount());
|
||||
assertEquals("*/*", recordedRequest.getHeader(HttpHeaders.ACCEPT));
|
||||
assertEquals("/greeting?name=Spring", recordedRequest.getPath());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildFilter() throws Exception {
|
||||
HttpUrl baseUrl = server.url("/greeting?name=Spring");
|
||||
this.server.enqueue(new MockResponse().setHeader("Content-Type", "text/plain").setBody("Hello Spring!"));
|
||||
|
||||
ExchangeFilterFunction filter = (request, next) -> {
|
||||
ClientRequest<?> filteredRequest = ClientRequest.from(request)
|
||||
.header("foo", "bar").build();
|
||||
return next.exchange(filteredRequest);
|
||||
};
|
||||
WebClient filteredClient = WebClient.builder(new ReactorClientHttpConnector())
|
||||
.filter(filter).build();
|
||||
|
||||
ClientRequest<Void> request = ClientRequest.GET(baseUrl.toString()).build();
|
||||
|
||||
Mono<String> result = filteredClient.exchange(request)
|
||||
.then(response -> response.body(toMono(String.class)));
|
||||
|
||||
StepVerifier.create(result)
|
||||
.expectNext("Hello Spring!")
|
||||
.expectComplete()
|
||||
.verify(Duration.ofSeconds(3));
|
||||
|
||||
RecordedRequest recordedRequest = server.takeRequest();
|
||||
assertEquals(1, server.getRequestCount());
|
||||
assertEquals("bar", recordedRequest.getHeader("foo"));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void filter() throws Exception {
|
||||
HttpUrl baseUrl = server.url("/greeting?name=Spring");
|
||||
this.server.enqueue(new MockResponse().setHeader("Content-Type", "text/plain").setBody("Hello Spring!"));
|
||||
|
||||
ExchangeFilterFunction filter = (request, next) -> {
|
||||
ClientRequest<?> filteredRequest = ClientRequest.from(request)
|
||||
.header("foo", "bar").build();
|
||||
return next.exchange(filteredRequest);
|
||||
};
|
||||
WebClient client = WebClient.create(new ReactorClientHttpConnector());
|
||||
WebClient filteredClient = client.filter(filter);
|
||||
|
||||
ClientRequest<Void> request = ClientRequest.GET(baseUrl.toString()).build();
|
||||
|
||||
Mono<String> result = filteredClient.exchange(request)
|
||||
.then(response -> response.body(toMono(String.class)));
|
||||
|
||||
StepVerifier.create(result)
|
||||
.expectNext("Hello Spring!")
|
||||
.expectComplete()
|
||||
.verify(Duration.ofSeconds(3));
|
||||
|
||||
RecordedRequest recordedRequest = server.takeRequest();
|
||||
assertEquals(1, server.getRequestCount());
|
||||
assertEquals("bar", recordedRequest.getHeader("foo"));
|
||||
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
this.server.shutdown();
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
/*
|
||||
* 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.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.context.support.StaticApplicationContext;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ReactiveHttpInputMessage;
|
||||
import org.springframework.http.ReactiveHttpOutputMessage;
|
||||
import org.springframework.http.codec.HttpMessageReader;
|
||||
import org.springframework.http.codec.HttpMessageWriter;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
* @author Arjen Poutsma
|
||||
*/
|
||||
public class WebClientStrategiesTests {
|
||||
|
||||
@Test
|
||||
public void empty() {
|
||||
WebClientStrategies strategies = WebClientStrategies.empty().build();
|
||||
assertEquals(Optional.empty(), strategies.messageReaders().get().findFirst());
|
||||
assertEquals(Optional.empty(), strategies.messageWriters().get().findFirst());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ofSuppliers() {
|
||||
HttpMessageReader<?> messageReader = new DummyMessageReader();
|
||||
HttpMessageWriter<?> messageWriter = new DummyMessageWriter();
|
||||
|
||||
WebClientStrategies strategies = WebClientStrategies.of(
|
||||
() -> Stream.of(messageReader),
|
||||
() -> Stream.of(messageWriter));
|
||||
|
||||
assertEquals(1L, strategies.messageReaders().get().collect(Collectors.counting()).longValue());
|
||||
assertEquals(Optional.of(messageReader), strategies.messageReaders().get().findFirst());
|
||||
|
||||
assertEquals(1L, strategies.messageWriters().get().collect(Collectors.counting()).longValue());
|
||||
assertEquals(Optional.of(messageWriter), strategies.messageWriters().get().findFirst());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toConfiguration() throws Exception {
|
||||
StaticApplicationContext applicationContext = new StaticApplicationContext();
|
||||
applicationContext.registerSingleton("messageWriter", DummyMessageWriter.class);
|
||||
applicationContext.registerSingleton("messageReader", DummyMessageReader.class);
|
||||
applicationContext.refresh();
|
||||
|
||||
WebClientStrategies strategies = WebClientStrategies.of(applicationContext);
|
||||
assertTrue(strategies.messageReaders().get()
|
||||
.allMatch(r -> r instanceof DummyMessageReader));
|
||||
assertTrue(strategies.messageWriters().get()
|
||||
.allMatch(r -> r instanceof DummyMessageWriter));
|
||||
|
||||
}
|
||||
|
||||
|
||||
private static class DummyMessageWriter implements HttpMessageWriter<Object> {
|
||||
|
||||
@Override
|
||||
public boolean canWrite(ResolvableType type, MediaType mediaType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MediaType> getWritableMediaTypes() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> write(Publisher<?> inputStream, ResolvableType type,
|
||||
MediaType contentType,
|
||||
ReactiveHttpOutputMessage outputMessage,
|
||||
Map<String, Object> hints) {
|
||||
return Mono.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private static class DummyMessageReader implements HttpMessageReader<Object> {
|
||||
|
||||
@Override
|
||||
public boolean canRead(ResolvableType type, MediaType mediaType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MediaType> getReadableMediaTypes() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<Object> read(ResolvableType type, ReactiveHttpInputMessage inputMessage,
|
||||
Map<String, Object> hints) {
|
||||
return Flux.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Object> readMono(ResolvableType type, ReactiveHttpInputMessage inputMessage,
|
||||
Map<String, Object> hints) {
|
||||
return Mono.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user