001/*
002 * Copyright 2002-2020 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.web.client;
018
019import java.io.IOException;
020import java.lang.reflect.Type;
021import java.util.List;
022
023import org.apache.commons.logging.Log;
024import org.apache.commons.logging.LogFactory;
025
026import org.springframework.core.ResolvableType;
027import org.springframework.http.MediaType;
028import org.springframework.http.client.ClientHttpResponse;
029import org.springframework.http.converter.GenericHttpMessageConverter;
030import org.springframework.http.converter.HttpMessageConverter;
031import org.springframework.http.converter.HttpMessageNotReadableException;
032import org.springframework.lang.Nullable;
033import org.springframework.util.Assert;
034import org.springframework.util.FileCopyUtils;
035
036/**
037 * Response extractor that uses the given {@linkplain HttpMessageConverter entity converters}
038 * to convert the response into a type {@code T}.
039 *
040 * @author Arjen Poutsma
041 * @author Sam Brannen
042 * @since 3.0
043 * @param <T> the data type
044 * @see RestTemplate
045 */
046public class HttpMessageConverterExtractor<T> implements ResponseExtractor<T> {
047
048        private final Type responseType;
049
050        @Nullable
051        private final Class<T> responseClass;
052
053        private final List<HttpMessageConverter<?>> messageConverters;
054
055        private final Log logger;
056
057
058        /**
059         * Create a new instance of the {@code HttpMessageConverterExtractor} with the given response
060         * type and message converters. The given converters must support the response type.
061         */
062        public HttpMessageConverterExtractor(Class<T> responseType, List<HttpMessageConverter<?>> messageConverters) {
063                this((Type) responseType, messageConverters);
064        }
065
066        /**
067         * Creates a new instance of the {@code HttpMessageConverterExtractor} with the given response
068         * type and message converters. The given converters must support the response type.
069         */
070        public HttpMessageConverterExtractor(Type responseType, List<HttpMessageConverter<?>> messageConverters) {
071                this(responseType, messageConverters, LogFactory.getLog(HttpMessageConverterExtractor.class));
072        }
073
074        @SuppressWarnings("unchecked")
075        HttpMessageConverterExtractor(Type responseType, List<HttpMessageConverter<?>> messageConverters, Log logger) {
076                Assert.notNull(responseType, "'responseType' must not be null");
077                Assert.notEmpty(messageConverters, "'messageConverters' must not be empty");
078                Assert.noNullElements(messageConverters, "'messageConverters' must not contain null elements");
079                this.responseType = responseType;
080                this.responseClass = (responseType instanceof Class ? (Class<T>) responseType : null);
081                this.messageConverters = messageConverters;
082                this.logger = logger;
083        }
084
085
086        @Override
087        @SuppressWarnings({"unchecked", "rawtypes", "resource"})
088        public T extractData(ClientHttpResponse response) throws IOException {
089                MessageBodyClientHttpResponseWrapper responseWrapper = new MessageBodyClientHttpResponseWrapper(response);
090                if (!responseWrapper.hasMessageBody() || responseWrapper.hasEmptyMessageBody()) {
091                        return null;
092                }
093                MediaType contentType = getContentType(responseWrapper);
094
095                try {
096                        for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
097                                if (messageConverter instanceof GenericHttpMessageConverter) {
098                                        GenericHttpMessageConverter<?> genericMessageConverter =
099                                                        (GenericHttpMessageConverter<?>) messageConverter;
100                                        if (genericMessageConverter.canRead(this.responseType, null, contentType)) {
101                                                if (logger.isDebugEnabled()) {
102                                                        ResolvableType resolvableType = ResolvableType.forType(this.responseType);
103                                                        logger.debug("Reading to [" + resolvableType + "]");
104                                                }
105                                                return (T) genericMessageConverter.read(this.responseType, null, responseWrapper);
106                                        }
107                                }
108                                if (this.responseClass != null) {
109                                        if (messageConverter.canRead(this.responseClass, contentType)) {
110                                                if (logger.isDebugEnabled()) {
111                                                        String className = this.responseClass.getName();
112                                                        logger.debug("Reading to [" + className + "] as \"" + contentType + "\"");
113                                                }
114                                                return (T) messageConverter.read((Class) this.responseClass, responseWrapper);
115                                        }
116                                }
117                        }
118                }
119                catch (IOException | HttpMessageNotReadableException ex) {
120                        throw new RestClientException("Error while extracting response for type [" +
121                                        this.responseType + "] and content type [" + contentType + "]", ex);
122                }
123
124                throw new UnknownContentTypeException(this.responseType, contentType,
125                                response.getRawStatusCode(), response.getStatusText(), response.getHeaders(),
126                                getResponseBody(response));
127        }
128
129        /**
130         * Determine the Content-Type of the response based on the "Content-Type"
131         * header or otherwise default to {@link MediaType#APPLICATION_OCTET_STREAM}.
132         * @param response the response
133         * @return the MediaType, or "application/octet-stream"
134         */
135        protected MediaType getContentType(ClientHttpResponse response) {
136                MediaType contentType = response.getHeaders().getContentType();
137                if (contentType == null) {
138                        if (logger.isTraceEnabled()) {
139                                logger.trace("No content-type, using 'application/octet-stream'");
140                        }
141                        contentType = MediaType.APPLICATION_OCTET_STREAM;
142                }
143                return contentType;
144        }
145
146        private static byte[] getResponseBody(ClientHttpResponse response) {
147                try {
148                        return FileCopyUtils.copyToByteArray(response.getBody());
149                }
150                catch (IOException ex) {
151                        // ignore
152                }
153                return new byte[0];
154        }
155}