Move WebClient to web.reactive.function.client

This commit is contained in:
Arjen Poutsma
2016-12-15 12:14:15 +01:00
parent dfcd5b9ed9
commit aa8f531526
22 changed files with 99 additions and 61 deletions

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
});
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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&lt;Void&gt; request = ClientRequest.GET("http://example.com/resource").build();
*
* Mono&lt;String&gt; 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();
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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;