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.portlet.handler;
018:
019: import java.util.Enumeration;
020: import java.util.Properties;
021: import java.util.Set;
022:
023: import javax.portlet.RenderRequest;
024: import javax.portlet.RenderResponse;
025: import javax.portlet.WindowState;
026:
027: import org.apache.commons.logging.Log;
028: import org.apache.commons.logging.LogFactory;
029:
030: import org.springframework.core.Ordered;
031: import org.springframework.web.portlet.HandlerExceptionResolver;
032: import org.springframework.web.portlet.ModelAndView;
033:
034: /**
035: * {@link org.springframework.web.portlet.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 DispatcherPortlet.
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: * @author John A. Lewis
045: * @since 2.0
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 boolean renderWhenMinimized = false;
065:
066: private Log warnLogger;
067:
068: private Properties exceptionMappings;
069:
070: private String defaultErrorView;
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 map.
084: * The exception mappings and the default error view will only apply
085: * to the specified handlers.
086: * <p>If no handlers set, both the exception mappings and the default error
087: * view will apply to all handlers. This means that a specified default
088: * error view will be used as fallback for all exceptions; any further
089: * HandlerExceptionResolvers in the chain will be ignored in this case.
090: */
091: public void setMappedHandlers(Set mappedHandlers) {
092: this .mappedHandlers = mappedHandlers;
093: }
094:
095: /**
096: * Specify the set of classes that this exception resolver should apply to.
097: * The exception mappings and the default error view will only apply
098: * to handlers of the specified type; the specified types may be interfaces
099: * and superclasses of handlers as well.
100: * <p>If no handlers and handler classes are set, the exception mappings
101: * and the default error view will apply to all handlers. This means that
102: * a specified default error view will be used as fallback for all exceptions;
103: * any further HandlerExceptionResolvers in the chain will be ignored in
104: * this case.
105: */
106: public void setMappedHandlerClasses(Class[] mappedHandlerClasses) {
107: this .mappedHandlerClasses = mappedHandlerClasses;
108: }
109:
110: /**
111: * Set if the resolver should render a view when the portlet is in
112: * a minimized window. The default is "false".
113: * @see javax.portlet.RenderRequest#getWindowState()
114: * @see javax.portlet.WindowState#MINIMIZED
115: */
116: public void setRenderWhenMinimized(boolean renderWhenMinimized) {
117: this .renderWhenMinimized = renderWhenMinimized;
118: }
119:
120: /**
121: * Set the log category for warn logging. The name will be passed to the
122: * underlying logger implementation through Commons Logging, getting
123: * interpreted as log category according to the logger's configuration.
124: * <p>Default is no warn logging. Specify this setting to activate
125: * warn logging into a specific category. Alternatively, override
126: * the {@link #logException} method for custom logging.
127: * @see org.apache.commons.logging.LogFactory#getLog(String)
128: * @see org.apache.log4j.Logger#getLogger(String)
129: * @see java.util.logging.Logger#getLogger(String)
130: */
131: public void setWarnLogCategory(String loggerName) {
132: this .warnLogger = LogFactory.getLog(loggerName);
133: }
134:
135: /**
136: * Set the mappings between exception class names and error view names.
137: * The exception class name can be a substring, with no wildcard support
138: * at present. A value of "PortletException" would match
139: * <code>javax.portet.PortletException</code> and subclasses, for example.
140: * <p><b>NB:</b> Consider carefully how specific the pattern is, and whether
141: * to include package information (which isn't mandatory). For example,
142: * "Exception" will match nearly anything, and will probably hide other rules.
143: * "java.lang.Exception" would be correct if "Exception" was meant to define
144: * a rule for all checked exceptions. With more unusual exception names such
145: * as "BaseBusinessException" there's no need to use a FQN.
146: * <p>Follows the same matching algorithm as RuleBasedTransactionAttribute
147: * and RollbackRuleAttribute.
148: * @param mappings exception patterns (can also be fully qualified class names)
149: * as keys, and error view names as values
150: * @see org.springframework.transaction.interceptor.RuleBasedTransactionAttribute
151: * @see org.springframework.transaction.interceptor.RollbackRuleAttribute
152: */
153: public void setExceptionMappings(Properties mappings) {
154: this .exceptionMappings = mappings;
155: }
156:
157: /**
158: * Set the name of the default error view.
159: * This view will be returned if no specific mapping was found.
160: * <p>Default is none.
161: */
162: public void setDefaultErrorView(String defaultErrorView) {
163: this .defaultErrorView = defaultErrorView;
164: }
165:
166: /**
167: * Set the name of the model attribute as which the exception should
168: * be exposed. Default is "exception".
169: * @see #DEFAULT_EXCEPTION_ATTRIBUTE
170: */
171: public void setExceptionAttribute(String exceptionAttribute) {
172: this .exceptionAttribute = exceptionAttribute;
173: }
174:
175: /**
176: * Checks whether this resolver is supposed to apply (i.e. the handler
177: * matches in case of "mappedHandlers" having been specified), then
178: * delegates to the {@link #doResolveException} template method.
179: */
180: public ModelAndView resolveException(RenderRequest request,
181: RenderResponse response, Object handler, Exception ex) {
182:
183: if (shouldApplyTo(request, handler)) {
184: return doResolveException(request, response, handler, ex);
185: } else {
186: return null;
187: }
188: }
189:
190: /**
191: * Check whether this resolver is supposed to apply to the given handler.
192: * <p>The default implementation checks against the specified mapped handlers
193: * and handler classes, if any, and alspo checks the window state (according
194: * to the "renderWhenMinimize" property).
195: * @param request current portlet request
196: * @param handler the executed handler, or <code>null</code> if none chosen at the
197: * time of the exception (for example, if multipart resolution failed)
198: * @return whether this resolved should proceed with resolving the exception
199: * for the given request and handler
200: * @see #setMappedHandlers
201: * @see #setMappedHandlerClasses
202: */
203: protected boolean shouldApplyTo(RenderRequest request,
204: Object handler) {
205: // If the portlet is minimized and we don't want to render then return null.
206: if (WindowState.MINIMIZED.equals(request.getWindowState())
207: && !this .renderWhenMinimized) {
208: return false;
209: }
210:
211: if (handler != null) {
212: if (this .mappedHandlers != null
213: && this .mappedHandlers.contains(handler)) {
214: return true;
215: }
216: if (this .mappedHandlerClasses != null) {
217: for (int i = 0; i < this .mappedHandlerClasses.length; i++) {
218: if (this .mappedHandlerClasses[i]
219: .isInstance(handler)) {
220: return true;
221: }
222: }
223: }
224: }
225: // Else only apply if there are no explicit handler mappings.
226: return (this .mappedHandlers == null && this .mappedHandlerClasses == null);
227: }
228:
229: /**
230: * Actually resolve the given exception that got thrown during on handler execution,
231: * returning a ModelAndView that represents a specific error page if appropriate.
232: * @param request current portlet request
233: * @param response current portlet response
234: * @param handler the executed handler, or null if none chosen at the time of
235: * the exception (for example, if multipart resolution failed)
236: * @param ex the exception that got thrown during handler execution
237: * @return a corresponding ModelAndView to forward to, or null for default processing
238: */
239: protected ModelAndView doResolveException(RenderRequest request,
240: RenderResponse response, Object handler, Exception ex) {
241:
242: // Log exception, both at debug log level and at warn level, if desired.
243: if (logger.isDebugEnabled()) {
244: logger.debug("Resolving exception from handler [" + handler
245: + "]: " + ex);
246: }
247: logException(ex, request);
248:
249: // Expose ModelAndView for chosen error view.
250: String viewName = determineViewName(ex, request);
251: if (viewName != null) {
252: return getModelAndView(viewName, ex, request);
253: } else {
254: return null;
255: }
256: }
257:
258: /**
259: * Log the given exception at warn level, provided that warn logging has been
260: * activated through the {@link #setWarnLogCategory "warnLogCategory"} property.
261: * <p>Calls {@link #buildLogMessage} in order to determine the concrete message
262: * to log. Always passes the full exception to the logger.
263: * @param ex the exception that got thrown during handler execution
264: * @param request current portlet request (useful for obtaining metadata)
265: * @see #setWarnLogCategory
266: * @see #buildLogMessage
267: * @see org.apache.commons.logging.Log#warn(Object, Throwable)
268: */
269: protected void logException(Exception ex, RenderRequest request) {
270: if (this .warnLogger != null && this .warnLogger.isWarnEnabled()) {
271: this .warnLogger.warn(buildLogMessage(ex, request), ex);
272: }
273: }
274:
275: /**
276: * Build a log message for the given exception, occured during processing
277: * the given request.
278: * @param ex the exception that got thrown during handler execution
279: * @param request current portlet request (useful for obtaining metadata)
280: * @return the log message to use
281: */
282: protected String buildLogMessage(Exception ex, RenderRequest request) {
283: return "Handler execution resulted in exception";
284: }
285:
286: /**
287: * Determine the view name for the given exception, searching the
288: * {@link #setExceptionMappings "exceptionMappings"}, using the
289: * {@link #setDefaultErrorView "defaultErrorView"} as fallback.
290: * @param ex the exception that got thrown during handler execution
291: * @param request current portlet request (useful for obtaining metadata)
292: * @return the resolved view name, or <code>null</code> if none found
293: */
294: protected String determineViewName(Exception ex,
295: RenderRequest request) {
296: String viewName = null;
297: // Check for specific exception mappings.
298: if (this .exceptionMappings != null) {
299: viewName = findMatchingViewName(this .exceptionMappings, ex);
300: }
301: // Return default error view else, if defined.
302: if (viewName == null && this .defaultErrorView != null) {
303: if (logger.isDebugEnabled()) {
304: logger.debug("Resolving to default view '"
305: + this .defaultErrorView
306: + "' for exception of type ["
307: + ex.getClass().getName() + "]");
308: }
309: viewName = this .defaultErrorView;
310: }
311: return viewName;
312: }
313:
314: /**
315: * Find a matching view name in the given exception mappings
316: * @param exceptionMappings mappings between exception class names and error view names
317: * @param ex the exception that got thrown during handler execution
318: * @return the view name, or <code>null</code> if none found
319: * @see #setExceptionMappings
320: */
321: protected String findMatchingViewName(Properties exceptionMappings,
322: Exception ex) {
323: String viewName = null;
324: String dominantMapping = null;
325: int deepest = Integer.MAX_VALUE;
326: for (Enumeration names = exceptionMappings.propertyNames(); names
327: .hasMoreElements();) {
328: String exceptionMapping = (String) names.nextElement();
329: int depth = getDepth(exceptionMapping, ex);
330: if (depth >= 0 && depth < deepest) {
331: deepest = depth;
332: dominantMapping = exceptionMapping;
333: viewName = exceptionMappings
334: .getProperty(exceptionMapping);
335: }
336: }
337: if (viewName != null && logger.isDebugEnabled()) {
338: logger.debug("Resolving to view '" + viewName
339: + "' for exception of type ["
340: + ex.getClass().getName()
341: + "], based on exception mapping ["
342: + dominantMapping + "]");
343: }
344: return viewName;
345: }
346:
347: /**
348: * Return the depth to the superclass matching.
349: * <p>0 means ex matches exactly. Returns -1 if there's no match.
350: * Otherwise, returns depth. Lowest depth wins.
351: * <p>Follows the same algorithm as
352: * {@link org.springframework.transaction.interceptor.RollbackRuleAttribute}.
353: */
354: protected int getDepth(String exceptionMapping, Exception ex) {
355: return getDepth(exceptionMapping, ex.getClass(), 0);
356: }
357:
358: private int getDepth(String exceptionMapping, Class exceptionClass,
359: int depth) {
360: if (exceptionClass.getName().indexOf(exceptionMapping) != -1) {
361: // Found it!
362: return depth;
363: }
364: // If we've gone as far as we can go and haven't found it...
365: if (exceptionClass.equals(Throwable.class)) {
366: return -1;
367: }
368: return getDepth(exceptionMapping, exceptionClass
369: .getSuperclass(), depth + 1);
370: }
371:
372: /**
373: * Return a ModelAndView for the given request, view name and exception.
374: * Default implementation delegates to <code>getModelAndView(viewName, ex)</code>.
375: * @param viewName the name of the error view
376: * @param ex the exception that got thrown during handler execution
377: * @param request current portlet request (useful for obtaining metadata)
378: * @return the ModelAndView instance
379: * @see #getModelAndView(String, Exception)
380: */
381: protected ModelAndView getModelAndView(String viewName,
382: Exception ex, RenderRequest request) {
383: return getModelAndView(viewName, ex);
384: }
385:
386: /**
387: * Return a ModelAndView for the given view name and exception.
388: * Default implementation adds the specified exception attribute.
389: * Can be overridden in subclasses.
390: * @param viewName the name of the error view
391: * @param ex the exception that got thrown during handler execution
392: * @return the ModelAndView instance
393: * @see #setExceptionAttribute
394: */
395: protected ModelAndView getModelAndView(String viewName, Exception ex) {
396: ModelAndView mv = new ModelAndView(viewName);
397: if (this .exceptionAttribute != null) {
398: if (logger.isDebugEnabled()) {
399: logger.debug("Exposing Exception as model attribute '"
400: + this .exceptionAttribute + "'");
401: }
402: mv.addObject(this.exceptionAttribute, ex);
403: }
404: return mv;
405: }
406:
407: }
|