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}