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}