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.servlet.mvc.annotation;
018
019import java.io.IOException;
020
021import javax.servlet.http.HttpServletRequest;
022import javax.servlet.http.HttpServletResponse;
023
024import org.springframework.context.MessageSource;
025import org.springframework.context.MessageSourceAware;
026import org.springframework.context.i18n.LocaleContextHolder;
027import org.springframework.core.annotation.AnnotatedElementUtils;
028import org.springframework.lang.Nullable;
029import org.springframework.util.StringUtils;
030import org.springframework.web.bind.annotation.ResponseStatus;
031import org.springframework.web.server.ResponseStatusException;
032import org.springframework.web.servlet.ModelAndView;
033import org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver;
034
035/**
036 * A {@link org.springframework.web.servlet.HandlerExceptionResolver
037 * HandlerExceptionResolver} that uses the {@link ResponseStatus @ResponseStatus}
038 * annotation to map exceptions to HTTP status codes.
039 *
040 * <p>This exception resolver is enabled by default in the
041 * {@link org.springframework.web.servlet.DispatcherServlet DispatcherServlet}
042 * and the MVC Java config and the MVC namespace.
043 *
044 * <p>As of 4.2 this resolver also looks recursively for {@code @ResponseStatus}
045 * present on cause exceptions, and as of 4.2.2 this resolver supports
046 * attribute overrides for {@code @ResponseStatus} in custom composed annotations.
047 *
048 * <p>As of 5.0 this resolver also supports {@link ResponseStatusException}.
049 *
050 * @author Arjen Poutsma
051 * @author Rossen Stoyanchev
052 * @author Sam Brannen
053 * @since 3.0
054 * @see ResponseStatus
055 * @see ResponseStatusException
056 */
057public class ResponseStatusExceptionResolver extends AbstractHandlerExceptionResolver implements MessageSourceAware {
058
059        @Nullable
060        private MessageSource messageSource;
061
062
063        @Override
064        public void setMessageSource(MessageSource messageSource) {
065                this.messageSource = messageSource;
066        }
067
068
069        @Override
070        @Nullable
071        protected ModelAndView doResolveException(
072                        HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
073
074                try {
075                        if (ex instanceof ResponseStatusException) {
076                                return resolveResponseStatusException((ResponseStatusException) ex, request, response, handler);
077                        }
078
079                        ResponseStatus status = AnnotatedElementUtils.findMergedAnnotation(ex.getClass(), ResponseStatus.class);
080                        if (status != null) {
081                                return resolveResponseStatus(status, request, response, handler, ex);
082                        }
083
084                        if (ex.getCause() instanceof Exception) {
085                                return doResolveException(request, response, handler, (Exception) ex.getCause());
086                        }
087                }
088                catch (Exception resolveEx) {
089                        if (logger.isWarnEnabled()) {
090                                logger.warn("Failure while trying to resolve exception [" + ex.getClass().getName() + "]", resolveEx);
091                        }
092                }
093                return null;
094        }
095
096        /**
097         * Template method that handles the {@link ResponseStatus @ResponseStatus} annotation.
098         * <p>The default implementation delegates to {@link #applyStatusAndReason}
099         * with the status code and reason from the annotation.
100         * @param responseStatus the {@code @ResponseStatus} annotation
101         * @param request current HTTP request
102         * @param response current HTTP response
103         * @param handler the executed handler, or {@code null} if none chosen at the
104         * time of the exception, e.g. if multipart resolution failed
105         * @param ex the exception
106         * @return an empty ModelAndView, i.e. exception resolved
107         */
108        protected ModelAndView resolveResponseStatus(ResponseStatus responseStatus, HttpServletRequest request,
109                        HttpServletResponse response, @Nullable Object handler, Exception ex) throws Exception {
110
111                int statusCode = responseStatus.code().value();
112                String reason = responseStatus.reason();
113                return applyStatusAndReason(statusCode, reason, response);
114        }
115
116        /**
117         * Template method that handles an {@link ResponseStatusException}.
118         * <p>The default implementation applies the headers from
119         * {@link ResponseStatusException#getResponseHeaders()} and delegates to
120         * {@link #applyStatusAndReason} with the status code and reason from the
121         * exception.
122         * @param ex the exception
123         * @param request current HTTP request
124         * @param response current HTTP response
125         * @param handler the executed handler, or {@code null} if none chosen at the
126         * time of the exception, e.g. if multipart resolution failed
127         * @return an empty ModelAndView, i.e. exception resolved
128         * @since 5.0
129         */
130        protected ModelAndView resolveResponseStatusException(ResponseStatusException ex,
131                        HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws Exception {
132
133                ex.getResponseHeaders().forEach((name, values) ->
134                                values.forEach(value -> response.addHeader(name, value)));
135
136                int statusCode = ex.getStatus().value();
137                String reason = ex.getReason();
138                return applyStatusAndReason(statusCode, reason, response);
139        }
140
141        /**
142         * Apply the resolved status code and reason to the response.
143         * <p>The default implementation sends a response error using
144         * {@link HttpServletResponse#sendError(int)} or
145         * {@link HttpServletResponse#sendError(int, String)} if there is a reason
146         * and then returns an empty ModelAndView.
147         * @param statusCode the HTTP status code
148         * @param reason the associated reason (may be {@code null} or empty)
149         * @param response current HTTP response
150         * @since 5.0
151         */
152        protected ModelAndView applyStatusAndReason(int statusCode, @Nullable String reason, HttpServletResponse response)
153                        throws IOException {
154
155                if (!StringUtils.hasLength(reason)) {
156                        response.sendError(statusCode);
157                }
158                else {
159                        String resolvedReason = (this.messageSource != null ?
160                                        this.messageSource.getMessage(reason, null, reason, LocaleContextHolder.getLocale()) :
161                                        reason);
162                        response.sendError(statusCode, resolvedReason);
163                }
164                return new ModelAndView();
165        }
166
167}