001/* 002 * Copyright 2002-2015 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.portlet.handler; 018 019import java.util.Enumeration; 020import java.util.Properties; 021import javax.portlet.MimeResponse; 022import javax.portlet.PortletRequest; 023 024import org.springframework.web.portlet.ModelAndView; 025 026/** 027 * {@link org.springframework.web.portlet.HandlerExceptionResolver} implementation 028 * that allows for mapping exception class names to view names, either for a 029 * set of given handlers or for all handlers in the DispatcherPortlet. 030 * 031 * <p>Error views are analogous to error page JSPs, but can be used with any 032 * kind of exception including any checked one, with fine-granular mappings for 033 * specific handlers. 034 * 035 * @author Juergen Hoeller 036 * @author John A. Lewis 037 * @author Arjen Poutsma 038 * @since 2.0 039 */ 040public class SimpleMappingExceptionResolver extends AbstractHandlerExceptionResolver { 041 042 /** 043 * The default name of the exception attribute: "exception". 044 */ 045 public static final String DEFAULT_EXCEPTION_ATTRIBUTE = "exception"; 046 047 private Properties exceptionMappings; 048 049 private String defaultErrorView; 050 051 private String exceptionAttribute = DEFAULT_EXCEPTION_ATTRIBUTE; 052 053 /** 054 * Set the mappings between exception class names and error view names. 055 * The exception class name can be a substring, with no wildcard support 056 * at present. A value of "PortletException" would match 057 * {@code javax.portet.PortletException} and subclasses, for example. 058 * <p><b>NB:</b> Consider carefully how specific the pattern is, and whether 059 * to include package information (which isn't mandatory). For example, 060 * "Exception" will match nearly anything, and will probably hide other rules. 061 * "java.lang.Exception" would be correct if "Exception" was meant to define 062 * a rule for all checked exceptions. With more unusual exception names such 063 * as "BaseBusinessException" there's no need to use a FQN. 064 * <p>Follows the same matching algorithm as RuleBasedTransactionAttribute 065 * and RollbackRuleAttribute. 066 * @param mappings exception patterns (can also be fully qualified class names) 067 * as keys, and error view names as values 068 * @see org.springframework.transaction.interceptor.RuleBasedTransactionAttribute 069 * @see org.springframework.transaction.interceptor.RollbackRuleAttribute 070 */ 071 public void setExceptionMappings(Properties mappings) { 072 this.exceptionMappings = mappings; 073 } 074 075 /** 076 * Set the name of the default error view. 077 * This view will be returned if no specific mapping was found. 078 * <p>Default is none. 079 */ 080 public void setDefaultErrorView(String defaultErrorView) { 081 this.defaultErrorView = defaultErrorView; 082 } 083 084 /** 085 * Set the name of the model attribute as which the exception should 086 * be exposed. Default is "exception". 087 * @see #DEFAULT_EXCEPTION_ATTRIBUTE 088 */ 089 public void setExceptionAttribute(String exceptionAttribute) { 090 this.exceptionAttribute = exceptionAttribute; 091 } 092 093 /** 094 * Actually resolve the given exception that got thrown during on handler execution, 095 * returning a ModelAndView that represents a specific error page if appropriate. 096 * @param request current portlet request 097 * @param response current portlet response 098 * @param handler the executed handler, or null if none chosen at the time of 099 * the exception (for example, if multipart resolution failed) 100 * @param ex the exception that got thrown during handler execution 101 * @return a corresponding ModelAndView to forward to, or null for default processing 102 */ 103 @Override 104 protected ModelAndView doResolveException( 105 PortletRequest request, MimeResponse response, Object handler, Exception ex) { 106 107 // Log exception, both at debug log level and at warn level, if desired. 108 if (logger.isDebugEnabled()) { 109 logger.debug("Resolving exception from handler [" + handler + "]: " + ex); 110 } 111 logException(ex, request); 112 113 // Expose ModelAndView for chosen error view. 114 String viewName = determineViewName(ex, request); 115 if (viewName != null) { 116 return getModelAndView(viewName, ex, request); 117 } 118 else { 119 return null; 120 } 121 } 122 123 /** 124 * Determine the view name for the given exception, searching the 125 * {@link #setExceptionMappings "exceptionMappings"}, using the 126 * {@link #setDefaultErrorView "defaultErrorView"} as fallback. 127 * @param ex the exception that got thrown during handler execution 128 * @param request current portlet request (useful for obtaining metadata) 129 * @return the resolved view name, or {@code null} if none found 130 */ 131 protected String determineViewName(Exception ex, PortletRequest request) { 132 String viewName = null; 133 // Check for specific exception mappings. 134 if (this.exceptionMappings != null) { 135 viewName = findMatchingViewName(this.exceptionMappings, ex); 136 } 137 // Return default error view else, if defined. 138 if (viewName == null && this.defaultErrorView != null) { 139 if (logger.isDebugEnabled()) { 140 logger.debug("Resolving to default view '" + this.defaultErrorView + 141 "' for exception of type [" + ex.getClass().getName() + "]"); 142 } 143 viewName = this.defaultErrorView; 144 } 145 return viewName; 146 } 147 148 /** 149 * Find a matching view name in the given exception mappings 150 * @param exceptionMappings mappings between exception class names and error view names 151 * @param ex the exception that got thrown during handler execution 152 * @return the view name, or {@code null} if none found 153 * @see #setExceptionMappings 154 */ 155 protected String findMatchingViewName(Properties exceptionMappings, Exception ex) { 156 String viewName = null; 157 String dominantMapping = null; 158 int deepest = Integer.MAX_VALUE; 159 for (Enumeration<?> names = exceptionMappings.propertyNames(); names.hasMoreElements();) { 160 String exceptionMapping = (String) names.nextElement(); 161 int depth = getDepth(exceptionMapping, ex); 162 if (depth >= 0 && (depth < deepest || (depth == deepest && 163 dominantMapping != null && exceptionMapping.length() > dominantMapping.length()))) { 164 deepest = depth; 165 dominantMapping = exceptionMapping; 166 viewName = exceptionMappings.getProperty(exceptionMapping); 167 } 168 } 169 if (viewName != null && logger.isDebugEnabled()) { 170 logger.debug("Resolving to view '" + viewName + "' for exception of type [" + ex.getClass().getName() + 171 "], based on exception mapping [" + dominantMapping + "]"); 172 } 173 return viewName; 174 } 175 176 /** 177 * Return the depth to the superclass matching. 178 * <p>0 means ex matches exactly. Returns -1 if there's no match. 179 * Otherwise, returns depth. Lowest depth wins. 180 * <p>Follows the same algorithm as 181 * {@link org.springframework.transaction.interceptor.RollbackRuleAttribute}. 182 */ 183 protected int getDepth(String exceptionMapping, Exception ex) { 184 return getDepth(exceptionMapping, ex.getClass(), 0); 185 } 186 187 private int getDepth(String exceptionMapping, Class<?> exceptionClass, int depth) { 188 if (exceptionClass.getName().contains(exceptionMapping)) { 189 // Found it! 190 return depth; 191 } 192 // If we've gone as far as we can go and haven't found it... 193 if (exceptionClass == Throwable.class) { 194 return -1; 195 } 196 return getDepth(exceptionMapping, exceptionClass.getSuperclass(), depth + 1); 197 } 198 199 200 /** 201 * Return a ModelAndView for the given request, view name and exception. 202 * Default implementation delegates to {@code getModelAndView(viewName, ex)}. 203 * @param viewName the name of the error view 204 * @param ex the exception that got thrown during handler execution 205 * @param request current portlet request (useful for obtaining metadata) 206 * @return the ModelAndView instance 207 * @see #getModelAndView(String, Exception) 208 */ 209 protected ModelAndView getModelAndView(String viewName, Exception ex, PortletRequest request) { 210 return getModelAndView(viewName, ex); 211 } 212 213 /** 214 * Return a ModelAndView for the given view name and exception. 215 * Default implementation adds the specified exception attribute. 216 * Can be overridden in subclasses. 217 * @param viewName the name of the error view 218 * @param ex the exception that got thrown during handler execution 219 * @return the ModelAndView instance 220 * @see #setExceptionAttribute 221 */ 222 protected ModelAndView getModelAndView(String viewName, Exception ex) { 223 ModelAndView mv = new ModelAndView(viewName); 224 if (this.exceptionAttribute != null) { 225 if (logger.isDebugEnabled()) { 226 logger.debug("Exposing Exception as model attribute '" + this.exceptionAttribute + "'"); 227 } 228 mv.addObject(this.exceptionAttribute, ex); 229 } 230 return mv; 231 } 232 233}