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