org.springframework.web.client.DefaultRestClient Maven / Gradle / Ivy
/*
* Copyright 2002-2024 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.InputStream;
import java.io.UncheckedIOException;
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.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
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.client.observation.ClientHttpObservationDocumentation;
import org.springframework.http.client.observation.ClientRequestObservationContext;
import org.springframework.http.client.observation.ClientRequestObservationConvention;
import org.springframework.http.client.observation.DefaultClientRequestObservationConvention;
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;
/**
* The default implementation of {@link RestClient},
* as created by the static factory methods.
*
* @author Arjen Poutsma
* @author Sebastien Deleuze
* @since 6.1
* @see RestClient#create()
* @see RestClient#create(String)
* @see RestClient#create(RestTemplate)
*/
final class DefaultRestClient implements RestClient {
private static final Log logger = LogFactory.getLog(DefaultRestClient.class);
private static final ClientRequestObservationConvention DEFAULT_OBSERVATION_CONVENTION = new DefaultClientRequestObservationConvention();
private final ClientHttpRequestFactory clientRequestFactory;
@Nullable
private volatile ClientHttpRequestFactory interceptingRequestFactory;
@Nullable
private final List initializers;
@Nullable
private final List interceptors;
private final UriBuilderFactory uriBuilderFactory;
@Nullable
private final HttpHeaders defaultHeaders;
@Nullable
private final Consumer> defaultRequest;
private final List defaultStatusHandlers;
private final DefaultRestClientBuilder builder;
private final List> messageConverters;
private final ObservationRegistry observationRegistry;
@Nullable
private final ClientRequestObservationConvention observationConvention;
DefaultRestClient(ClientHttpRequestFactory clientRequestFactory,
@Nullable List interceptors,
@Nullable List initializers,
UriBuilderFactory uriBuilderFactory,
@Nullable HttpHeaders defaultHeaders,
@Nullable Consumer> defaultRequest,
@Nullable List statusHandlers,
List> messageConverters,
ObservationRegistry observationRegistry,
@Nullable ClientRequestObservationConvention observationConvention,
DefaultRestClientBuilder builder) {
this.clientRequestFactory = clientRequestFactory;
this.initializers = initializers;
this.interceptors = interceptors;
this.uriBuilderFactory = uriBuilderFactory;
this.defaultHeaders = defaultHeaders;
this.defaultRequest = defaultRequest;
this.defaultStatusHandlers = (statusHandlers != null ? new ArrayList<>(statusHandlers) : new ArrayList<>());
this.messageConverters = messageConverters;
this.observationRegistry = observationRegistry;
this.observationConvention = observationConvention;
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, "HttpMethod 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);
}
@Nullable
@SuppressWarnings({"rawtypes", "unchecked"})
private T readWithMessageConverters(ClientHttpResponse clientResponse, Runnable callback, Type bodyType,
Class bodyClass, @Nullable Observation observation) {
MediaType contentType = getContentType(clientResponse);
try {
callback.run();
IntrospectingClientHttpResponse responseWrapper = new IntrospectingClientHttpResponse(clientResponse);
if (!responseWrapper.hasMessageBody() || responseWrapper.hasEmptyMessageBody()) {
return null;
}
for (HttpMessageConverter> messageConverter : this.messageConverters) {
if (messageConverter instanceof GenericHttpMessageConverter genericHttpMessageConverter) {
if (genericHttpMessageConverter.canRead(bodyType, null, contentType)) {
if (logger.isDebugEnabled()) {
logger.debug("Reading to [" + ResolvableType.forType(bodyType) + "]");
}
return (T) genericHttpMessageConverter.read(bodyType, null, responseWrapper);
}
}
if (messageConverter.canRead(bodyClass, contentType)) {
if (logger.isDebugEnabled()) {
logger.debug("Reading to [" + bodyClass.getName() + "] as \"" + contentType + "\"");
}
return (T) messageConverter.read((Class)bodyClass, responseWrapper);
}
}
throw new UnknownContentTypeException(bodyType, contentType,
responseWrapper.getStatusCode(), responseWrapper.getStatusText(),
responseWrapper.getHeaders(), RestClientUtils.getBody(responseWrapper));
}
catch (UncheckedIOException | IOException | HttpMessageNotReadableException exc) {
Throwable cause;
if (exc instanceof UncheckedIOException uncheckedIOException) {
cause = uncheckedIOException.getCause();
}
else {
cause = exc;
}
RestClientException restClientException = new RestClientException("Error while extracting response for type [" +
ResolvableType.forType(bodyType) + "] and content type [" + contentType + "]", cause);
if (observation != null) {
observation.error(restClientException);
}
throw restClientException;
}
catch (RestClientException restClientException) {
if (observation != null) {
observation.error(restClientException);
}
throw restClientException;
}
finally {
clientResponse.close();
}
}
private static MediaType getContentType(ClientHttpResponse clientResponse) {
MediaType contentType = clientResponse.getHeaders().getContentType();
if (contentType == null) {
contentType = MediaType.APPLICATION_OCTET_STREAM;
}
return contentType;
}
@SuppressWarnings("unchecked")
private static Class bodyClass(Type type) {
if (type instanceof Class> clazz) {
return (Class) clazz;
}
if (type instanceof ParameterizedType parameterizedType &&
parameterizedType.getRawType() instanceof Class> rawType) {
return (Class) rawType;
}
return (Class) Object.class;
}
private class DefaultRequestBodyUriSpec implements RequestBodyUriSpec {
private final HttpMethod httpMethod;
@Nullable
private URI uri;
@Nullable
private HttpHeaders headers;
@Nullable
private InternalBody body;
@Nullable
private String uriTemplate;
@Nullable
private Consumer httpRequestConsumer;
public DefaultRequestBodyUriSpec(HttpMethod httpMethod) {
this.httpMethod = httpMethod;
}
@Override
public RequestBodySpec uri(String uriTemplate, Object... uriVariables) {
this.uriTemplate = uriTemplate;
return uri(DefaultRestClient.this.uriBuilderFactory.expand(uriTemplate, uriVariables));
}
@Override
public RequestBodySpec uri(String uriTemplate, Map uriVariables) {
this.uriTemplate = uriTemplate;
return uri(DefaultRestClient.this.uriBuilderFactory.expand(uriTemplate, uriVariables));
}
@Override
public RequestBodySpec uri(String uriTemplate, Function uriFunction) {
this.uriTemplate = uriTemplate;
return uri(uriFunction.apply(DefaultRestClient.this.uriBuilderFactory.uriString(uriTemplate)));
}
@Override
public RequestBodySpec uri(Function 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 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 httpRequest(Consumer 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 RequestBodySpec body(T body, ParameterizedTypeReference 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() {
ResponseSpec responseSpec = exchangeInternal(DefaultResponseSpec::new, false);
Assert.state(responseSpec != null, "No ResponseSpec");
return responseSpec;
}
@Override
@Nullable
public T exchange(ExchangeFunction exchangeFunction, boolean close) {
return exchangeInternal(exchangeFunction, close);
}
@Nullable
private T exchangeInternal(ExchangeFunction exchangeFunction, boolean close) {
Assert.notNull(exchangeFunction, "ExchangeFunction must not be null");
ClientHttpResponse clientResponse = null;
Observation observation = null;
Observation.Scope observationScope = null;
URI uri = null;
try {
if (DefaultRestClient.this.defaultRequest != null) {
DefaultRestClient.this.defaultRequest.accept(this);
}
uri = initUri();
HttpHeaders headers = initHeaders();
ClientHttpRequest clientRequest = createRequest(uri);
clientRequest.getHeaders().addAll(headers);
ClientRequestObservationContext observationContext = new ClientRequestObservationContext(clientRequest);
observationContext.setUriTemplate(this.uriTemplate);
observation = ClientHttpObservationDocumentation.HTTP_CLIENT_EXCHANGES.observation(observationConvention,
DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, observationRegistry).start();
observationScope = observation.openScope();
if (this.body != null) {
this.body.writeTo(clientRequest);
}
if (this.httpRequestConsumer != null) {
this.httpRequestConsumer.accept(clientRequest);
}
clientResponse = clientRequest.execute();
observationContext.setResponse(clientResponse);
ConvertibleClientHttpResponse convertibleWrapper = new DefaultConvertibleClientHttpResponse(clientResponse, observation, observationScope);
return exchangeFunction.exchange(clientRequest, convertibleWrapper);
}
catch (IOException ex) {
ResourceAccessException resourceAccessException = createResourceAccessException(uri, this.httpMethod, ex);
if (observationScope != null) {
observationScope.close();
}
if (observation != null) {
observation.error(resourceAccessException);
observation.stop();
}
throw resourceAccessException;
}
catch (Throwable error) {
if (observationScope != null) {
observationScope.close();
}
if (observation != null) {
observation.error(error);
observation.stop();
}
throw error;
}
finally {
if (close && clientResponse != null) {
clientResponse.close();
if (observationScope != null) {
observationScope.close();
}
if (observation != null) {
observation.stop();
}
}
}
}
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 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 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, "ResponseErrorHandler 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
@Nullable
public T body(Class bodyType) {
return readBody(bodyType, bodyType);
}
@Override
@Nullable
public T body(ParameterizedTypeReference bodyType) {
Type type = bodyType.getType();
Class bodyClass = bodyClass(type);
return readBody(type, bodyClass);
}
@Override
public ResponseEntity toEntity(Class bodyType) {
return toEntityInternal(bodyType, bodyType);
}
@Override
public ResponseEntity toEntity(ParameterizedTypeReference bodyType) {
Type type = bodyType.getType();
Class bodyClass = bodyClass(type);
return toEntityInternal(type, bodyClass);
}
private ResponseEntity toEntityInternal(Type bodyType, Class bodyClass) {
T body = readBody(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 toBodilessEntity() {
try (this.clientResponse) {
applyStatusHandlers();
return ResponseEntity.status(this.clientResponse.getStatusCode())
.headers(this.clientResponse.getHeaders())
.build();
}
catch (UncheckedIOException ex) {
throw new ResourceAccessException("Could not retrieve response status code: " + ex.getMessage(), ex.getCause());
}
catch (IOException ex) {
throw new ResourceAccessException("Could not retrieve response status code: " + ex.getMessage(), ex);
}
}
@Nullable
private T readBody(Type bodyType, Class bodyClass) {
return DefaultRestClient.this.readWithMessageConverters(this.clientResponse, this::applyStatusHandlers,
bodyType, bodyClass, getCurrentObservation());
}
private void applyStatusHandlers() {
try {
ClientHttpResponse response = this.clientResponse;
if (response instanceof DefaultConvertibleClientHttpResponse convertibleResponse) {
response = convertibleResponse.delegate;
}
for (StatusHandler handler : this.statusHandlers) {
if (handler.test(response)) {
handler.handle(this.clientRequest, response);
return;
}
}
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
@Nullable
private Observation getCurrentObservation() {
if (this.clientResponse instanceof DefaultConvertibleClientHttpResponse convertibleResponse) {
return convertibleResponse.observation;
}
return null;
}
}
private class DefaultConvertibleClientHttpResponse implements RequestHeadersSpec.ConvertibleClientHttpResponse {
private final ClientHttpResponse delegate;
private final Observation observation;
private final Observation.Scope observationScope;
public DefaultConvertibleClientHttpResponse(ClientHttpResponse delegate, Observation observation, Observation.Scope observationScope) {
this.delegate = delegate;
this.observation = observation;
this.observationScope = observationScope;
}
@Nullable
@Override
public T bodyTo(Class bodyType) {
return readWithMessageConverters(this.delegate, () -> {} , bodyType, bodyType, this.observation);
}
@Nullable
@Override
public T bodyTo(ParameterizedTypeReference bodyType) {
Type type = bodyType.getType();
Class bodyClass = bodyClass(type);
return readWithMessageConverters(this.delegate, () -> {}, type, bodyClass, this.observation);
}
@Override
public InputStream getBody() throws IOException {
return this.delegate.getBody();
}
@Override
public HttpHeaders getHeaders() {
return this.delegate.getHeaders();
}
@Override
public HttpStatusCode getStatusCode() throws IOException {
return this.delegate.getStatusCode();
}
@Override
public String getStatusText() throws IOException {
return this.delegate.getStatusText();
}
@Override
public void close() {
this.delegate.close();
this.observationScope.close();
this.observation.stop();
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy