001/*
002 * Copyright 2002-2018 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.util.Collections;
021import java.util.LinkedHashMap;
022import java.util.List;
023import java.util.Map;
024
025import org.springframework.http.HttpStatus;
026import org.springframework.http.client.ClientHttpResponse;
027import org.springframework.http.converter.HttpMessageConverter;
028import org.springframework.lang.Nullable;
029import org.springframework.util.CollectionUtils;
030
031/**
032 * Implementation of {@link ResponseErrorHandler} that uses {@link HttpMessageConverter
033 * HttpMessageConverters} to convert HTTP error responses to {@link RestClientException
034 * RestClientExceptions}.
035 *
036 * <p>To use this error handler, you must specify a
037 * {@linkplain #setStatusMapping(Map) status mapping} and/or a
038 * {@linkplain #setSeriesMapping(Map) series mapping}. If either of these mappings has a match
039 * for the {@linkplain ClientHttpResponse#getStatusCode() status code} of a given
040 * {@code ClientHttpResponse}, {@link #hasError(ClientHttpResponse)} will return
041 * {@code true}, and {@link #handleError(ClientHttpResponse)} will attempt to use the
042 * {@linkplain #setMessageConverters(List) configured message converters} to convert the response
043 * into the mapped subclass of {@link RestClientException}. Note that the
044 * {@linkplain #setStatusMapping(Map) status mapping} takes precedence over
045 * {@linkplain #setSeriesMapping(Map) series mapping}.
046 *
047 * <p>If there is no match, this error handler will default to the behavior of
048 * {@link DefaultResponseErrorHandler}. Note that you can override this default behavior
049 * by specifying a {@linkplain #setSeriesMapping(Map) series mapping} from
050 * {@code HttpStatus.Series#CLIENT_ERROR} and/or {@code HttpStatus.Series#SERVER_ERROR}
051 * to {@code null}.
052 *
053 * @author Simon Galperin
054 * @author Arjen Poutsma
055 * @since 5.0
056 * @see RestTemplate#setErrorHandler(ResponseErrorHandler)
057 */
058public class ExtractingResponseErrorHandler extends DefaultResponseErrorHandler {
059
060        private List<HttpMessageConverter<?>> messageConverters = Collections.emptyList();
061
062        private final Map<HttpStatus, Class<? extends RestClientException>> statusMapping = new LinkedHashMap<>();
063
064        private final Map<HttpStatus.Series, Class<? extends RestClientException>> seriesMapping = new LinkedHashMap<>();
065
066
067        /**
068         * Create a new, empty {@code ExtractingResponseErrorHandler}.
069         * <p>Note that {@link #setMessageConverters(List)} must be called when using this constructor.
070         */
071        public ExtractingResponseErrorHandler() {
072        }
073
074        /**
075         * Create a new {@code ExtractingResponseErrorHandler} with the given
076         * {@link HttpMessageConverter} instances.
077         * @param messageConverters the message converters to use
078         */
079        public ExtractingResponseErrorHandler(List<HttpMessageConverter<?>> messageConverters) {
080                this.messageConverters = messageConverters;
081        }
082
083
084        /**
085         * Set the message converters to use by this extractor.
086         */
087        public void setMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
088                this.messageConverters = messageConverters;
089        }
090
091        /**
092         * Set the mapping from HTTP status code to {@code RestClientException} subclass.
093         * If this mapping has a match
094         * for the {@linkplain ClientHttpResponse#getStatusCode() status code} of a given
095         * {@code ClientHttpResponse}, {@link #hasError(ClientHttpResponse)} will return
096         * {@code true} and {@link #handleError(ClientHttpResponse)} will attempt to use the
097         * {@linkplain #setMessageConverters(List) configured message converters} to convert the
098         * response into the mapped subclass of {@link RestClientException}.
099         */
100        public void setStatusMapping(Map<HttpStatus, Class<? extends RestClientException>> statusMapping) {
101                if (!CollectionUtils.isEmpty(statusMapping)) {
102                        this.statusMapping.putAll(statusMapping);
103                }
104        }
105
106        /**
107         * Set the mapping from HTTP status series to {@code RestClientException} subclass.
108         * If this mapping has a match
109         * for the {@linkplain ClientHttpResponse#getStatusCode() status code} of a given
110         * {@code ClientHttpResponse}, {@link #hasError(ClientHttpResponse)} will return
111         * {@code true} and {@link #handleError(ClientHttpResponse)} will attempt to use the
112         * {@linkplain #setMessageConverters(List) configured message converters} to convert the
113         * response into the mapped subclass of {@link RestClientException}.
114         */
115        public void setSeriesMapping(Map<HttpStatus.Series, Class<? extends RestClientException>> seriesMapping) {
116                if (!CollectionUtils.isEmpty(seriesMapping)) {
117                        this.seriesMapping.putAll(seriesMapping);
118                }
119        }
120
121
122        @Override
123        protected boolean hasError(HttpStatus statusCode) {
124                if (this.statusMapping.containsKey(statusCode)) {
125                        return this.statusMapping.get(statusCode) != null;
126                }
127                else if (this.seriesMapping.containsKey(statusCode.series())) {
128                        return this.seriesMapping.get(statusCode.series()) != null;
129                }
130                else {
131                        return super.hasError(statusCode);
132                }
133        }
134
135        @Override
136        public void handleError(ClientHttpResponse response, HttpStatus statusCode) throws IOException {
137                if (this.statusMapping.containsKey(statusCode)) {
138                        extract(this.statusMapping.get(statusCode), response);
139                }
140                else if (this.seriesMapping.containsKey(statusCode.series())) {
141                        extract(this.seriesMapping.get(statusCode.series()), response);
142                }
143                else {
144                        super.handleError(response, statusCode);
145                }
146        }
147
148        private void extract(@Nullable Class<? extends RestClientException> exceptionClass,
149                        ClientHttpResponse response) throws IOException {
150
151                if (exceptionClass == null) {
152                        return;
153                }
154
155                HttpMessageConverterExtractor<? extends RestClientException> extractor =
156                                new HttpMessageConverterExtractor<>(exceptionClass, this.messageConverters);
157                RestClientException exception = extractor.extractData(response);
158                if (exception != null) {
159                        throw exception;
160                }
161        }
162
163}