/* * 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.ZonedDateTime; 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.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.http.client.reactive.ClientHttpRequest; import org.springframework.util.MultiValueMap; import org.springframework.web.reactive.function.BodyInserter; import org.springframework.web.util.UriBuilder; import org.springframework.web.util.UriBuilderFactory; /** * The main entry point for initiating web requests on the client side. * * <pre class="code"> * // Initialize the client * WebClient client = WebClient.create("http://abc.com"); * * // Perform requests... * Mono<String> result = client.get() * .uri("/foo") * .exchange() * .then(response -> response.bodyToMono(String.class)); * </pre> * * @author Rossen Stoyanchev * @author Arjen Poutsma * @since 5.0 */ public interface WebClient { /** * Prepare an HTTP GET request. * @return a spec for specifying the target URL */ UriSpec<RequestHeadersSpec<?>> get(); /** * Prepare an HTTP HEAD request. * @return a spec for specifying the target URL */ UriSpec<RequestHeadersSpec<?>> head(); /** * Prepare an HTTP POST request. * @return a spec for specifying the target URL */ UriSpec<RequestBodySpec> post(); /** * Prepare an HTTP PUT request. * @return a spec for specifying the target URL */ UriSpec<RequestBodySpec> put(); /** * Prepare an HTTP PATCH request. * @return a spec for specifying the target URL */ UriSpec<RequestBodySpec> patch(); /** * Prepare an HTTP DELETE request. * @return a spec for specifying the target URL */ UriSpec<RequestHeadersSpec<?>> delete(); /** * Prepare an HTTP OPTIONS request. * @return a spec for specifying the target URL */ UriSpec<RequestHeadersSpec<?>> options(); /** * Prepare a request for the specified {@code HttpMethod}. * @return a spec for specifying the target URL */ UriSpec<RequestBodySpec> method(HttpMethod method); /** * Filter the client with the given {@code ExchangeFilterFunction}. * @param filterFunction the filter to apply to this client * @return the filtered client * @see ExchangeFilterFunction#apply(ExchangeFunction) */ WebClient filter(ExchangeFilterFunction filterFunction); // Static, factory methods /** * Create a new {@code WebClient} with no default, shared preferences across * requests such as base URI, default headers, and others. * @see #create(String) */ static WebClient create() { return new DefaultWebClientBuilder().build(); } /** * Configure a base URI for requests performed through the client for * example to avoid repeating the same host, port, base path, or even * query parameters with every request. * <p>For example given this initialization: * <pre class="code"> * WebClient client = WebClient.create("http://abc.com/v1"); * </pre> * <p>The base URI is applied to exchanges with a URI template: * <pre class="code"> * // GET http://abc.com/v1/accounts/43 * Mono<Account> result = client.get() * .uri("/accounts/{id}", 43) * .exchange() * .then(response -> response.bodyToMono(Account.class)); * </pre> * <p>The base URI is also applied to exchanges with a {@code UriBuilder}: * <pre class="code"> * // GET http://abc.com/v1/accounts?q=12 * Flux<Account> result = client.get() * .uri(builder -> builder.path("/accounts").queryParam("q", "12").build()) * .exchange() * .then(response -> response.bodyToFlux(Account.class)); * </pre> * <p>The base URI can be overridden with an absolute URI: * <pre class="code"> * // GET http://xyz.com/path * Mono<Account> result = client.get() * .uri("http://xyz.com/path") * .exchange() * .then(response -> response.bodyToMono(Account.class)); * </pre> * <p>The base URI can be partially overridden with a {@code UriBuilder}: * <pre class="code"> * // GET http://abc.com/v2/accounts?q=12 * Flux<Account> result = client.get() * .uri(builder -> builder.replacePath("/v2/accounts").queryParam("q", "12").build()) * .exchange() * .then(response -> response.bodyToFlux(Account.class)); * </pre> * @param baseUrl the base URI for all requests */ static WebClient create(String baseUrl) { return new DefaultWebClientBuilder().baseUrl(baseUrl).build(); } /** * Obtain a {@code WebClient} builder. */ static WebClient.Builder builder() { return new DefaultWebClientBuilder(); } /** * A mutable builder for a {@link WebClient}. */ interface Builder { /** * Configure a base URI as described in {@link WebClient#create(String) * WebClient.create(String)}. * @see #defaultUriVariables(Map) * @see #uriBuilderFactory(UriBuilderFactory) */ Builder baseUrl(String baseUrl); /** * Configure default URI variable values that will be used when expanding * URI templates using a {@link Map}. * @param defaultUriVariables the default values to use * @see #baseUrl(String) * @see #uriBuilderFactory(UriBuilderFactory) */ Builder defaultUriVariables(Map<String, ?> defaultUriVariables); /** * Provide a pre-configured {@link UriBuilderFactory} instance. This is * an alternative to and effectively overrides the following: * <ul> * <li>{@link #baseUrl(String)} * <li>{@link #defaultUriVariables(Map)}. * </ul> * @param uriBuilderFactory the URI builder factory to use * @see #baseUrl(String) * @see #defaultUriVariables(Map) */ Builder uriBuilderFactory(UriBuilderFactory uriBuilderFactory); /** * Add the given header to all requests that haven't added it. * @param headerName the header name * @param headerValues the header values */ Builder defaultHeader(String headerName, String... headerValues); /** * Add the given header to all requests that haven't added it. * @param cookieName the cookie name * @param cookieValues the cookie values */ Builder defaultCookie(String cookieName, String... cookieValues); /** * Configure the {@link ClientHttpConnector} to use. * <p>By default an instance of * {@link org.springframework.http.client.reactive.ReactorClientHttpConnector * ReactorClientHttpConnector} is created if this is not set. However a * shared instance may be passed instead, e.g. for use with multiple * {@code WebClient}'s targeting different base URIs. * @param connector the connector to use * @see #exchangeStrategies(ExchangeStrategies) * @see #exchangeFunction(ExchangeFunction) */ Builder clientConnector(ClientHttpConnector connector); /** * Configure the {@link ExchangeStrategies} to use. * <p>By default {@link ExchangeStrategies#withDefaults()} is used. * @param strategies the strategies to use * @see #clientConnector(ClientHttpConnector) * @see #exchangeFunction(ExchangeFunction) */ Builder exchangeStrategies(ExchangeStrategies strategies); /** * Provide a pre-configured {@link ExchangeFunction} instance. This is * an alternative to and effectively overrides the following: * <ul> * <li>{@link #clientConnector(ClientHttpConnector)} * <li>{@link #exchangeStrategies(ExchangeStrategies)}. * </ul> * @param exchangeFunction the exchange function to use * @see #clientConnector(ClientHttpConnector) * @see #exchangeStrategies(ExchangeStrategies) */ Builder exchangeFunction(ExchangeFunction exchangeFunction); /** * Builder the {@link WebClient} instance. */ WebClient build(); } /** * Contract for specifying the URI for a request. */ interface UriSpec<S extends RequestHeadersSpec<?>> { /** * Specify the URI using an absolute, fully constructed {@link URI}. */ S uri(URI uri); /** * Specify the URI for the request using a URI template and URI variables. * If a {@link UriBuilderFactory} was configured for the client (e.g. * with a base URI) it will be used to expand the URI template. */ S uri(String uri, Object... uriVariables); /** * Specify the URI for the request using a URI template and URI variables. * If a {@link UriBuilderFactory} was configured for the client (e.g. * with a base URI) it will be used to expand the URI template. */ S uri(String uri, Map<String, ?> uriVariables); /** * Build the URI for the request using the {@link UriBuilderFactory} * configured for this client. */ S uri(Function<UriBuilder, URI> uriFunction); } /** * Contract for specifying request headers leading up to the exchange. */ interface RequestHeadersSpec<S extends RequestHeadersSpec<S>> { /** * 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 */ S 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 */ S acceptCharset(Charset... acceptableCharsets); /** * Add a cookie with the given name and value. * @param name the cookie name * @param value the cookie value * @return this builder */ S 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 */ S cookies(MultiValueMap<String, String> cookies); /** * 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 */ S ifModifiedSince(ZonedDateTime ifModifiedSince); /** * Set the values of the {@code If-None-Match} header. * @param ifNoneMatches the new value of the header * @return this builder */ S ifNoneMatch(String... ifNoneMatches); /** * Add the given, single header value under the given name. * @param headerName the header name * @param headerValues the header value(s) * @return this builder */ S header(String headerName, String... headerValues); /** * Copy the given headers into the entity's headers map. * @param headers the existing headers to copy from * @return this builder */ S headers(HttpHeaders headers); /** * Exchange the request for a {@code ClientResponse} with full access * to the response status and headers before extracting the body. * * <p>Use {@link Mono#flatMap(Function)} or * {@link Mono#flatMapMany(Function)} to compose further on the response: * * <pre> * Mono<Pojo> mono = client.get().uri("/") * .accept(MediaType.APPLICATION_JSON) * .exchange() * .flatMap(response -> response.bodyToMono(Pojo.class)); * * Flux<Pojo> flux = client.get().uri("/") * .accept(MediaType.APPLICATION_STREAM_JSON) * .exchange() * .flatMapMany(response -> response.bodyToFlux(Pojo.class)); * </pre> * * @return a {@code Mono} with the response * @see #retrieve() */ Mono<ClientResponse> exchange(); /** * A variant of {@link #exchange()} that provides the shortest path to * retrieving the full response (i.e. status, headers, and body) where * instead of returning {@code Mono<ClientResponse>} it exposes shortcut * methods to extract the response body. * * <p>Use of this method is simpler when you don't need to deal directly * with {@link ClientResponse}, e.g. to use a custom {@code BodyExtractor} * or to check the status and headers before extracting the response. * * <pre> * Mono<Pojo> bodyMono = client.get().uri("/") * .accept(MediaType.APPLICATION_JSON) * .retrieve() * .bodyToMono(Pojo.class); * * Mono<ResponseEntity<Pojo>> entityMono = client.get().uri("/") * .accept(MediaType.APPLICATION_JSON) * .retrieve() * .bodyToEntity(Pojo.class); * </pre> * * @return spec with options for extracting the response body */ ResponseSpec retrieve(); } interface RequestBodySpec extends RequestHeadersSpec<RequestBodySpec> { /** * 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) */ RequestBodySpec 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) */ RequestBodySpec contentType(MediaType contentType); /** * Set the body of the request to the given {@code BodyInserter}. * @param inserter the {@code BodyInserter} that writes to the request * @return this builder */ RequestHeadersSpec<?> body(BodyInserter<?, ? super ClientHttpRequest> inserter); /** * Set the body of the request to the given asynchronous {@code Publisher}. * <p>This method is a convenient shortcut for {@link #body(BodyInserter)} with a * {@linkplain org.springframework.web.reactive.function.BodyInserters#fromPublisher} * Publisher body inserter}. * @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 <P> the type of the {@code Publisher} * @return this builder */ <T, P extends Publisher<T>> RequestHeadersSpec<?> body(P publisher, Class<T> elementClass); /** * Set the body of the request to the given synchronous {@code Object}. * <p>This method is a convenient shortcut for {@link #body(BodyInserter)} with a * {@linkplain org.springframework.web.reactive.function.BodyInserters#fromObject * Object body inserter}. * @param body the {@code Object} to write to the request * @return this builder */ RequestHeadersSpec<?> syncBody(Object body); } interface ResponseSpec { /** * 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 bodyType the expected response body type * @param <T> response body type * @return a mono containing the body, or a {@link WebClientException} if the status code is * 4xx or 5xx */ <T> Mono<T> bodyToMono(Class<T> bodyType); /** * 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 elementType the type of element in the response * @param <T> the type of elements in the response * @return a flux containing the body, or a {@link WebClientException} if the status code is * 4xx or 5xx */ <T> Flux<T> bodyToFlux(Class<T> elementType); /** * Returns the response as a delayed {@code ResponseEntity}. 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 bodyType the expected response body type * @param <T> response body type * @return {@code Mono} with the {@code ResponseEntity} */ <T> Mono<ResponseEntity<T>> toEntity(Class<T> bodyType); /** * Returns the response as a delayed list of {@code ResponseEntity}s. 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 elementType the expected response body list element type * @param <T> the type of elements in the list * @return {@code Mono} with the list of {@code ResponseEntity}s */ <T> Mono<ResponseEntity<List<T>>> toEntityList(Class<T> elementType); } }