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}