Introduce RestClient
This commit introduces the RestClient, a synchronous HTTP client that offers an API similar to WebClient, using the same infrastructure (i.e. request factory, error handler, interceptors, etc) as RestTemplate. Closes gh-29552
This commit is contained in:
@@ -0,0 +1,618 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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
|
||||
*
|
||||
* https://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;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.Charset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpRequest;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.StreamingHttpOutputMessage;
|
||||
import org.springframework.http.client.ClientHttpRequest;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.http.client.ClientHttpRequestInitializer;
|
||||
import org.springframework.http.client.ClientHttpRequestInterceptor;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.http.client.InterceptingClientHttpRequestFactory;
|
||||
import org.springframework.http.converter.GenericHttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.util.UriBuilder;
|
||||
import org.springframework.web.util.UriBuilderFactory;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link RestClient}.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @since 6.1
|
||||
*/
|
||||
final class DefaultRestClient implements RestClient {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(DefaultRestClient.class);
|
||||
|
||||
private static final String URI_TEMPLATE_ATTRIBUTE = RestClient.class.getName() + ".uriTemplate";
|
||||
|
||||
|
||||
private final ClientHttpRequestFactory clientRequestFactory;
|
||||
|
||||
@Nullable
|
||||
private volatile ClientHttpRequestFactory interceptingRequestFactory;
|
||||
|
||||
@Nullable
|
||||
private final List<ClientHttpRequestInitializer> initializers;
|
||||
|
||||
@Nullable
|
||||
private final List<ClientHttpRequestInterceptor> interceptors;
|
||||
|
||||
private final UriBuilderFactory uriBuilderFactory;
|
||||
|
||||
@Nullable
|
||||
private final HttpHeaders defaultHeaders;
|
||||
|
||||
private final List<StatusHandler> defaultStatusHandlers;
|
||||
|
||||
private final DefaultRestClientBuilder builder;
|
||||
|
||||
private final List<HttpMessageConverter<?>> messageConverters;
|
||||
|
||||
|
||||
DefaultRestClient(ClientHttpRequestFactory clientRequestFactory, @Nullable List<ClientHttpRequestInterceptor> interceptors,
|
||||
@Nullable List<ClientHttpRequestInitializer> initializers,
|
||||
UriBuilderFactory uriBuilderFactory,
|
||||
@Nullable HttpHeaders defaultHeaders,
|
||||
@Nullable List<StatusHandler> statusHandlers,
|
||||
List<HttpMessageConverter<?>> messageConverters,
|
||||
DefaultRestClientBuilder builder) {
|
||||
|
||||
this.clientRequestFactory = clientRequestFactory;
|
||||
this.initializers = initializers;
|
||||
this.interceptors = interceptors;
|
||||
this.uriBuilderFactory = uriBuilderFactory;
|
||||
this.defaultHeaders = defaultHeaders;
|
||||
this.defaultStatusHandlers = (statusHandlers != null) ? new ArrayList<>(statusHandlers) : new ArrayList<>();
|
||||
this.messageConverters = messageConverters;
|
||||
this.builder = builder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestHeadersUriSpec<?> get() {
|
||||
return methodInternal(HttpMethod.GET);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestHeadersUriSpec<?> head() {
|
||||
return methodInternal(HttpMethod.HEAD);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodyUriSpec post() {
|
||||
return methodInternal(HttpMethod.POST);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodyUriSpec put() {
|
||||
return methodInternal(HttpMethod.PUT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodyUriSpec patch() {
|
||||
return methodInternal(HttpMethod.PATCH);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestHeadersUriSpec<?> delete() {
|
||||
return methodInternal(HttpMethod.DELETE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestHeadersUriSpec<?> options() {
|
||||
return methodInternal(HttpMethod.OPTIONS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodyUriSpec method(HttpMethod method) {
|
||||
Assert.notNull(method, "Method must not be null");
|
||||
return methodInternal(method);
|
||||
}
|
||||
|
||||
private RequestBodyUriSpec methodInternal(HttpMethod httpMethod) {
|
||||
return new DefaultRequestBodyUriSpec(httpMethod);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder mutate() {
|
||||
return new DefaultRestClientBuilder(this.builder);
|
||||
}
|
||||
|
||||
|
||||
private class DefaultRequestBodyUriSpec implements RequestBodyUriSpec {
|
||||
|
||||
private final HttpMethod httpMethod;
|
||||
|
||||
@Nullable
|
||||
private URI uri;
|
||||
|
||||
@Nullable
|
||||
private HttpHeaders headers;
|
||||
|
||||
@Nullable
|
||||
private InternalBody body;
|
||||
|
||||
private final Map<String, Object> attributes = new LinkedHashMap<>(4);
|
||||
|
||||
@Nullable
|
||||
private Consumer<ClientHttpRequest> httpRequestConsumer;
|
||||
|
||||
public DefaultRequestBodyUriSpec(HttpMethod httpMethod) {
|
||||
this.httpMethod = httpMethod;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public RequestBodySpec uri(String uriTemplate, Object... uriVariables) {
|
||||
attribute(URI_TEMPLATE_ATTRIBUTE, uriTemplate);
|
||||
return uri(DefaultRestClient.this.uriBuilderFactory.expand(uriTemplate, uriVariables));
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec uri(String uriTemplate, Map<String, ?> uriVariables) {
|
||||
attribute(URI_TEMPLATE_ATTRIBUTE, uriTemplate);
|
||||
return uri(DefaultRestClient.this.uriBuilderFactory.expand(uriTemplate, uriVariables));
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec uri(String uriTemplate, Function<UriBuilder, URI> uriFunction) {
|
||||
attribute(URI_TEMPLATE_ATTRIBUTE, uriTemplate);
|
||||
return uri(uriFunction.apply(DefaultRestClient.this.uriBuilderFactory.uriString(uriTemplate)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec uri(Function<UriBuilder, URI> uriFunction) {
|
||||
return uri(uriFunction.apply(DefaultRestClient.this.uriBuilderFactory.builder()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec uri(URI uri) {
|
||||
this.uri = uri;
|
||||
return this;
|
||||
}
|
||||
|
||||
private HttpHeaders getHeaders() {
|
||||
if (this.headers == null) {
|
||||
this.headers = new HttpHeaders();
|
||||
}
|
||||
return this.headers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultRequestBodyUriSpec header(String headerName, String... headerValues) {
|
||||
for (String headerValue : headerValues) {
|
||||
getHeaders().add(headerName, headerValue);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultRequestBodyUriSpec headers(Consumer<HttpHeaders> headersConsumer) {
|
||||
headersConsumer.accept(getHeaders());
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultRequestBodyUriSpec accept(MediaType... acceptableMediaTypes) {
|
||||
getHeaders().setAccept(Arrays.asList(acceptableMediaTypes));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultRequestBodyUriSpec acceptCharset(Charset... acceptableCharsets) {
|
||||
getHeaders().setAcceptCharset(Arrays.asList(acceptableCharsets));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultRequestBodyUriSpec contentType(MediaType contentType) {
|
||||
getHeaders().setContentType(contentType);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultRequestBodyUriSpec contentLength(long contentLength) {
|
||||
getHeaders().setContentLength(contentLength);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultRequestBodyUriSpec ifModifiedSince(ZonedDateTime ifModifiedSince) {
|
||||
getHeaders().setIfModifiedSince(ifModifiedSince);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultRequestBodyUriSpec ifNoneMatch(String... ifNoneMatches) {
|
||||
getHeaders().setIfNoneMatch(Arrays.asList(ifNoneMatches));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec attribute(String name, Object value) {
|
||||
this.attributes.put(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec attributes(Consumer<Map<String, Object>> attributesConsumer) {
|
||||
attributesConsumer.accept(this.attributes);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec httpRequest(Consumer<ClientHttpRequest> requestConsumer) {
|
||||
this.httpRequestConsumer = (this.httpRequestConsumer != null ?
|
||||
this.httpRequestConsumer.andThen(requestConsumer) : requestConsumer);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec body(Object body) {
|
||||
this.body = clientHttpRequest -> writeWithMessageConverters(body, body.getClass(), clientHttpRequest);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> RequestBodySpec body(T body, ParameterizedTypeReference<T> bodyType) {
|
||||
this.body = clientHttpRequest -> writeWithMessageConverters(body, bodyType.getType(), clientHttpRequest);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec body(StreamingHttpOutputMessage.Body body) {
|
||||
this.body = request -> body.writeTo(request.getBody());
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
private void writeWithMessageConverters(Object body, Type bodyType, ClientHttpRequest clientRequest)
|
||||
throws IOException {
|
||||
|
||||
MediaType contentType = clientRequest.getHeaders().getContentType();
|
||||
Class<?> bodyClass = body.getClass();
|
||||
|
||||
for (HttpMessageConverter messageConverter : DefaultRestClient.this.messageConverters) {
|
||||
if (messageConverter instanceof GenericHttpMessageConverter genericMessageConverter) {
|
||||
if (genericMessageConverter.canWrite(bodyType, bodyClass, contentType)) {
|
||||
logBody(body, contentType, genericMessageConverter);
|
||||
genericMessageConverter.write(body, bodyType, contentType, clientRequest);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (messageConverter.canWrite(bodyClass, contentType)) {
|
||||
logBody(body, contentType, messageConverter);
|
||||
messageConverter.write(body, contentType, clientRequest);
|
||||
return;
|
||||
}
|
||||
}
|
||||
String message = "No HttpMessageConverter for " + bodyClass.getName();
|
||||
if (contentType != null) {
|
||||
message += " and content type \"" + contentType + "\"";
|
||||
}
|
||||
throw new RestClientException(message);
|
||||
}
|
||||
|
||||
private void logBody(Object body, @Nullable MediaType mediaType, HttpMessageConverter<?> converter) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
StringBuilder msg = new StringBuilder("Writing [");
|
||||
msg.append(body);
|
||||
msg.append("] ");
|
||||
if (mediaType != null) {
|
||||
msg.append("as \"");
|
||||
msg.append(mediaType);
|
||||
msg.append("\" ");
|
||||
}
|
||||
msg.append("with ");
|
||||
msg.append(converter.getClass().getName());
|
||||
logger.debug(msg.toString());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public ResponseSpec retrieve() {
|
||||
return exchangeInternal(DefaultResponseSpec::new, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T exchange(ExchangeFunction<T> exchangeFunction) {
|
||||
return exchangeInternal(exchangeFunction, true);
|
||||
}
|
||||
|
||||
private <T> T exchangeInternal(ExchangeFunction<T> exchangeFunction, boolean close) {
|
||||
Assert.notNull(exchangeFunction, "ExchangeFunction must not be null");
|
||||
|
||||
ClientHttpResponse clientResponse = null;
|
||||
URI uri = null;
|
||||
try {
|
||||
uri = initUri();
|
||||
HttpHeaders headers = initHeaders();
|
||||
ClientHttpRequest clientRequest = createRequest(uri);
|
||||
clientRequest.getHeaders().addAll(headers);
|
||||
if (this.body != null) {
|
||||
this.body.writeTo(clientRequest);
|
||||
}
|
||||
if (this.httpRequestConsumer != null) {
|
||||
this.httpRequestConsumer.accept(clientRequest);
|
||||
}
|
||||
clientResponse = clientRequest.execute();
|
||||
return exchangeFunction.exchange(clientRequest, clientResponse);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw createResourceAccessException(uri, this.httpMethod, ex);
|
||||
}
|
||||
finally {
|
||||
if (close && clientResponse != null) {
|
||||
clientResponse.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private URI initUri() {
|
||||
return (this.uri != null ? this.uri : DefaultRestClient.this.uriBuilderFactory.expand(""));
|
||||
}
|
||||
|
||||
private HttpHeaders initHeaders() {
|
||||
HttpHeaders defaultHeaders = DefaultRestClient.this.defaultHeaders;
|
||||
if (CollectionUtils.isEmpty(this.headers)) {
|
||||
return (defaultHeaders != null ? defaultHeaders : new HttpHeaders());
|
||||
}
|
||||
else if (CollectionUtils.isEmpty(defaultHeaders)) {
|
||||
return this.headers;
|
||||
}
|
||||
else {
|
||||
HttpHeaders result = new HttpHeaders();
|
||||
result.putAll(defaultHeaders);
|
||||
result.putAll(this.headers);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private ClientHttpRequest createRequest(URI uri) throws IOException {
|
||||
ClientHttpRequestFactory factory;
|
||||
if (DefaultRestClient.this.interceptors != null) {
|
||||
factory = DefaultRestClient.this.interceptingRequestFactory;
|
||||
if (factory == null) {
|
||||
factory = new InterceptingClientHttpRequestFactory(DefaultRestClient.this.clientRequestFactory, DefaultRestClient.this.interceptors);
|
||||
DefaultRestClient.this.interceptingRequestFactory = factory;
|
||||
}
|
||||
}
|
||||
else {
|
||||
factory = DefaultRestClient.this.clientRequestFactory;
|
||||
}
|
||||
ClientHttpRequest request = factory.createRequest(uri, this.httpMethod);
|
||||
if (DefaultRestClient.this.initializers != null) {
|
||||
DefaultRestClient.this.initializers.forEach(initializer -> initializer.initialize(request));
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
private static ResourceAccessException createResourceAccessException(URI url, HttpMethod method, IOException ex) {
|
||||
StringBuilder msg = new StringBuilder("I/O error on ");
|
||||
msg.append(method.name());
|
||||
msg.append(" request for \"");
|
||||
String urlString = url.toString();
|
||||
int idx = urlString.indexOf('?');
|
||||
if (idx != -1) {
|
||||
msg.append(urlString, 0, idx);
|
||||
}
|
||||
else {
|
||||
msg.append(urlString);
|
||||
}
|
||||
msg.append("\": ");
|
||||
msg.append(ex.getMessage());
|
||||
return new ResourceAccessException(msg.toString(), ex);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@FunctionalInterface
|
||||
private interface InternalBody {
|
||||
|
||||
void writeTo(ClientHttpRequest request) throws IOException;
|
||||
}
|
||||
}
|
||||
|
||||
private class DefaultResponseSpec implements ResponseSpec {
|
||||
|
||||
private final HttpRequest clientRequest;
|
||||
|
||||
private final ClientHttpResponse clientResponse;
|
||||
|
||||
private final List<StatusHandler> statusHandlers = new ArrayList<>(1);
|
||||
|
||||
private final int defaultStatusHandlerCount;
|
||||
|
||||
|
||||
DefaultResponseSpec(HttpRequest clientRequest, ClientHttpResponse clientResponse) {
|
||||
this.clientRequest = clientRequest;
|
||||
this.clientResponse = clientResponse;
|
||||
this.statusHandlers.addAll(DefaultRestClient.this.defaultStatusHandlers);
|
||||
this.statusHandlers.add(StatusHandler.defaultHandler(DefaultRestClient.this.messageConverters));
|
||||
this.defaultStatusHandlerCount = this.statusHandlers.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseSpec onStatus(Predicate<HttpStatusCode> statusPredicate, ErrorHandler errorHandler) {
|
||||
Assert.notNull(statusPredicate, "StatusPredicate must not be null");
|
||||
Assert.notNull(errorHandler, "ErrorHandler must not be null");
|
||||
|
||||
return onStatusInternal(StatusHandler.of(statusPredicate, errorHandler));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseSpec onStatus(ResponseErrorHandler errorHandler) {
|
||||
Assert.notNull(errorHandler, "ErrorHandler must not be null");
|
||||
|
||||
return onStatusInternal(StatusHandler.fromErrorHandler(errorHandler));
|
||||
}
|
||||
|
||||
private ResponseSpec onStatusInternal(StatusHandler statusHandler) {
|
||||
Assert.notNull(statusHandler, "StatusHandler must not be null");
|
||||
|
||||
int index = this.statusHandlers.size() - this.defaultStatusHandlerCount; // Default handlers always last
|
||||
this.statusHandlers.add(index, statusHandler);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T body(Class<T> bodyType) {
|
||||
return readWithMessageConverters(bodyType, bodyType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T body(ParameterizedTypeReference<T> bodyType) {
|
||||
Type type = bodyType.getType();
|
||||
Class<T> bodyClass = bodyClass(type);
|
||||
return readWithMessageConverters(type, bodyClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> ResponseEntity<T> toEntity(Class<T> bodyType) {
|
||||
return toEntityInternal(bodyType, bodyType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> ResponseEntity<T> toEntity(ParameterizedTypeReference<T> bodyType) {
|
||||
Type type = bodyType.getType();
|
||||
Class<T> bodyClass = bodyClass(type);
|
||||
return toEntityInternal(type, bodyClass);
|
||||
}
|
||||
|
||||
private <T> ResponseEntity<T> toEntityInternal(Type bodyType, Class<T> bodyClass) {
|
||||
T body = readWithMessageConverters(bodyType, bodyClass);
|
||||
try {
|
||||
return ResponseEntity.status(this.clientResponse.getStatusCode())
|
||||
.headers(this.clientResponse.getHeaders())
|
||||
.body(body);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new ResourceAccessException("Could not retrieve response status code: " + ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Void> toBodilessEntity() {
|
||||
try (this.clientResponse) {
|
||||
applyStatusHandlers(this.clientRequest, this.clientResponse);
|
||||
return ResponseEntity.status(this.clientResponse.getStatusCode())
|
||||
.headers(this.clientResponse.getHeaders())
|
||||
.build();
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new ResourceAccessException("Could not retrieve response status code: " + ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> Class<T> bodyClass(Type type) {
|
||||
if (type instanceof Class<?> clazz) {
|
||||
return (Class<T>) clazz;
|
||||
}
|
||||
if (type instanceof ParameterizedType parameterizedType &&
|
||||
parameterizedType.getRawType() instanceof Class<?> rawType) {
|
||||
return (Class<T>) rawType;
|
||||
}
|
||||
return (Class<T>) Object.class;
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
private <T> T readWithMessageConverters(Type bodyType, Class<T> bodyClass) {
|
||||
MediaType contentType = getContentType();
|
||||
|
||||
try (this.clientResponse) {
|
||||
applyStatusHandlers(this.clientRequest, this.clientResponse);
|
||||
|
||||
for (HttpMessageConverter<?> messageConverter : DefaultRestClient.this.messageConverters) {
|
||||
if (messageConverter instanceof GenericHttpMessageConverter genericHttpMessageConverter) {
|
||||
if (genericHttpMessageConverter.canRead(bodyType, bodyClass, contentType)) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Reading to [" + ResolvableType.forType(bodyType) + "]");
|
||||
}
|
||||
return (T) genericHttpMessageConverter.read(bodyType, bodyClass, this.clientResponse);
|
||||
}
|
||||
}
|
||||
if (messageConverter.canRead(bodyClass, contentType)) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Reading to [" + bodyClass.getName() + "] as \"" + contentType + "\"");
|
||||
}
|
||||
return (T) messageConverter.read((Class)bodyClass, this.clientResponse);
|
||||
}
|
||||
}
|
||||
throw new UnknownContentTypeException(bodyType, contentType,
|
||||
this.clientResponse.getStatusCode(), this.clientResponse.getStatusText(),
|
||||
this.clientResponse.getHeaders(), RestClientUtils.getBody(this.clientResponse));
|
||||
}
|
||||
catch (IOException | HttpMessageNotReadableException ex) {
|
||||
throw new RestClientException("Error while extracting response for type [" +
|
||||
ResolvableType.forType(bodyType) + "] and content type [" + contentType + "]", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private MediaType getContentType() {
|
||||
MediaType contentType = this.clientResponse.getHeaders().getContentType();
|
||||
if (contentType == null) {
|
||||
contentType = MediaType.APPLICATION_OCTET_STREAM;
|
||||
}
|
||||
return contentType;
|
||||
}
|
||||
|
||||
private void applyStatusHandlers(HttpRequest request, ClientHttpResponse response) throws IOException {
|
||||
for (StatusHandler handler : this.statusHandlers) {
|
||||
if (handler.test(response)) {
|
||||
handler.handle(request, response);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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
|
||||
*
|
||||
* https://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;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.http.client.ClientHttpRequestInitializer;
|
||||
import org.springframework.http.client.ClientHttpRequestInterceptor;
|
||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.ResourceHttpMessageConverter;
|
||||
import org.springframework.http.converter.StringHttpMessageConverter;
|
||||
import org.springframework.http.converter.cbor.MappingJackson2CborHttpMessageConverter;
|
||||
import org.springframework.http.converter.json.GsonHttpMessageConverter;
|
||||
import org.springframework.http.converter.json.JsonbHttpMessageConverter;
|
||||
import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
import org.springframework.http.converter.smile.MappingJackson2SmileHttpMessageConverter;
|
||||
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.util.DefaultUriBuilderFactory;
|
||||
import org.springframework.web.util.UriBuilderFactory;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link RestClient.Builder}.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @since 6.1
|
||||
*/
|
||||
final class DefaultRestClientBuilder implements RestClient.Builder {
|
||||
|
||||
private static final boolean httpComponentsClientPresent;
|
||||
|
||||
private static final boolean jackson2Present;
|
||||
|
||||
private static final boolean gsonPresent;
|
||||
|
||||
private static final boolean jsonbPresent;
|
||||
|
||||
private static final boolean kotlinSerializationJsonPresent;
|
||||
|
||||
private static final boolean jackson2SmilePresent;
|
||||
|
||||
private static final boolean jackson2CborPresent;
|
||||
|
||||
|
||||
static {
|
||||
ClassLoader loader = DefaultRestClientBuilder.class.getClassLoader();
|
||||
httpComponentsClientPresent = ClassUtils.isPresent("org.apache.hc.client5.http.classic.HttpClient", loader);
|
||||
jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", loader) &&
|
||||
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", loader);
|
||||
gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", loader);
|
||||
jsonbPresent = ClassUtils.isPresent("jakarta.json.bind.Jsonb", loader);
|
||||
kotlinSerializationJsonPresent = ClassUtils.isPresent("kotlinx.serialization.json.Json", loader);
|
||||
jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", loader);
|
||||
jackson2CborPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", loader);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String baseUrl;
|
||||
|
||||
@Nullable
|
||||
private Map<String, ?> defaultUriVariables;
|
||||
|
||||
@Nullable
|
||||
private UriBuilderFactory uriBuilderFactory;
|
||||
|
||||
@Nullable
|
||||
private HttpHeaders defaultHeaders;
|
||||
|
||||
@Nullable
|
||||
private Consumer<RestClient.RequestHeadersSpec<?>> defaultRequest;
|
||||
|
||||
@Nullable
|
||||
private List<StatusHandler> statusHandlers;
|
||||
|
||||
@Nullable
|
||||
private ClientHttpRequestFactory requestFactory;
|
||||
|
||||
@Nullable
|
||||
private List<HttpMessageConverter<?>> messageConverters;
|
||||
|
||||
@Nullable
|
||||
private List<ClientHttpRequestInterceptor> interceptors;
|
||||
|
||||
@Nullable
|
||||
private List<ClientHttpRequestInitializer> initializers;
|
||||
|
||||
|
||||
public DefaultRestClientBuilder() {
|
||||
}
|
||||
|
||||
public DefaultRestClientBuilder(DefaultRestClientBuilder other) {
|
||||
Assert.notNull(other, "Other must not be null");
|
||||
|
||||
this.baseUrl = other.baseUrl;
|
||||
this.defaultUriVariables = (other.defaultUriVariables != null ?
|
||||
new LinkedHashMap<>(other.defaultUriVariables) : null);
|
||||
this.uriBuilderFactory = other.uriBuilderFactory;
|
||||
|
||||
if (other.defaultHeaders != null) {
|
||||
this.defaultHeaders = new HttpHeaders();
|
||||
this.defaultHeaders.putAll(other.defaultHeaders);
|
||||
}
|
||||
else {
|
||||
this.defaultHeaders = null;
|
||||
}
|
||||
this.defaultRequest = other.defaultRequest;
|
||||
this.statusHandlers = (other.statusHandlers != null ? new ArrayList<>(other.statusHandlers) : null);
|
||||
|
||||
this.requestFactory = other.requestFactory;
|
||||
this.messageConverters = (other.messageConverters != null ?
|
||||
new ArrayList<>(other.messageConverters) : null);
|
||||
|
||||
this.interceptors = (other.interceptors != null) ? new ArrayList<>(other.interceptors) : null;
|
||||
this.initializers = (other.initializers != null) ? new ArrayList<>(other.initializers) : null;
|
||||
}
|
||||
|
||||
public DefaultRestClientBuilder(RestTemplate restTemplate) {
|
||||
Assert.notNull(restTemplate, "RestTemplate must not be null");
|
||||
|
||||
if (restTemplate.getUriTemplateHandler() instanceof UriBuilderFactory builderFactory) {
|
||||
this.uriBuilderFactory = builderFactory;
|
||||
}
|
||||
this.statusHandlers = new ArrayList<>();
|
||||
this.statusHandlers.add(StatusHandler.fromErrorHandler(restTemplate.getErrorHandler()));
|
||||
|
||||
this.requestFactory = restTemplate.getRequestFactory();
|
||||
this.messageConverters = new ArrayList<>(restTemplate.getMessageConverters());
|
||||
|
||||
if (!CollectionUtils.isEmpty(restTemplate.getInterceptors())) {
|
||||
this.interceptors = new ArrayList<>(restTemplate.getInterceptors());
|
||||
}
|
||||
if (!CollectionUtils.isEmpty(restTemplate.getClientHttpRequestInitializers())) {
|
||||
this.initializers = new ArrayList<>(restTemplate.getClientHttpRequestInitializers());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public RestClient.Builder baseUrl(String baseUrl) {
|
||||
this.baseUrl = baseUrl;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestClient.Builder defaultUriVariables(Map<String, ?> defaultUriVariables) {
|
||||
this.defaultUriVariables = defaultUriVariables;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestClient.Builder uriBuilderFactory(UriBuilderFactory uriBuilderFactory) {
|
||||
this.uriBuilderFactory = uriBuilderFactory;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestClient.Builder defaultHeader(String header, String... values) {
|
||||
initHeaders().put(header, Arrays.asList(values));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestClient.Builder defaultHeaders(Consumer<HttpHeaders> headersConsumer) {
|
||||
headersConsumer.accept(initHeaders());
|
||||
return this;
|
||||
}
|
||||
|
||||
private HttpHeaders initHeaders() {
|
||||
if (this.defaultHeaders == null) {
|
||||
this.defaultHeaders = new HttpHeaders();
|
||||
}
|
||||
return this.defaultHeaders;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestClient.Builder defaultRequest(Consumer<RestClient.RequestHeadersSpec<?>> defaultRequest) {
|
||||
this.defaultRequest = this.defaultRequest != null ?
|
||||
this.defaultRequest.andThen(defaultRequest) : defaultRequest;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestClient.Builder defaultStatusHandler(Predicate<HttpStatusCode> statusPredicate, RestClient.ResponseSpec.ErrorHandler errorHandler) {
|
||||
return defaultStatusHandlerInternal(StatusHandler.of(statusPredicate, errorHandler));
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestClient.Builder defaultStatusHandler(ResponseErrorHandler errorHandler) {
|
||||
return defaultStatusHandlerInternal(StatusHandler.fromErrorHandler(errorHandler));
|
||||
}
|
||||
|
||||
private RestClient.Builder defaultStatusHandlerInternal(StatusHandler statusHandler) {
|
||||
if (this.statusHandlers == null) {
|
||||
this.statusHandlers = new ArrayList<>();
|
||||
}
|
||||
this.statusHandlers.add(statusHandler);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestClient.Builder requestInterceptor(ClientHttpRequestInterceptor interceptor) {
|
||||
Assert.notNull(interceptor, "Interceptor must not be null");
|
||||
initInterceptors().add(interceptor);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestClient.Builder requestInterceptors(Consumer<List<ClientHttpRequestInterceptor>> interceptorsConsumer) {
|
||||
interceptorsConsumer.accept(initInterceptors());
|
||||
return this;
|
||||
}
|
||||
|
||||
private List<ClientHttpRequestInterceptor> initInterceptors() {
|
||||
if (this.interceptors == null) {
|
||||
this.interceptors = new ArrayList<>();
|
||||
}
|
||||
return this.interceptors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestClient.Builder requestInitializer(ClientHttpRequestInitializer initializer) {
|
||||
Assert.notNull(initializer, "Initializer must not be null");
|
||||
initInitializers().add(initializer);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestClient.Builder requestInitializers(Consumer<List<ClientHttpRequestInitializer>> initializersConsumer) {
|
||||
initializersConsumer.accept(initInitializers());
|
||||
return this;
|
||||
}
|
||||
|
||||
private List<ClientHttpRequestInitializer> initInitializers() {
|
||||
if (this.initializers == null) {
|
||||
this.initializers = new ArrayList<>();
|
||||
}
|
||||
return this.initializers;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public RestClient.Builder requestFactory(ClientHttpRequestFactory requestFactory) {
|
||||
this.requestFactory = requestFactory;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestClient.Builder messageConverters(Consumer<List<HttpMessageConverter<?>>> configurer) {
|
||||
configurer.accept(initMessageConverters());
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestClient.Builder apply(Consumer<RestClient.Builder> builderConsumer) {
|
||||
builderConsumer.accept(this);
|
||||
return this;
|
||||
}
|
||||
|
||||
private List<HttpMessageConverter<?>> initMessageConverters() {
|
||||
if (this.messageConverters == null) {
|
||||
this.messageConverters = new ArrayList<>();
|
||||
this.messageConverters.add(new ByteArrayHttpMessageConverter());
|
||||
this.messageConverters.add(new StringHttpMessageConverter());
|
||||
this.messageConverters.add(new ResourceHttpMessageConverter(false));
|
||||
this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
|
||||
|
||||
if (kotlinSerializationJsonPresent) {
|
||||
this.messageConverters.add(new KotlinSerializationJsonHttpMessageConverter());
|
||||
}
|
||||
if (jackson2Present) {
|
||||
this.messageConverters.add(new MappingJackson2HttpMessageConverter());
|
||||
}
|
||||
else if (gsonPresent) {
|
||||
this.messageConverters.add(new GsonHttpMessageConverter());
|
||||
}
|
||||
else if (jsonbPresent) {
|
||||
this.messageConverters.add(new JsonbHttpMessageConverter());
|
||||
}
|
||||
if (jackson2SmilePresent) {
|
||||
this.messageConverters.add(new MappingJackson2SmileHttpMessageConverter());
|
||||
}
|
||||
if (jackson2CborPresent) {
|
||||
this.messageConverters.add(new MappingJackson2CborHttpMessageConverter());
|
||||
}
|
||||
}
|
||||
return this.messageConverters;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public RestClient.Builder clone() {
|
||||
return new DefaultRestClientBuilder(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestClient build() {
|
||||
ClientHttpRequestFactory requestFactory = initRequestFactory();
|
||||
UriBuilderFactory uriBuilderFactory = initUriBuilderFactory();
|
||||
HttpHeaders defaultHeaders = copyDefaultHeaders();
|
||||
List<HttpMessageConverter<?>> messageConverters = (this.messageConverters != null ?
|
||||
this.messageConverters : initMessageConverters());
|
||||
return new DefaultRestClient(requestFactory,
|
||||
this.interceptors, this.initializers, uriBuilderFactory,
|
||||
defaultHeaders,
|
||||
this.statusHandlers,
|
||||
messageConverters,
|
||||
new DefaultRestClientBuilder(this)
|
||||
);
|
||||
}
|
||||
|
||||
private ClientHttpRequestFactory initRequestFactory() {
|
||||
if (this.requestFactory != null) {
|
||||
return this.requestFactory;
|
||||
}
|
||||
else if (httpComponentsClientPresent) {
|
||||
return new HttpComponentsClientHttpRequestFactory();
|
||||
}
|
||||
else {
|
||||
return new SimpleClientHttpRequestFactory();
|
||||
}
|
||||
}
|
||||
|
||||
private UriBuilderFactory initUriBuilderFactory() {
|
||||
if (this.uriBuilderFactory != null) {
|
||||
return this.uriBuilderFactory;
|
||||
}
|
||||
DefaultUriBuilderFactory factory = (this.baseUrl != null ?
|
||||
new DefaultUriBuilderFactory(this.baseUrl) : new DefaultUriBuilderFactory());
|
||||
factory.setDefaultUriVariables(this.defaultUriVariables);
|
||||
return factory;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private HttpHeaders copyDefaultHeaders() {
|
||||
if (this.defaultHeaders != null) {
|
||||
HttpHeaders copy = new HttpHeaders();
|
||||
this.defaultHeaders.forEach((key, values) -> copy.put(key, new ArrayList<>(values)));
|
||||
return HttpHeaders.readOnlyHttpHeaders(copy);
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,751 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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
|
||||
*
|
||||
* https://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;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
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.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpRequest;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.StreamingHttpOutputMessage;
|
||||
import org.springframework.http.client.ClientHttpRequest;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.http.client.ClientHttpRequestInitializer;
|
||||
import org.springframework.http.client.ClientHttpRequestInterceptor;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.web.util.DefaultUriBuilderFactory;
|
||||
import org.springframework.web.util.UriBuilder;
|
||||
import org.springframework.web.util.UriBuilderFactory;
|
||||
|
||||
/**
|
||||
* Client to perform HTTP requests, exposing a fluent, synchronous API over
|
||||
* underlying HTTP client libraries such the JDK {@code HttpClient}, Apache
|
||||
* HttpComponents, and others.
|
||||
*
|
||||
* <p>Use static factory methods {@link #create()}, {@link #create(String)},
|
||||
* or {@link RestClient#builder()} to prepare an instance. To use the same
|
||||
* configuration as a {@link RestTemplate}, use {@link #create(RestTemplate)} or
|
||||
* {@link #builder(RestTemplate)}.
|
||||
*
|
||||
* <p>For examples with a response body see:
|
||||
* <ul>
|
||||
* <li>{@link RequestHeadersSpec#retrieve() retrieve()}
|
||||
* <li>{@link RequestHeadersSpec#exchange(RequestHeadersSpec.ExchangeFunction) exchange(Function<ClientHttpRequest, T>)}
|
||||
* </ul>
|
||||
* <p>For examples with a request body see:
|
||||
* <ul>
|
||||
* <li>{@link RequestBodySpec#body(Object) body(Object)}
|
||||
* <li>{@link RequestBodySpec#body(Object, ParameterizedTypeReference) body(Object, ParameterizedTypeReference)}
|
||||
* <li>{@link RequestBodySpec#body(StreamingHttpOutputMessage.Body) body(Consumer<OutputStream>}
|
||||
* </ul>
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @since 6.1
|
||||
*/
|
||||
public interface RestClient {
|
||||
|
||||
/**
|
||||
* Start building an HTTP GET request.
|
||||
* @return a spec for specifying the target URL
|
||||
*/
|
||||
RequestHeadersUriSpec<?> get();
|
||||
|
||||
/**
|
||||
* Start building an HTTP HEAD request.
|
||||
* @return a spec for specifying the target URL
|
||||
*/
|
||||
RequestHeadersUriSpec<?> head();
|
||||
|
||||
/**
|
||||
* Start building an HTTP POST request.
|
||||
* @return a spec for specifying the target URL
|
||||
*/
|
||||
RequestBodyUriSpec post();
|
||||
|
||||
/**
|
||||
* Start building an HTTP PUT request.
|
||||
* @return a spec for specifying the target URL
|
||||
*/
|
||||
RequestBodyUriSpec put();
|
||||
|
||||
/**
|
||||
* Start building an HTTP PATCH request.
|
||||
* @return a spec for specifying the target URL
|
||||
*/
|
||||
RequestBodyUriSpec patch();
|
||||
|
||||
/**
|
||||
* Start building an HTTP DELETE request.
|
||||
* @return a spec for specifying the target URL
|
||||
*/
|
||||
RequestHeadersUriSpec<?> delete();
|
||||
|
||||
/**
|
||||
* Start building an HTTP OPTIONS request.
|
||||
* @return a spec for specifying the target URL
|
||||
*/
|
||||
RequestHeadersUriSpec<?> options();
|
||||
|
||||
/**
|
||||
* Start building a request for the given {@code HttpMethod}.
|
||||
* @return a spec for specifying the target URL
|
||||
*/
|
||||
RequestBodyUriSpec method(HttpMethod method);
|
||||
|
||||
|
||||
/**
|
||||
* Return a builder to create a new {@code RestClient} whose settings are
|
||||
* replicated from the current {@code RestClient}.
|
||||
*/
|
||||
Builder mutate();
|
||||
|
||||
|
||||
// Static, factory methods
|
||||
|
||||
/**
|
||||
* Create a new {@code RestClient}.
|
||||
* @see #create(String)
|
||||
* @see #builder()
|
||||
*/
|
||||
static RestClient create() {
|
||||
return new DefaultRestClientBuilder().build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Variant of {@link #create()} that accepts a default base URL. For more
|
||||
* details see {@link Builder#baseUrl(String) Builder.baseUrl(String)}.
|
||||
* @param baseUrl the base URI for all requests
|
||||
* @see #builder()
|
||||
*/
|
||||
static RestClient create(String baseUrl) {
|
||||
return new DefaultRestClientBuilder().baseUrl(baseUrl).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@code RestClient} based on the configuration of the
|
||||
* given {@code RestTemplate}. The returned builder is configured with the
|
||||
* template's
|
||||
* <ul>
|
||||
* <li>{@link RestTemplate#getRequestFactory() ClientHttpRequestFactory},</li>
|
||||
* <li>{@link RestTemplate#getMessageConverters() HttpMessageConverters},</li>
|
||||
* <li>{@link RestTemplate#getInterceptors() ClientHttpRequestInterceptors},</li>
|
||||
* <li>{@link RestTemplate#getClientHttpRequestInitializers() ClientHttpRequestInitializers},</li>
|
||||
* <li>{@link RestTemplate#getUriTemplateHandler() UriBuilderFactory}, and</li>
|
||||
* <li>{@linkplain RestTemplate#getErrorHandler() error handler}.</li>
|
||||
* </ul>
|
||||
* @param restTemplate the rest template to base the returned client's
|
||||
* configuration on
|
||||
* @return a {@code RestClient} initialized with the {@code restTemplate}'s
|
||||
* configuration
|
||||
*/
|
||||
static RestClient create(RestTemplate restTemplate) {
|
||||
return new DefaultRestClientBuilder(restTemplate).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain a {@code RestClient} builder.
|
||||
*/
|
||||
static RestClient.Builder builder() {
|
||||
return new DefaultRestClientBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain a {@code RestClient} builder based on the configuration of the
|
||||
* given {@code RestTemplate}. The returned builder is configured with the
|
||||
* template's
|
||||
* <ul>
|
||||
* <li>{@link RestTemplate#getRequestFactory() ClientHttpRequestFactory},</li>
|
||||
* <li>{@link RestTemplate#getMessageConverters() HttpMessageConverters},</li>
|
||||
* <li>{@link RestTemplate#getInterceptors() ClientHttpRequestInterceptors},</li>
|
||||
* <li>{@link RestTemplate#getClientHttpRequestInitializers() ClientHttpRequestInitializers},</li>
|
||||
* <li>{@link RestTemplate#getUriTemplateHandler() UriBuilderFactory}, and</li>
|
||||
* <li>{@linkplain RestTemplate#getErrorHandler() error handler}.</li>
|
||||
* </ul>
|
||||
* @param restTemplate the rest template to base the returned builder's
|
||||
* configuration on
|
||||
* @return a {@code RestClient} builder initialized with {@code restTemplate}'s
|
||||
* configuration
|
||||
*/
|
||||
static RestClient.Builder builder(RestTemplate restTemplate) {
|
||||
return new DefaultRestClientBuilder(restTemplate);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A mutable builder for creating a {@link RestClient}.
|
||||
*/
|
||||
interface Builder {
|
||||
|
||||
/**
|
||||
* Configure a base URL for requests. Effectively a shortcut for:
|
||||
* <p>
|
||||
* <pre class="code">
|
||||
* String baseUrl = "https://abc.go.com/v1";
|
||||
* DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
|
||||
* RestClient client = RestClient.builder().uriBuilderFactory(factory).build();
|
||||
* </pre>
|
||||
* <p>The {@code DefaultUriBuilderFactory} is used to prepare the URL
|
||||
* for every request with the given base URL, unless the URL request
|
||||
* for a given URL is absolute in which case the base URL is ignored.
|
||||
* <p><strong>Note:</strong> this method is mutually exclusive with
|
||||
* {@link #uriBuilderFactory(UriBuilderFactory)}. If both are used, the
|
||||
* baseUrl value provided here will be ignored.
|
||||
* @see DefaultUriBuilderFactory#DefaultUriBuilderFactory(String)
|
||||
* @see #uriBuilderFactory(UriBuilderFactory)
|
||||
*/
|
||||
Builder baseUrl(String baseUrl);
|
||||
|
||||
/**
|
||||
* Configure default URL variable values to use when expanding URI
|
||||
* templates with a {@link Map}. Effectively a shortcut for:
|
||||
* <p>
|
||||
* <pre class="code">
|
||||
* Map<String, ?> defaultVars = ...;
|
||||
* DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory();
|
||||
* factory.setDefaultVariables(defaultVars);
|
||||
* RestClient client = RestClient.builder().uriBuilderFactory(factory).build();
|
||||
* </pre>
|
||||
* <p><strong>Note:</strong> this method is mutually exclusive with
|
||||
* {@link #uriBuilderFactory(UriBuilderFactory)}. If both are used, the
|
||||
* defaultUriVariables value provided here will be ignored.
|
||||
* @see DefaultUriBuilderFactory#setDefaultUriVariables(Map)
|
||||
* @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 shortcut
|
||||
* properties:
|
||||
* <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);
|
||||
|
||||
/**
|
||||
* Global option to specify a header to be added to every request,
|
||||
* if the request does not already contain such a header.
|
||||
* @param header the header name
|
||||
* @param values the header values
|
||||
*/
|
||||
Builder defaultHeader(String header, String... values);
|
||||
|
||||
/**
|
||||
* Provides access to every {@link #defaultHeader(String, String...)}
|
||||
* declared so far with the possibility to add, replace, or remove.
|
||||
* @param headersConsumer the consumer
|
||||
*/
|
||||
Builder defaultHeaders(Consumer<HttpHeaders> headersConsumer);
|
||||
|
||||
/**
|
||||
* Provide a consumer to customize every request being built.
|
||||
* @param defaultRequest the consumer to use for modifying requests
|
||||
*/
|
||||
Builder defaultRequest(Consumer<RequestHeadersSpec<?>> defaultRequest);
|
||||
|
||||
/**
|
||||
* Register a default
|
||||
* {@linkplain ResponseSpec#onStatus(Predicate, ResponseSpec.ErrorHandler) status handler}
|
||||
* to apply to every response. Such default handlers are applied in the
|
||||
* order in which they are registered, and after any others that are
|
||||
* registered for a specific response.
|
||||
* @param statusPredicate to match responses with
|
||||
* @param errorHandler handler that typically, though not necessarily,
|
||||
* throws an exception
|
||||
* @return this builder
|
||||
*/
|
||||
Builder defaultStatusHandler(Predicate<HttpStatusCode> statusPredicate,
|
||||
ResponseSpec.ErrorHandler errorHandler);
|
||||
|
||||
/**
|
||||
* Register a default
|
||||
* {@linkplain ResponseSpec#onStatus(ResponseErrorHandler) status handler}
|
||||
* to apply to every response. Such default handlers are applied in the
|
||||
* order in which they are registered, and after any others that are
|
||||
* registered for a specific response.
|
||||
* @param errorHandler handler that typically, though not necessarily,
|
||||
* throws an exception
|
||||
* @return this builder
|
||||
*/
|
||||
Builder defaultStatusHandler(ResponseErrorHandler errorHandler);
|
||||
|
||||
/**
|
||||
* Add the given request interceptor to the end of the interceptor chain.
|
||||
* @param interceptor the interceptor to be added to the chain
|
||||
*/
|
||||
Builder requestInterceptor(ClientHttpRequestInterceptor interceptor);
|
||||
|
||||
/**
|
||||
* Manipulate the interceptors with the given consumer. The list provided to
|
||||
* the consumer is "live", so that the consumer can be used to remove
|
||||
* interceptors, change ordering, etc.
|
||||
* @param interceptorsConsumer a function that consumes the interceptors list
|
||||
* @return this builder
|
||||
*/
|
||||
Builder requestInterceptors(Consumer<List<ClientHttpRequestInterceptor>> interceptorsConsumer);
|
||||
|
||||
/**
|
||||
* Add the given request initializer to the end of the initializer chain.
|
||||
* @param initializer the initializer to be added to the chain
|
||||
*/
|
||||
Builder requestInitializer(ClientHttpRequestInitializer initializer);
|
||||
|
||||
/**
|
||||
* Manipulate the initializers with the given consumer. The list provided to
|
||||
* the consumer is "live", so that the consumer can be used to remove
|
||||
* initializers, change ordering, etc.
|
||||
* @param initializersConsumer a function that consumes the initializers list
|
||||
* @return this builder
|
||||
*/
|
||||
Builder requestInitializers(Consumer<List<ClientHttpRequestInitializer>> initializersConsumer);
|
||||
|
||||
/**
|
||||
* Configure the {@link ClientHttpRequestFactory} to use. This is useful
|
||||
* for plugging in and/or customizing options of the underlying HTTP
|
||||
* client library (e.g. SSL).
|
||||
* @param requestFactory the request factory to use
|
||||
*/
|
||||
Builder requestFactory(ClientHttpRequestFactory requestFactory);
|
||||
|
||||
/**
|
||||
* Configure the message converters for the {@code RestClient} to use.
|
||||
* @param configurer the configurer to apply
|
||||
*/
|
||||
Builder messageConverters(Consumer<List<HttpMessageConverter<?>>> configurer);
|
||||
|
||||
/**
|
||||
* Apply the given {@code Consumer} to this builder instance.
|
||||
* <p>This can be useful for applying pre-packaged customizations.
|
||||
* @param builderConsumer the consumer to apply
|
||||
*/
|
||||
Builder apply(Consumer<Builder> builderConsumer);
|
||||
|
||||
/**
|
||||
* Clone this {@code RestClient.Builder}.
|
||||
*/
|
||||
Builder clone();
|
||||
|
||||
/**
|
||||
* Build the {@link RestClient} instance.
|
||||
*/
|
||||
RestClient build();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Contract for specifying the URI for a request.
|
||||
* @param <S> a self reference to the spec type
|
||||
*/
|
||||
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);
|
||||
|
||||
/**
|
||||
* Specify the URI starting with a URI template and finishing off with a
|
||||
* {@link UriBuilder} created from the template.
|
||||
*/
|
||||
S uri(String uri, Function<UriBuilder, URI> uriFunction);
|
||||
|
||||
/**
|
||||
* Specify the URI by through a {@link UriBuilder}.
|
||||
* @see #uri(String, Function)
|
||||
*/
|
||||
S uri(Function<UriBuilder, URI> uriFunction);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Contract for specifying request headers leading up to the exchange.
|
||||
* @param <S> a self reference to the spec type
|
||||
*/
|
||||
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);
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
/**
|
||||
* Provides access to every header declared so far with the possibility
|
||||
* to add, replace, or remove values.
|
||||
* @param headersConsumer the consumer to provide access to
|
||||
* @return this builder
|
||||
*/
|
||||
S headers(Consumer<HttpHeaders> headersConsumer);
|
||||
|
||||
/**
|
||||
* Set the attribute with the given name to the given value.
|
||||
* @param name the name of the attribute to add
|
||||
* @param value the value of the attribute to add
|
||||
* @return this builder
|
||||
*/
|
||||
S attribute(String name, Object value);
|
||||
|
||||
/**
|
||||
* Provides access to every attribute declared so far with the
|
||||
* possibility to add, replace, or remove values.
|
||||
* @param attributesConsumer the consumer to provide access to
|
||||
* @return this builder
|
||||
*/
|
||||
S attributes(Consumer<Map<String, Object>> attributesConsumer);
|
||||
|
||||
/**
|
||||
* Callback for access to the {@link ClientHttpRequest} that in turn
|
||||
* provides access to the native request of the underlying HTTP library.
|
||||
* This could be useful for setting advanced, per-request options that
|
||||
* exposed by the underlying library.
|
||||
* @param requestConsumer a consumer to access the
|
||||
* {@code ClientHttpRequest} with
|
||||
* @return {@code ResponseSpec} to specify how to decode the body
|
||||
*/
|
||||
S httpRequest(Consumer<ClientHttpRequest> requestConsumer);
|
||||
|
||||
/**
|
||||
* Proceed to declare how to extract the response. For example to extract
|
||||
* a {@link ResponseEntity} with status, headers, and body:
|
||||
* <p><pre>
|
||||
* ResponseEntity<Person> entity = client.get()
|
||||
* .uri("/persons/1")
|
||||
* .accept(MediaType.APPLICATION_JSON)
|
||||
* .retrieve()
|
||||
* .toEntity(Person.class);
|
||||
* </pre>
|
||||
* <p>Or if interested only in the body:
|
||||
* <p><pre>
|
||||
* Person person = client.get()
|
||||
* .uri("/persons/1")
|
||||
* .accept(MediaType.APPLICATION_JSON)
|
||||
* .retrieve()
|
||||
* .body(Person.class);
|
||||
* </pre>
|
||||
* <p>By default, 4xx response code result in a
|
||||
* {@link HttpClientErrorException} and 5xx response codes in a
|
||||
* {@link HttpServerErrorException}. To customize error handling, use
|
||||
* {@link ResponseSpec#onStatus(Predicate, ResponseSpec.ErrorHandler) onStatus} handlers.
|
||||
*/
|
||||
ResponseSpec retrieve();
|
||||
|
||||
/**
|
||||
* Exchange the {@link ClientHttpResponse} for a type {@code T}. This
|
||||
* can be useful for advanced scenarios, for example to decode the
|
||||
* response differently depending on the response status:
|
||||
* <p><pre>
|
||||
* Person person = client.get()
|
||||
* .uri("/people/1")
|
||||
* .accept(MediaType.APPLICATION_JSON)
|
||||
* .exchange((request, response) -> {
|
||||
* if (response.getStatusCode().equals(HttpStatus.OK)) {
|
||||
* return deserialize(response.getBody());
|
||||
* }
|
||||
* else {
|
||||
* throw new BusinessException();
|
||||
* }
|
||||
* });
|
||||
* </pre>
|
||||
* <p><strong>Note:</strong> The response is
|
||||
* {@linkplain ClientHttpResponse#close() closed} after the exchange
|
||||
* function has been invoked.
|
||||
* @param exchangeFunction the function to handle the response with
|
||||
* @param <T> the type the response will be transformed to
|
||||
* @return the value returned from the exchange function
|
||||
*/
|
||||
<T> T exchange(ExchangeFunction<T> exchangeFunction);
|
||||
|
||||
|
||||
/**
|
||||
* Defines the contract for {@link #exchange(ExchangeFunction)}.
|
||||
* @param <T> the type the response will be transformed to
|
||||
*/
|
||||
@FunctionalInterface
|
||||
interface ExchangeFunction<T> {
|
||||
|
||||
/**
|
||||
* Exchange the given response into a type {@code T}.
|
||||
* @param clientRequest the request
|
||||
* @param clientResponse the response
|
||||
* @return the exchanged type
|
||||
* @throws IOException in case of I/O errors
|
||||
*/
|
||||
T exchange(HttpRequest clientRequest, ClientHttpResponse clientResponse) throws IOException;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Contract for specifying request headers and body leading up to the exchange.
|
||||
*/
|
||||
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 Object}.
|
||||
* For example:
|
||||
* <p><pre class="code">
|
||||
* Person person = ... ;
|
||||
* ResponseEntity<Void> response = client.post()
|
||||
* .uri("/persons/{id}", id)
|
||||
* .contentType(MediaType.APPLICATION_JSON)
|
||||
* .body(person)
|
||||
* .retrieve()
|
||||
* .toBodilessEntity();
|
||||
* </pre>
|
||||
* @param body the body of the response
|
||||
* @return the built response
|
||||
*/
|
||||
RequestBodySpec body(Object body);
|
||||
|
||||
/**
|
||||
* Set the body of the response to the given {@code Object}. The parameter
|
||||
* {@code bodyType} is used to capture the generic type.
|
||||
* @param body the body of the response
|
||||
* @param bodyType the type of the body, used to capture the generic type
|
||||
* @return the built response
|
||||
*/
|
||||
<T> RequestBodySpec body(T body, ParameterizedTypeReference<T> bodyType);
|
||||
|
||||
/**
|
||||
* Set the body of the response to the given function that writes to
|
||||
* an {@link OutputStream}.
|
||||
* @param body a function that takes an {@code OutputStream} and can
|
||||
* throw an {@code IOException}
|
||||
* @return the built response
|
||||
*/
|
||||
RequestBodySpec body(StreamingHttpOutputMessage.Body body);
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Contract for specifying response operations following the exchange.
|
||||
*/
|
||||
interface ResponseSpec {
|
||||
|
||||
/**
|
||||
* Provide a function to map specific error status codes to an error
|
||||
* handler.
|
||||
* <p>By default, if there are no matching status handlers, responses
|
||||
* with status codes >= 400 wil throw a
|
||||
* {@link RestClientResponseException}.
|
||||
* @param statusPredicate to match responses with
|
||||
* @param errorHandler handler that typically, though not necessarily,
|
||||
* throws an exception
|
||||
* @return this builder
|
||||
*/
|
||||
ResponseSpec onStatus(Predicate<HttpStatusCode> statusPredicate,
|
||||
ErrorHandler errorHandler);
|
||||
|
||||
/**
|
||||
* Provide a function to map specific error status codes to an error
|
||||
* handler.
|
||||
* <p>By default, if there are no matching status handlers, responses
|
||||
* with status codes >= 400 wil throw a
|
||||
* {@link RestClientResponseException}.
|
||||
* @param errorHandler the error handler
|
||||
* @return this builder
|
||||
*/
|
||||
ResponseSpec onStatus(ResponseErrorHandler errorHandler);
|
||||
|
||||
/**
|
||||
* Extract the body as an object of the given type.
|
||||
* @param bodyType the type of return value
|
||||
* @param <T> the body type
|
||||
* @return the body, or {@code null} if no response body was available
|
||||
* @throws RestClientResponseException by default when receiving a
|
||||
* response with a status code of 4xx or 5xx. Use
|
||||
* {@link #onStatus(Predicate, ErrorHandler)} to customize error response
|
||||
* handling.
|
||||
*/
|
||||
@Nullable
|
||||
<T> T body(Class<T> bodyType);
|
||||
|
||||
/**
|
||||
* Extract the body as an object of the given type.
|
||||
* @param bodyType the type of return value
|
||||
* @param <T> the body type
|
||||
* @return the body, or {@code null} if no response body was available
|
||||
* @throws RestClientResponseException by default when receiving a
|
||||
* response with a status code of 4xx or 5xx. Use
|
||||
* {@link #onStatus(Predicate, ErrorHandler)} to customize error response
|
||||
* handling.
|
||||
*/
|
||||
@Nullable
|
||||
<T> T body(ParameterizedTypeReference<T> bodyType);
|
||||
|
||||
/**
|
||||
* Return a {@code ResponseEntity} with the body decoded to an Object of
|
||||
* the given type.
|
||||
* @param bodyType the expected response body type
|
||||
* @param <T> response body type
|
||||
* @return the {@code ResponseEntity} with the decoded body
|
||||
* @throws RestClientResponseException by default when receiving a
|
||||
* response with a status code of 4xx or 5xx. Use
|
||||
* {@link #onStatus(Predicate, ErrorHandler)} to customize error response
|
||||
* handling.
|
||||
*/
|
||||
<T> ResponseEntity<T> toEntity(Class<T> bodyType);
|
||||
|
||||
/**
|
||||
* Return a {@code ResponseEntity} with the body decoded to an Object of
|
||||
* the given type.
|
||||
* @param bodyType the expected response body type
|
||||
* @param <T> response body type
|
||||
* @return the {@code ResponseEntity} with the decoded body
|
||||
* @throws RestClientResponseException by default when receiving a
|
||||
* response with a status code of 4xx or 5xx. Use
|
||||
* {@link #onStatus(Predicate, ErrorHandler)} to customize error response
|
||||
* handling.
|
||||
*/
|
||||
<T> ResponseEntity<T> toEntity(ParameterizedTypeReference<T> bodyType);
|
||||
|
||||
/**
|
||||
* Return a {@code ResponseEntity} without a body.
|
||||
* @return the {@code ResponseEntity}
|
||||
* @throws RestClientResponseException by default when receiving a
|
||||
* response with a status code of 4xx or 5xx. Use
|
||||
* {@link #onStatus(Predicate, ErrorHandler)} to customize error response
|
||||
* handling.
|
||||
*/
|
||||
ResponseEntity<Void> toBodilessEntity();
|
||||
|
||||
|
||||
/**
|
||||
* Used in {@link #onStatus(Predicate, ErrorHandler)}.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
interface ErrorHandler {
|
||||
|
||||
/**
|
||||
* Handle the error in the given response.
|
||||
* @param response the response with the error
|
||||
* @throws IOException in case of I/O errors
|
||||
*/
|
||||
void handle(HttpRequest request, ClientHttpResponse response) throws IOException;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Contract for specifying request headers and URI for a request.
|
||||
* @param <S> a self reference to the spec type
|
||||
*/
|
||||
interface RequestHeadersUriSpec<S extends RequestHeadersSpec<S>> extends UriSpec<S>, RequestHeadersSpec<S> {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Contract for specifying request headers, body and URI for a request.
|
||||
*/
|
||||
interface RequestBodyUriSpec extends RequestBodySpec, RequestHeadersUriSpec<RequestBodySpec> {
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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
|
||||
*
|
||||
* https://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;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpInputMessage;
|
||||
import org.springframework.http.HttpMessage;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
|
||||
/**
|
||||
* Internal methods shared between types in this package.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @since 6.1
|
||||
*/
|
||||
abstract class RestClientUtils {
|
||||
|
||||
public static byte[] getBody(HttpInputMessage message) {
|
||||
try {
|
||||
return FileCopyUtils.copyToByteArray(message.getBody());
|
||||
}
|
||||
catch (IOException ignore) {
|
||||
}
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Charset getCharset(HttpMessage response) {
|
||||
HttpHeaders headers = response.getHeaders();
|
||||
MediaType contentType = headers.getContentType();
|
||||
return (contentType != null ? contentType.getCharset() : null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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
|
||||
*
|
||||
* https://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;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.log.LogFormatUtils;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpRequest;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* Used by {@link DefaultRestClient} and {@link DefaultRestClientBuilder}.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @since 6.1
|
||||
*/
|
||||
final class StatusHandler {
|
||||
|
||||
private final ResponsePredicate predicate;
|
||||
|
||||
private final RestClient.ResponseSpec.ErrorHandler errorHandler;
|
||||
|
||||
|
||||
private StatusHandler(ResponsePredicate predicate, RestClient.ResponseSpec.ErrorHandler errorHandler) {
|
||||
this.predicate = predicate;
|
||||
this.errorHandler = errorHandler;
|
||||
}
|
||||
|
||||
|
||||
public static StatusHandler of(Predicate<HttpStatusCode> predicate,
|
||||
RestClient.ResponseSpec.ErrorHandler errorHandler) {
|
||||
Assert.notNull(predicate, "Predicate must not be null");
|
||||
Assert.notNull(errorHandler, "ErrorHandler must not be null");
|
||||
|
||||
return new StatusHandler(response -> predicate.test(response.getStatusCode()), errorHandler);
|
||||
}
|
||||
|
||||
public static StatusHandler fromErrorHandler(ResponseErrorHandler errorHandler) {
|
||||
Assert.notNull(errorHandler, "ErrorHandler must not be null");
|
||||
|
||||
return new StatusHandler(errorHandler::hasError, (request, response) ->
|
||||
errorHandler.handleError(request.getURI(), request.getMethod(), response));
|
||||
}
|
||||
|
||||
public static StatusHandler defaultHandler(List<HttpMessageConverter<?>> messageConverters) {
|
||||
return new StatusHandler(response -> response.getStatusCode().isError(),
|
||||
(request, response) -> {
|
||||
HttpStatusCode statusCode = response.getStatusCode();
|
||||
String statusText = response.getStatusText();
|
||||
HttpHeaders headers = response.getHeaders();
|
||||
byte[] body = RestClientUtils.getBody(response);
|
||||
Charset charset = RestClientUtils.getCharset(response);
|
||||
String message = getErrorMessage(statusCode.value(), statusText, body, charset);
|
||||
RestClientResponseException ex;
|
||||
|
||||
if (statusCode.is4xxClientError()) {
|
||||
ex = HttpClientErrorException.create(message, statusCode, statusText, headers, body, charset);
|
||||
}
|
||||
else if (statusCode.is5xxServerError()) {
|
||||
ex = HttpServerErrorException.create(message, statusCode, statusText, headers, body, charset);
|
||||
}
|
||||
else {
|
||||
ex = new UnknownHttpStatusCodeException(message, statusCode.value(), statusText, headers, body, charset);
|
||||
}
|
||||
if (!CollectionUtils.isEmpty(messageConverters)) {
|
||||
ex.setBodyConvertFunction(initBodyConvertFunction(response, body, messageConverters));
|
||||
}
|
||||
throw ex;
|
||||
});
|
||||
}
|
||||
|
||||
private static Function<ResolvableType, ?> initBodyConvertFunction(ClientHttpResponse response, byte[] body, List<HttpMessageConverter<?>> messageConverters) {
|
||||
Assert.state(!CollectionUtils.isEmpty(messageConverters), "Expected message converters");
|
||||
return resolvableType -> {
|
||||
try {
|
||||
HttpMessageConverterExtractor<?> extractor =
|
||||
new HttpMessageConverterExtractor<>(resolvableType.getType(), messageConverters);
|
||||
|
||||
return extractor.extractData(new ClientHttpResponseDecorator(response) {
|
||||
@Override
|
||||
public InputStream getBody() {
|
||||
return new ByteArrayInputStream(body);
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new RestClientException("Error while extracting response for type [" + resolvableType + "]", ex);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private static String getErrorMessage(int rawStatusCode, String statusText, @Nullable byte[] responseBody,
|
||||
@Nullable Charset charset) {
|
||||
|
||||
String preface = rawStatusCode + " " + statusText + ": ";
|
||||
|
||||
if (ObjectUtils.isEmpty(responseBody)) {
|
||||
return preface + "[no body]";
|
||||
}
|
||||
|
||||
charset = (charset != null ? charset : StandardCharsets.UTF_8);
|
||||
|
||||
String bodyText = new String(responseBody, charset);
|
||||
bodyText = LogFormatUtils.formatValue(bodyText, -1, true);
|
||||
|
||||
return preface + bodyText;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public boolean test(ClientHttpResponse response) throws IOException {
|
||||
return this.predicate.test(response);
|
||||
}
|
||||
|
||||
public void handle(HttpRequest request, ClientHttpResponse response) throws IOException {
|
||||
this.errorHandler.handle(request, response);
|
||||
}
|
||||
|
||||
|
||||
@FunctionalInterface
|
||||
private interface ResponsePredicate {
|
||||
|
||||
boolean test(ClientHttpResponse response) throws IOException;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Core package of the client-side web support.
|
||||
* Provides a RestTemplate class and various callback interfaces.
|
||||
* Provides the RestTemplate and RestClient.
|
||||
*/
|
||||
@NonNullApi
|
||||
@NonNullFields
|
||||
|
||||
Reference in New Issue
Block a user