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.servlet.handler;
018
019import java.util.Collections;
020import java.util.Enumeration;
021import java.util.HashMap;
022import java.util.Map;
023import java.util.Properties;
024import javax.servlet.http.HttpServletRequest;
025import javax.servlet.http.HttpServletResponse;
026
027import org.springframework.web.servlet.ModelAndView;
028import org.springframework.web.util.WebUtils;
029
030/**
031 * {@link org.springframework.web.servlet.HandlerExceptionResolver} implementation
032 * that allows for mapping exception class names to view names, either for a set of
033 * given handlers or for all handlers in the DispatcherServlet.
034 *
035 * <p>Error views are analogous to error page JSPs, but can be used with any kind of
036 * exception including any checked one, with fine-granular mappings for specific handlers.
037 *
038 * @author Juergen Hoeller
039 * @author Arjen Poutsma
040 * @author Rossen Stoyanchev
041 * @since 22.11.2003
042 * @see org.springframework.web.servlet.DispatcherServlet
043 */
044public class SimpleMappingExceptionResolver extends AbstractHandlerExceptionResolver {
045
046        /** The default name of the exception attribute: "exception". */
047        public static final String DEFAULT_EXCEPTION_ATTRIBUTE = "exception";
048
049
050        private Properties exceptionMappings;
051
052        private Class<?>[] excludedExceptions;
053
054        private String defaultErrorView;
055
056        private Integer defaultStatusCode;
057
058        private Map<String, Integer> statusCodes = new HashMap<String, Integer>();
059
060        private String exceptionAttribute = DEFAULT_EXCEPTION_ATTRIBUTE;
061
062
063        /**
064         * Set the mappings between exception class names and error view names.
065         * The exception class name can be a substring, with no wildcard support at present.
066         * A value of "ServletException" would match {@code javax.servlet.ServletException}
067         * and subclasses, for example.
068         * <p><b>NB:</b> Consider carefully how
069         * specific the pattern is, and whether to include package information (which isn't mandatory).
070         * For example, "Exception" will match nearly anything, and will probably hide other rules.
071         * "java.lang.Exception" would be correct if "Exception" was meant to define a rule for all
072         * checked exceptions. With more unusual exception names such as "BaseBusinessException"
073         * there's no need to use a FQN.
074         * @param mappings exception patterns (can also be fully qualified class names) as keys,
075         * and error view names as values
076         */
077        public void setExceptionMappings(Properties mappings) {
078                this.exceptionMappings = mappings;
079        }
080
081        /**
082         * Set one or more exceptions to be excluded from the exception mappings.
083         * Excluded exceptions are checked first and if one of them equals the actual
084         * exception, the exception will remain unresolved.
085         * @param excludedExceptions one or more excluded exception types
086         */
087        public void setExcludedExceptions(Class<?>... excludedExceptions) {
088                this.excludedExceptions = excludedExceptions;
089        }
090
091        /**
092         * Set the name of the default error view.
093         * This view will be returned if no specific mapping was found.
094         * <p>Default is none.
095         */
096        public void setDefaultErrorView(String defaultErrorView) {
097                this.defaultErrorView = defaultErrorView;
098        }
099
100        /**
101         * Set the HTTP status code that this exception resolver will apply for a given
102         * resolved error view. Keys are view names; values are status codes.
103         * <p>Note that this error code will only get applied in case of a top-level request.
104         * It will not be set for an include request, since the HTTP status cannot be modified
105         * from within an include.
106         * <p>If not specified, the default status code will be applied.
107         * @see #setDefaultStatusCode(int)
108         */
109        public void setStatusCodes(Properties statusCodes) {
110                for (Enumeration<?> enumeration = statusCodes.propertyNames(); enumeration.hasMoreElements();) {
111                        String viewName = (String) enumeration.nextElement();
112                        Integer statusCode = Integer.valueOf(statusCodes.getProperty(viewName));
113                        this.statusCodes.put(viewName, statusCode);
114                }
115        }
116
117        /**
118         * An alternative to {@link #setStatusCodes(Properties)} for use with
119         * Java-based configuration.
120         */
121        public void addStatusCode(String viewName, int statusCode) {
122                this.statusCodes.put(viewName, statusCode);
123        }
124
125        /**
126         * Returns the HTTP status codes provided via {@link #setStatusCodes(Properties)}.
127         * Keys are view names; values are status codes.
128         */
129        public Map<String, Integer> getStatusCodesAsMap() {
130                return Collections.unmodifiableMap(statusCodes);
131        }
132
133        /**
134         * Set the default HTTP status code that this exception resolver will apply
135         * if it resolves an error view and if there is no status code mapping defined.
136         * <p>Note that this error code will only get applied in case of a top-level request.
137         * It will not be set for an include request, since the HTTP status cannot be modified
138         * from within an include.
139         * <p>If not specified, no status code will be applied, either leaving this to the
140         * controller or view, or keeping the servlet engine's default of 200 (OK).
141         * @param defaultStatusCode HTTP status code value, for example 500
142         * ({@link HttpServletResponse#SC_INTERNAL_SERVER_ERROR}) or 404 ({@link HttpServletResponse#SC_NOT_FOUND})
143         * @see #setStatusCodes(Properties)
144         */
145        public void setDefaultStatusCode(int defaultStatusCode) {
146                this.defaultStatusCode = defaultStatusCode;
147        }
148
149        /**
150         * Set the name of the model attribute as which the exception should be exposed.
151         * Default is "exception".
152         * <p>This can be either set to a different attribute name or to {@code null}
153         * for not exposing an exception attribute at all.
154         * @see #DEFAULT_EXCEPTION_ATTRIBUTE
155         */
156        public void setExceptionAttribute(String exceptionAttribute) {
157                this.exceptionAttribute = exceptionAttribute;
158        }
159
160
161        /**
162         * Actually resolve the given exception that got thrown during on handler execution,
163         * returning a ModelAndView that represents a specific error page if appropriate.
164         * <p>May be overridden in subclasses, in order to apply specific exception checks.
165         * Note that this template method will be invoked <i>after</i> checking whether this
166         * resolved applies ("mappedHandlers" etc), so an implementation may simply proceed
167         * with its actual exception handling.
168         * @param request current HTTP request
169         * @param response current HTTP response
170         * @param handler the executed handler, or {@code null} if none chosen at the time
171         * of the exception (for example, if multipart resolution failed)
172         * @param ex the exception that got thrown during handler execution
173         * @return a corresponding {@code ModelAndView} to forward to,
174         * or {@code null} for default processing in the resolution chain
175         */
176        @Override
177        protected ModelAndView doResolveException(
178                        HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
179
180                // Expose ModelAndView for chosen error view.
181                String viewName = determineViewName(ex, request);
182                if (viewName != null) {
183                        // Apply HTTP status code for error views, if specified.
184                        // Only apply it if we're processing a top-level request.
185                        Integer statusCode = determineStatusCode(request, viewName);
186                        if (statusCode != null) {
187                                applyStatusCodeIfPossible(request, response, statusCode);
188                        }
189                        return getModelAndView(viewName, ex, request);
190                }
191                else {
192                        return null;
193                }
194        }
195
196        /**
197         * Determine the view name for the given exception, first checking against the
198         * {@link #setExcludedExceptions(Class[]) "excludedExecptions"}, then searching the
199         * {@link #setExceptionMappings "exceptionMappings"}, and finally using the
200         * {@link #setDefaultErrorView "defaultErrorView"} as a fallback.
201         * @param ex the exception that got thrown during handler execution
202         * @param request current HTTP request (useful for obtaining metadata)
203         * @return the resolved view name, or {@code null} if excluded or none found
204         */
205        protected String determineViewName(Exception ex, HttpServletRequest request) {
206                String viewName = null;
207                if (this.excludedExceptions != null) {
208                        for (Class<?> excludedEx : this.excludedExceptions) {
209                                if (excludedEx.equals(ex.getClass())) {
210                                        return null;
211                                }
212                        }
213                }
214                // Check for specific exception mappings.
215                if (this.exceptionMappings != null) {
216                        viewName = findMatchingViewName(this.exceptionMappings, ex);
217                }
218                // Return default error view else, if defined.
219                if (viewName == null && this.defaultErrorView != null) {
220                        if (logger.isDebugEnabled()) {
221                                logger.debug("Resolving to default view '" + this.defaultErrorView + "' for exception of type [" +
222                                                ex.getClass().getName() + "]");
223                        }
224                        viewName = this.defaultErrorView;
225                }
226                return viewName;
227        }
228
229        /**
230         * Find a matching view name in the given exception mappings.
231         * @param exceptionMappings mappings between exception class names and error view names
232         * @param ex the exception that got thrown during handler execution
233         * @return the view name, or {@code null} if none found
234         * @see #setExceptionMappings
235         */
236        protected String findMatchingViewName(Properties exceptionMappings, Exception ex) {
237                String viewName = null;
238                String dominantMapping = null;
239                int deepest = Integer.MAX_VALUE;
240                for (Enumeration<?> names = exceptionMappings.propertyNames(); names.hasMoreElements();) {
241                        String exceptionMapping = (String) names.nextElement();
242                        int depth = getDepth(exceptionMapping, ex);
243                        if (depth >= 0 && (depth < deepest || (depth == deepest &&
244                                        dominantMapping != null && exceptionMapping.length() > dominantMapping.length()))) {
245                                deepest = depth;
246                                dominantMapping = exceptionMapping;
247                                viewName = exceptionMappings.getProperty(exceptionMapping);
248                        }
249                }
250                if (viewName != null && logger.isDebugEnabled()) {
251                        logger.debug("Resolving to view '" + viewName + "' for exception of type [" + ex.getClass().getName() +
252                                        "], based on exception mapping [" + dominantMapping + "]");
253                }
254                return viewName;
255        }
256
257        /**
258         * Return the depth to the superclass matching.
259         * <p>0 means ex matches exactly. Returns -1 if there's no match.
260         * Otherwise, returns depth. Lowest depth wins.
261         */
262        protected int getDepth(String exceptionMapping, Exception ex) {
263                return getDepth(exceptionMapping, ex.getClass(), 0);
264        }
265
266        private int getDepth(String exceptionMapping, Class<?> exceptionClass, int depth) {
267                if (exceptionClass.getName().contains(exceptionMapping)) {
268                        // Found it!
269                        return depth;
270                }
271                // If we've gone as far as we can go and haven't found it...
272                if (exceptionClass == Throwable.class) {
273                        return -1;
274                }
275                return getDepth(exceptionMapping, exceptionClass.getSuperclass(), depth + 1);
276        }
277
278        /**
279         * Determine the HTTP status code to apply for the given error view.
280         * <p>The default implementation returns the status code for the given view name (specified through the
281         * {@link #setStatusCodes(Properties) statusCodes} property), or falls back to the
282         * {@link #setDefaultStatusCode defaultStatusCode} if there is no match.
283         * <p>Override this in a custom subclass to customize this behavior.
284         * @param request current HTTP request
285         * @param viewName the name of the error view
286         * @return the HTTP status code to use, or {@code null} for the servlet container's default
287         * (200 in case of a standard error view)
288         * @see #setDefaultStatusCode
289         * @see #applyStatusCodeIfPossible
290         */
291        protected Integer determineStatusCode(HttpServletRequest request, String viewName) {
292                if (this.statusCodes.containsKey(viewName)) {
293                        return this.statusCodes.get(viewName);
294                }
295                return this.defaultStatusCode;
296        }
297
298        /**
299         * Apply the specified HTTP status code to the given response, if possible (that is,
300         * if not executing within an include request).
301         * @param request current HTTP request
302         * @param response current HTTP response
303         * @param statusCode the status code to apply
304         * @see #determineStatusCode
305         * @see #setDefaultStatusCode
306         * @see HttpServletResponse#setStatus
307         */
308        protected void applyStatusCodeIfPossible(HttpServletRequest request, HttpServletResponse response, int statusCode) {
309                if (!WebUtils.isIncludeRequest(request)) {
310                        if (logger.isDebugEnabled()) {
311                                logger.debug("Applying HTTP status code " + statusCode);
312                        }
313                        response.setStatus(statusCode);
314                        request.setAttribute(WebUtils.ERROR_STATUS_CODE_ATTRIBUTE, statusCode);
315                }
316        }
317
318        /**
319         * Return a ModelAndView for the given request, view name and exception.
320         * <p>The default implementation delegates to {@link #getModelAndView(String, Exception)}.
321         * @param viewName the name of the error view
322         * @param ex the exception that got thrown during handler execution
323         * @param request current HTTP request (useful for obtaining metadata)
324         * @return the ModelAndView instance
325         */
326        protected ModelAndView getModelAndView(String viewName, Exception ex, HttpServletRequest request) {
327                return getModelAndView(viewName, ex);
328        }
329
330        /**
331         * Return a ModelAndView for the given view name and exception.
332         * <p>The default implementation adds the specified exception attribute.
333         * Can be overridden in subclasses.
334         * @param viewName the name of the error view
335         * @param ex the exception that got thrown during handler execution
336         * @return the ModelAndView instance
337         * @see #setExceptionAttribute
338         */
339        protected ModelAndView getModelAndView(String viewName, Exception ex) {
340                ModelAndView mv = new ModelAndView(viewName);
341                if (this.exceptionAttribute != null) {
342                        if (logger.isDebugEnabled()) {
343                                logger.debug("Exposing Exception as model attribute '" + this.exceptionAttribute + "'");
344                        }
345                        mv.addObject(this.exceptionAttribute, ex);
346                }
347                return mv;
348        }
349
350}