/*
* Copyright 2002-2017 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.reactive.function.client;
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.List;
import java.util.Map;
import java.util.function.Function;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.reactive.ClientHttpRequest;
import org.springframework.http.client.reactive.ClientHttpResponse;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyExtractor;
import org.springframework.web.reactive.function.BodyExtractors;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.util.DefaultUriBuilderFactory;
import org.springframework.web.util.UriBuilder;
import org.springframework.web.util.UriBuilderFactory;
/**
* Default implementation of {@link WebClient}.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
class DefaultWebClient implements WebClient {
private final ExchangeFunction exchangeFunction;
private final UriBuilderFactory uriBuilderFactory;
private final HttpHeaders defaultHeaders;
private final MultiValueMap<String, String> defaultCookies;
DefaultWebClient(ExchangeFunction exchangeFunction, UriBuilderFactory factory,
HttpHeaders defaultHeaders, MultiValueMap<String, String> defaultCookies) {
this.exchangeFunction = exchangeFunction;
this.uriBuilderFactory = (factory != null ? factory : new DefaultUriBuilderFactory());
this.defaultHeaders = (defaultHeaders != null ?
HttpHeaders.readOnlyHttpHeaders(defaultHeaders) : null);
this.defaultCookies = (defaultCookies != null ?
CollectionUtils.unmodifiableMultiValueMap(defaultCookies) : null);
}
@Override
public UriSpec<RequestHeadersSpec<?>> get() {
return methodInternal(HttpMethod.GET);
}
@Override
public UriSpec<RequestHeadersSpec<?>> head() {
return methodInternal(HttpMethod.HEAD);
}
@Override
public UriSpec<RequestBodySpec> post() {
return methodInternal(HttpMethod.POST);
}
@Override
public UriSpec<RequestBodySpec> put() {
return methodInternal(HttpMethod.PUT);
}
@Override
public UriSpec<RequestBodySpec> patch() {
return methodInternal(HttpMethod.PATCH);
}
@Override
public UriSpec<RequestHeadersSpec<?>> delete() {
return methodInternal(HttpMethod.DELETE);
}
@Override
public UriSpec<RequestHeadersSpec<?>> options() {
return methodInternal(HttpMethod.OPTIONS);
}
@Override
public UriSpec<RequestBodySpec> method(HttpMethod httpMethod) {
return methodInternal(httpMethod);
}
@SuppressWarnings("unchecked")
private <S extends RequestHeadersSpec<?>> UriSpec<S> methodInternal(HttpMethod httpMethod) {
return new DefaultUriSpec<>(httpMethod);
}
@Override
public WebClient filter(ExchangeFilterFunction filterFunction) {
ExchangeFunction filteredExchangeFunction = this.exchangeFunction.filter(filterFunction);
return new DefaultWebClient(filteredExchangeFunction,
this.uriBuilderFactory, this.defaultHeaders, this.defaultCookies);
}
private class DefaultUriSpec<S extends RequestHeadersSpec<?>> implements UriSpec<S> {
private final HttpMethod httpMethod;
DefaultUriSpec(HttpMethod httpMethod) {
this.httpMethod = httpMethod;
}
@Override
public S uri(String uriTemplate, Object... uriVariables) {
return uri(uriBuilderFactory.expand(uriTemplate, uriVariables));
}
@Override
public S uri(String uriTemplate, Map<String, ?> uriVariables) {
return uri(uriBuilderFactory.expand(uriTemplate, uriVariables));
}
@Override
public S uri(Function<UriBuilder, URI> uriFunction) {
return uri(uriFunction.apply(uriBuilderFactory.builder()));
}
@Override
@SuppressWarnings("unchecked")
public S uri(URI uri) {
return (S) new DefaultRequestBodySpec(this.httpMethod, uri);
}
}
private class DefaultRequestBodySpec implements RequestBodySpec {
private final HttpMethod httpMethod;
private final URI uri;
private HttpHeaders headers;
private MultiValueMap<String, String> cookies;
private BodyInserter<?, ? super ClientHttpRequest> inserter;
DefaultRequestBodySpec(HttpMethod httpMethod, URI uri) {
this.httpMethod = httpMethod;
this.uri = uri;
}
private HttpHeaders getHeaders() {
if (this.headers == null) {
this.headers = new HttpHeaders();
}
return this.headers;
}
private MultiValueMap<String, String> getCookies() {
if (this.cookies == null) {
this.cookies = new LinkedMultiValueMap<>(4);
}
return this.cookies;
}
@Override
public DefaultRequestBodySpec header(String headerName, String... headerValues) {
for (String headerValue : headerValues) {
getHeaders().add(headerName, headerValue);
}
return this;
}
@Override
public DefaultRequestBodySpec headers(HttpHeaders headers) {
if (headers != null) {
getHeaders().putAll(headers);
}
return this;
}
@Override
public DefaultRequestBodySpec accept(MediaType... acceptableMediaTypes) {
getHeaders().setAccept(Arrays.asList(acceptableMediaTypes));
return this;
}
@Override
public DefaultRequestBodySpec acceptCharset(Charset... acceptableCharsets) {
getHeaders().setAcceptCharset(Arrays.asList(acceptableCharsets));
return this;
}
@Override
public DefaultRequestBodySpec contentType(MediaType contentType) {
getHeaders().setContentType(contentType);
return this;
}
@Override
public DefaultRequestBodySpec contentLength(long contentLength) {
getHeaders().setContentLength(contentLength);
return this;
}
@Override
public DefaultRequestBodySpec cookie(String name, String value) {
getCookies().add(name, value);
return this;
}
@Override
public DefaultRequestBodySpec cookies(MultiValueMap<String, String> cookies) {
if (cookies != null) {
getCookies().putAll(cookies);
}
return this;
}
@Override
public DefaultRequestBodySpec ifModifiedSince(ZonedDateTime ifModifiedSince) {
ZonedDateTime gmt = ifModifiedSince.withZoneSameInstant(ZoneId.of("GMT"));
String headerValue = DateTimeFormatter.RFC_1123_DATE_TIME.format(gmt);
getHeaders().set(HttpHeaders.IF_MODIFIED_SINCE, headerValue);
return this;
}
@Override
public DefaultRequestBodySpec ifNoneMatch(String... ifNoneMatches) {
getHeaders().setIfNoneMatch(Arrays.asList(ifNoneMatches));
return this;
}
@Override
public RequestHeadersSpec<?> body(BodyInserter<?, ? super ClientHttpRequest> inserter) {
this.inserter = inserter;
return this;
}
@Override
public <T, P extends Publisher<T>> RequestHeadersSpec<?> body(P publisher, Class<T> elementClass) {
this.inserter = BodyInserters.fromPublisher(publisher, elementClass);
return this;
}
@Override
public RequestHeadersSpec<?> syncBody(Object body) {
Assert.isTrue(!(body instanceof Publisher), "Please specify the element class by " +
"using body(Publisher, Class)");
this.inserter = BodyInserters.fromObject(body);
return this;
}
@Override
public Mono<ClientResponse> exchange() {
ClientRequest request = this.inserter != null ?
initRequestBuilder().body(this.inserter).build() :
initRequestBuilder().build();
return exchangeFunction.exchange(request);
}
private ClientRequest.Builder initRequestBuilder() {
return ClientRequest.method(this.httpMethod, this.uri).headers(initHeaders()).cookies(initCookies());
}
private HttpHeaders initHeaders() {
if (CollectionUtils.isEmpty(defaultHeaders) && CollectionUtils.isEmpty(this.headers)) {
return null;
}
else if (CollectionUtils.isEmpty(defaultHeaders)) {
return this.headers;
}
else if (CollectionUtils.isEmpty(this.headers)) {
return defaultHeaders;
}
else {
HttpHeaders result = new HttpHeaders();
result.putAll(this.headers);
defaultHeaders.forEach((name, values) -> {
if (!this.headers.containsKey(name)) {
values.forEach(value -> result.add(name, value));
}
});
return result;
}
}
private MultiValueMap<String, String> initCookies() {
if (CollectionUtils.isEmpty(defaultCookies) && CollectionUtils.isEmpty(this.cookies)) {
return null;
}
else if (CollectionUtils.isEmpty(defaultCookies)) {
return this.cookies;
}
else if (CollectionUtils.isEmpty(this.cookies)) {
return defaultCookies;
}
else {
MultiValueMap<String, String> result = new LinkedMultiValueMap<>();
result.putAll(this.cookies);
defaultCookies.forEach(result::putIfAbsent);
return result;
}
}
@Override
public ResponseSpec retrieve() {
return new DefaultResponseSpec(exchange());
}
}
private static class DefaultResponseSpec implements ResponseSpec {
private final Mono<ClientResponse> responseMono;
DefaultResponseSpec(Mono<ClientResponse> responseMono) {
this.responseMono = responseMono;
}
@Override
public <T> Mono<T> bodyToMono(Class<T> bodyType) {
return this.responseMono.flatMap(
response -> bodyToPublisher(response, BodyExtractors.toMono(bodyType),
Mono::error));
}
@Override
public <T> Flux<T> bodyToFlux(Class<T> elementType) {
return this.responseMono.flatMapMany(
response -> bodyToPublisher(response, BodyExtractors.toFlux(elementType),
Flux::error));
}
private <T extends Publisher<?>> T bodyToPublisher(ClientResponse response,
BodyExtractor<T, ? super ClientHttpResponse> extractor,
Function<WebClientException, T> errorFunction) {
HttpStatus status = response.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 response.body(extractor);
}
}
@Override
public <T> Mono<ResponseEntity<T>> toEntity(Class<T> bodyType) {
return this.responseMono.flatMap(response ->
response.bodyToMono(bodyType).map(body -> {
HttpHeaders headers = response.headers().asHttpHeaders();
return new ResponseEntity<>(body, headers, response.statusCode());
})
);
}
@Override
public <T> Mono<ResponseEntity<List<T>>> toEntityList(Class<T> responseType) {
return this.responseMono.flatMap(response ->
response.bodyToFlux(responseType).collectList().map(body -> {
HttpHeaders headers = response.headers().asHttpHeaders();
return new ResponseEntity<>(body, headers, response.statusCode());
})
);
}
}
}