001: /*
002: * Copyright 2002-2007 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: * http://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:
017: package org.springframework.web.servlet.handler;
018:
019: import java.util.Enumeration;
020: import java.util.Properties;
021: import java.util.Set;
022:
023: import javax.servlet.http.HttpServletRequest;
024: import javax.servlet.http.HttpServletResponse;
025:
026: import org.apache.commons.logging.Log;
027: import org.apache.commons.logging.LogFactory;
028:
029: import org.springframework.core.Ordered;
030: import org.springframework.web.servlet.HandlerExceptionResolver;
031: import org.springframework.web.servlet.ModelAndView;
032: import org.springframework.web.util.WebUtils;
033:
034: /**
035: * {@link org.springframework.web.servlet.HandlerExceptionResolver} implementation
036: * that allows for mapping exception class names to view names, either for a
037: * set of given handlers or for all handlers in the DispatcherServlet.
038: *
039: * <p>Error views are analogous to error page JSPs, but can be used with any
040: * kind of exception including any checked one, with fine-granular mappings for
041: * specific handlers.
042: *
043: * @author Juergen Hoeller
044: * @since 22.11.2003
045: * @see org.springframework.web.servlet.DispatcherServlet
046: */
047: public class SimpleMappingExceptionResolver implements
048: HandlerExceptionResolver, Ordered {
049:
050: /**
051: * The default name of the exception attribute: "exception".
052: */
053: public static final String DEFAULT_EXCEPTION_ATTRIBUTE = "exception";
054:
055: /** Logger available to subclasses */
056: protected final Log logger = LogFactory.getLog(getClass());
057:
058: private int order = Integer.MAX_VALUE; // default: same as non-Ordered
059:
060: private Set mappedHandlers;
061:
062: private Class[] mappedHandlerClasses;
063:
064: private Log warnLogger;
065:
066: private Properties exceptionMappings;
067:
068: private String defaultErrorView;
069:
070: private Integer defaultStatusCode;
071:
072: private String exceptionAttribute = DEFAULT_EXCEPTION_ATTRIBUTE;
073:
074: public void setOrder(int order) {
075: this .order = order;
076: }
077:
078: public int getOrder() {
079: return this .order;
080: }
081:
082: /**
083: * Specify the set of handlers that this exception resolver should apply to.
084: * The exception mappings and the default error view will only apply
085: * to the specified handlers.
086: * <p>If no handlers and handler classes are set, the exception mappings
087: * and the default error view will apply to all handlers. This means that
088: * a specified default error view will be used as fallback for all exceptions;
089: * any further HandlerExceptionResolvers in the chain will be ignored in
090: * this case.
091: */
092: public void setMappedHandlers(Set mappedHandlers) {
093: this .mappedHandlers = mappedHandlers;
094: }
095:
096: /**
097: * Specify the set of classes that this exception resolver should apply to.
098: * The exception mappings and the default error view will only apply
099: * to handlers of the specified type; the specified types may be interfaces
100: * and superclasses of handlers as well.
101: * <p>If no handlers and handler classes are set, the exception mappings
102: * and the default error view will apply to all handlers. This means that
103: * a specified default error view will be used as fallback for all exceptions;
104: * any further HandlerExceptionResolvers in the chain will be ignored in
105: * this case.
106: */
107: public void setMappedHandlerClasses(Class[] mappedHandlerClasses) {
108: this .mappedHandlerClasses = mappedHandlerClasses;
109: }
110:
111: /**
112: * Set the log category for warn logging. The name will be passed to the
113: * underlying logger implementation through Commons Logging, getting
114: * interpreted as log category according to the logger's configuration.
115: * <p>Default is no warn logging. Specify this setting to activate
116: * warn logging into a specific category. Alternatively, override
117: * the {@link #logException} method for custom logging.
118: * @see org.apache.commons.logging.LogFactory#getLog(String)
119: * @see org.apache.log4j.Logger#getLogger(String)
120: * @see java.util.logging.Logger#getLogger(String)
121: */
122: public void setWarnLogCategory(String loggerName) {
123: this .warnLogger = LogFactory.getLog(loggerName);
124: }
125:
126: /**
127: * Set the mappings between exception class names and error view names.
128: * The exception class name can be a substring, with no wildcard support
129: * at present. A value of "ServletException" would match
130: * <code>javax.servlet.ServletException</code> and subclasses, for example.
131: * <p><b>NB:</b> Consider carefully how specific the pattern is, and whether
132: * to include package information (which isn't mandatory). For example,
133: * "Exception" will match nearly anything, and will probably hide other rules.
134: * "java.lang.Exception" would be correct if "Exception" was meant to define
135: * a rule for all checked exceptions. With more unusual exception names such
136: * as "BaseBusinessException" there's no need to use a FQN.
137: * <p>Follows the same matching algorithm as RuleBasedTransactionAttribute
138: * and RollbackRuleAttribute.
139: * @param mappings exception patterns (can also be fully qualified class names)
140: * as keys, and error view names as values
141: * @see org.springframework.transaction.interceptor.RuleBasedTransactionAttribute
142: * @see org.springframework.transaction.interceptor.RollbackRuleAttribute
143: */
144: public void setExceptionMappings(Properties mappings) {
145: this .exceptionMappings = mappings;
146: }
147:
148: /**
149: * Set the name of the default error view.
150: * This view will be returned if no specific mapping was found.
151: * <p>Default is none.
152: */
153: public void setDefaultErrorView(String defaultErrorView) {
154: this .defaultErrorView = defaultErrorView;
155: }
156:
157: /**
158: * Set the default HTTP status code that this exception resolver will apply
159: * if it resolves an error view.
160: * <p>Note that this error code will only get applied in case of a top-level
161: * request. It will not be set for an include request, since the HTTP status
162: * cannot be modified from within an include.
163: * <p>If not specified, no status code will be applied, either leaving this to
164: * the controller or view, or keeping the servlet engine's default of 200 (OK).
165: * @param defaultStatusCode HTTP status code value, for example
166: * 500 (SC_INTERNAL_SERVER_ERROR) or 404 (SC_NOT_FOUND)
167: * @see javax.servlet.http.HttpServletResponse#SC_INTERNAL_SERVER_ERROR
168: * @see javax.servlet.http.HttpServletResponse#SC_NOT_FOUND
169: */
170: public void setDefaultStatusCode(int defaultStatusCode) {
171: this .defaultStatusCode = new Integer(defaultStatusCode);
172: }
173:
174: /**
175: * Set the name of the model attribute as which the exception should
176: * be exposed. Default is "exception".
177: * <p>This can be either set to a different attribute name or to
178: * <code>null</code> for not exposing an exception attribute at all.
179: * @see #DEFAULT_EXCEPTION_ATTRIBUTE
180: */
181: public void setExceptionAttribute(String exceptionAttribute) {
182: this .exceptionAttribute = exceptionAttribute;
183: }
184:
185: /**
186: * Checks whether this resolver is supposed to apply (i.e. the handler
187: * matches in case of "mappedHandlers" having been specified), then
188: * delegates to the {@link #doResolveException} template method.
189: */
190: public ModelAndView resolveException(HttpServletRequest request,
191: HttpServletResponse response, Object handler, Exception ex) {
192:
193: if (shouldApplyTo(request, handler)) {
194: return doResolveException(request, response, handler, ex);
195: } else {
196: return null;
197: }
198: }
199:
200: /**
201: * Check whether this resolver is supposed to apply to the given handler.
202: * <p>The default implementation checks against the specified mapped handlers
203: * and handler classes, if any.
204: * @param request current HTTP request
205: * @param handler the executed handler, or <code>null</code> if none chosen at the
206: * time of the exception (for example, if multipart resolution failed)
207: * @return whether this resolved should proceed with resolving the exception
208: * for the given request and handler
209: * @see #setMappedHandlers
210: * @see #setMappedHandlerClasses
211: */
212: protected boolean shouldApplyTo(HttpServletRequest request,
213: Object handler) {
214: if (handler != null) {
215: if (this .mappedHandlers != null
216: && this .mappedHandlers.contains(handler)) {
217: return true;
218: }
219: if (this .mappedHandlerClasses != null) {
220: for (int i = 0; i < this .mappedHandlerClasses.length; i++) {
221: if (this .mappedHandlerClasses[i]
222: .isInstance(handler)) {
223: return true;
224: }
225: }
226: }
227: }
228: // Else only apply if there are no explicit handler mappings.
229: return (this .mappedHandlers == null && this .mappedHandlerClasses == null);
230: }
231:
232: /**
233: * Actually resolve the given exception that got thrown during on handler execution,
234: * returning a ModelAndView that represents a specific error page if appropriate.
235: * <p>May be overridden in subclasses, in order to apply specific exception checks.
236: * Note that this template method will be invoked <i>after</i> checking whether
237: * this resolved applies ("mappedHandlers" etc), so an implementation may simply
238: * proceed with its actual exception handling.
239: * @param request current HTTP request
240: * @param response current HTTP response
241: * @param handler the executed handler, or <code>null</code> if none chosen at the
242: * time of the exception (for example, if multipart resolution failed)
243: * @param ex the exception that got thrown during handler execution
244: * @return a corresponding ModelAndView to forward to, or <code>null</code> for default processing
245: */
246: protected ModelAndView doResolveException(
247: HttpServletRequest request, HttpServletResponse response,
248: Object handler, Exception ex) {
249:
250: // Log exception, both at debug log level and at warn level, if desired.
251: if (logger.isDebugEnabled()) {
252: logger.debug("Resolving exception from handler [" + handler
253: + "]: " + ex);
254: }
255: logException(ex, request);
256:
257: // Expose ModelAndView for chosen error view.
258: String viewName = determineViewName(ex, request);
259: if (viewName != null) {
260: // Apply HTTP status code for error views, if specified.
261: // Only apply it if we're processing a top-level request.
262: Integer statusCode = determineStatusCode(request, viewName);
263: if (statusCode != null) {
264: applyStatusCodeIfPossible(request, response, statusCode
265: .intValue());
266: }
267: return getModelAndView(viewName, ex, request);
268: } else {
269: return null;
270: }
271: }
272:
273: /**
274: * Log the given exception at warn level, provided that warn logging has been
275: * activated through the {@link #setWarnLogCategory "warnLogCategory"} property.
276: * <p>Calls {@link #buildLogMessage} in order to determine the concrete message
277: * to log. Always passes the full exception to the logger.
278: * @param ex the exception that got thrown during handler execution
279: * @param request current HTTP request (useful for obtaining metadata)
280: * @see #setWarnLogCategory
281: * @see #buildLogMessage
282: * @see org.apache.commons.logging.Log#warn(Object, Throwable)
283: */
284: protected void logException(Exception ex, HttpServletRequest request) {
285: if (this .warnLogger != null && this .warnLogger.isWarnEnabled()) {
286: this .warnLogger.warn(buildLogMessage(ex, request), ex);
287: }
288: }
289:
290: /**
291: * Build a log message for the given exception, occured during processing
292: * the given request.
293: * @param ex the exception that got thrown during handler execution
294: * @param request current HTTP request (useful for obtaining metadata)
295: * @return the log message to use
296: */
297: protected String buildLogMessage(Exception ex,
298: HttpServletRequest request) {
299: return "Handler execution resulted in exception";
300: }
301:
302: /**
303: * Determine the view name for the given exception, searching the
304: * {@link #setExceptionMappings "exceptionMappings"}, using the
305: * {@link #setDefaultErrorView "defaultErrorView"} as fallback.
306: * @param ex the exception that got thrown during handler execution
307: * @param request current HTTP request (useful for obtaining metadata)
308: * @return the resolved view name, or <code>null</code> if none found
309: */
310: protected String determineViewName(Exception ex,
311: HttpServletRequest request) {
312: String viewName = null;
313: // Check for specific exception mappings.
314: if (this .exceptionMappings != null) {
315: viewName = findMatchingViewName(this .exceptionMappings, ex);
316: }
317: // Return default error view else, if defined.
318: if (viewName == null && this .defaultErrorView != null) {
319: if (logger.isDebugEnabled()) {
320: logger.debug("Resolving to default view '"
321: + this .defaultErrorView
322: + "' for exception of type ["
323: + ex.getClass().getName() + "]");
324: }
325: viewName = this .defaultErrorView;
326: }
327: return viewName;
328: }
329:
330: /**
331: * Find a matching view name in the given exception mappings.
332: * @param exceptionMappings mappings between exception class names and error view names
333: * @param ex the exception that got thrown during handler execution
334: * @return the view name, or <code>null</code> if none found
335: * @see #setExceptionMappings
336: */
337: protected String findMatchingViewName(Properties exceptionMappings,
338: Exception ex) {
339: String viewName = null;
340: String dominantMapping = null;
341: int deepest = Integer.MAX_VALUE;
342: for (Enumeration names = exceptionMappings.propertyNames(); names
343: .hasMoreElements();) {
344: String exceptionMapping = (String) names.nextElement();
345: int depth = getDepth(exceptionMapping, ex);
346: if (depth >= 0 && depth < deepest) {
347: deepest = depth;
348: dominantMapping = exceptionMapping;
349: viewName = exceptionMappings
350: .getProperty(exceptionMapping);
351: }
352: }
353: if (viewName != null && logger.isDebugEnabled()) {
354: logger.debug("Resolving to view '" + viewName
355: + "' for exception of type ["
356: + ex.getClass().getName()
357: + "], based on exception mapping ["
358: + dominantMapping + "]");
359: }
360: return viewName;
361: }
362:
363: /**
364: * Return the depth to the superclass matching.
365: * <p>0 means ex matches exactly. Returns -1 if there's no match.
366: * Otherwise, returns depth. Lowest depth wins.
367: * <p>Follows the same algorithm as
368: * {@link org.springframework.transaction.interceptor.RollbackRuleAttribute}.
369: */
370: protected int getDepth(String exceptionMapping, Exception ex) {
371: return getDepth(exceptionMapping, ex.getClass(), 0);
372: }
373:
374: private int getDepth(String exceptionMapping, Class exceptionClass,
375: int depth) {
376: if (exceptionClass.getName().indexOf(exceptionMapping) != -1) {
377: // Found it!
378: return depth;
379: }
380: // If we've gone as far as we can go and haven't found it...
381: if (exceptionClass.equals(Throwable.class)) {
382: return -1;
383: }
384: return getDepth(exceptionMapping, exceptionClass
385: .getSuperclass(), depth + 1);
386: }
387:
388: /**
389: * Determine the HTTP status code to apply for the given error view.
390: * <p>The default implementation always returns the specified
391: * {@link #setDefaultStatusCode "defaultStatusCode"}, as a common
392: * status code for all error views. Override this in a custom subclass
393: * to determine a specific status code for the given view.
394: * @param request current HTTP request
395: * @param viewName the name of the error view
396: * @return the HTTP status code to use, or <code>null</code> for the
397: * servlet container's default (200 in case of a standard error view)
398: * @see #setDefaultStatusCode
399: * @see #applyStatusCodeIfPossible
400: */
401: protected Integer determineStatusCode(HttpServletRequest request,
402: String viewName) {
403: return this .defaultStatusCode;
404: }
405:
406: /**
407: * Apply the specified HTTP status code to the given response, if possible
408: * (that is, if not executing within an include request).
409: * @param request current HTTP request
410: * @param response current HTTP response
411: * @param statusCode the status code to apply
412: * @see #determineStatusCode
413: * @see #setDefaultStatusCode
414: * @see javax.servlet.http.HttpServletResponse#setStatus
415: */
416: protected void applyStatusCodeIfPossible(
417: HttpServletRequest request, HttpServletResponse response,
418: int statusCode) {
419: if (!WebUtils.isIncludeRequest(request)) {
420: if (logger.isDebugEnabled()) {
421: logger.debug("Applying HTTP status code " + statusCode);
422: }
423: response.setStatus(statusCode);
424: }
425: }
426:
427: /**
428: * Return a ModelAndView for the given request, view name and exception.
429: * <p>The default implementation delegates to {@link #getModelAndView(String, Exception)}.
430: * @param viewName the name of the error view
431: * @param ex the exception that got thrown during handler execution
432: * @param request current HTTP request (useful for obtaining metadata)
433: * @return the ModelAndView instance
434: */
435: protected ModelAndView getModelAndView(String viewName,
436: Exception ex, HttpServletRequest request) {
437: return getModelAndView(viewName, ex);
438: }
439:
440: /**
441: * Return a ModelAndView for the given view name and exception.
442: * <p>The default implementation adds the specified exception attribute.
443: * Can be overridden in subclasses.
444: * @param viewName the name of the error view
445: * @param ex the exception that got thrown during handler execution
446: * @return the ModelAndView instance
447: * @see #setExceptionAttribute
448: */
449: protected ModelAndView getModelAndView(String viewName, Exception ex) {
450: ModelAndView mv = new ModelAndView(viewName);
451: if (this .exceptionAttribute != null) {
452: if (logger.isDebugEnabled()) {
453: logger.debug("Exposing Exception as model attribute '"
454: + this .exceptionAttribute + "'");
455: }
456: mv.addObject(this.exceptionAttribute, ex);
457: }
458: return mv;
459: }
460:
461: }
|