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.mvc.annotation; 018 019import java.io.InputStream; 020import java.io.OutputStream; 021import java.io.Reader; 022import java.io.Writer; 023import java.lang.reflect.InvocationTargetException; 024import java.lang.reflect.Method; 025import java.security.Principal; 026import java.util.ArrayList; 027import java.util.Arrays; 028import java.util.HashMap; 029import java.util.List; 030import java.util.Locale; 031import java.util.Map; 032import java.util.concurrent.ConcurrentHashMap; 033import javax.portlet.ClientDataRequest; 034import javax.portlet.Event; 035import javax.portlet.EventRequest; 036import javax.portlet.MimeResponse; 037import javax.portlet.PortalContext; 038import javax.portlet.PortletMode; 039import javax.portlet.PortletPreferences; 040import javax.portlet.PortletRequest; 041import javax.portlet.PortletResponse; 042import javax.portlet.PortletSession; 043import javax.portlet.WindowState; 044import javax.servlet.ServletRequest; 045import javax.servlet.ServletResponse; 046import javax.servlet.http.HttpSession; 047 048import org.springframework.core.ExceptionDepthComparator; 049import org.springframework.core.GenericTypeResolver; 050import org.springframework.core.MethodParameter; 051import org.springframework.core.annotation.AnnotationUtils; 052import org.springframework.core.annotation.SynthesizingMethodParameter; 053import org.springframework.ui.Model; 054import org.springframework.util.ClassUtils; 055import org.springframework.util.ObjectUtils; 056import org.springframework.util.ReflectionUtils; 057import org.springframework.web.bind.annotation.ExceptionHandler; 058import org.springframework.web.bind.support.WebArgumentResolver; 059import org.springframework.web.context.request.NativeWebRequest; 060import org.springframework.web.context.request.WebRequest; 061import org.springframework.web.portlet.ModelAndView; 062import org.springframework.web.portlet.context.PortletWebRequest; 063import org.springframework.web.portlet.handler.AbstractHandlerExceptionResolver; 064import org.springframework.web.servlet.View; 065 066/** 067 * Implementation of the {@link org.springframework.web.portlet.HandlerExceptionResolver} interface that handles 068 * exceptions through the {@link ExceptionHandler} annotation. 069 * 070 * <p>This exception resolver is enabled by default in the {@link org.springframework.web.portlet.DispatcherPortlet}. 071 * 072 * @author Arjen Poutsma 073 * @author Juergen Hoeller 074 * @since 3.0 075 */ 076public class AnnotationMethodHandlerExceptionResolver extends AbstractHandlerExceptionResolver { 077 078 /** 079 * Arbitrary {@link Method} reference, indicating no method found in the cache. 080 */ 081 private static final Method NO_METHOD_FOUND = ClassUtils.getMethodIfAvailable(System.class, "currentTimeMillis"); 082 083 084 private final Map<Class<?>, Map<Class<? extends Throwable>, Method>> exceptionHandlerCache = 085 new ConcurrentHashMap<Class<?>, Map<Class<? extends Throwable>, Method>>(64); 086 087 private WebArgumentResolver[] customArgumentResolvers; 088 089 090 /** 091 * Set a custom ArgumentResolvers to use for special method parameter types. 092 * <p>Such a custom ArgumentResolver will kick in first, having a chance to resolve 093 * an argument value before the standard argument handling kicks in. 094 */ 095 public void setCustomArgumentResolver(WebArgumentResolver argumentResolver) { 096 this.customArgumentResolvers = new WebArgumentResolver[]{argumentResolver}; 097 } 098 099 /** 100 * Set one or more custom ArgumentResolvers to use for special method parameter types. 101 * <p>Any such custom ArgumentResolver will kick in first, having a chance to resolve 102 * an argument value before the standard argument handling kicks in. 103 */ 104 public void setCustomArgumentResolvers(WebArgumentResolver[] argumentResolvers) { 105 this.customArgumentResolvers = argumentResolvers; 106 } 107 108 109 @Override 110 protected ModelAndView doResolveException( 111 PortletRequest request, MimeResponse response, Object handler, Exception ex) { 112 113 if (handler != null) { 114 Method handlerMethod = findBestExceptionHandlerMethod(handler, ex); 115 if (handlerMethod != null) { 116 NativeWebRequest webRequest = new PortletWebRequest(request, response); 117 try { 118 Object[] args = resolveHandlerArguments(handlerMethod, handler, webRequest, ex); 119 if (logger.isDebugEnabled()) { 120 logger.debug("Invoking request handler method: " + handlerMethod); 121 } 122 Object retVal = doInvokeMethod(handlerMethod, handler, args); 123 return getModelAndView(retVal); 124 } 125 catch (Exception invocationEx) { 126 logger.error("Invoking request method resulted in exception : " + handlerMethod, invocationEx); 127 } 128 } 129 } 130 return null; 131 } 132 133 /** 134 * Finds the handler method that matches the thrown exception best. 135 * @param handler the handler object 136 * @param thrownException the exception to be handled 137 * @return the best matching method; or {@code null} if none is found 138 */ 139 private Method findBestExceptionHandlerMethod(Object handler, final Exception thrownException) { 140 final Class<?> handlerType = handler.getClass(); 141 final Class<? extends Throwable> thrownExceptionType = thrownException.getClass(); 142 Method handlerMethod; 143 144 Map<Class<? extends Throwable>, Method> handlers = this.exceptionHandlerCache.get(handlerType); 145 if (handlers != null) { 146 handlerMethod = handlers.get(thrownExceptionType); 147 if (handlerMethod != null) { 148 return (handlerMethod == NO_METHOD_FOUND ? null : handlerMethod); 149 } 150 } 151 else { 152 handlers = new ConcurrentHashMap<Class<? extends Throwable>, Method>(16); 153 this.exceptionHandlerCache.put(handlerType, handlers); 154 } 155 156 final Map<Class<? extends Throwable>, Method> matchedHandlers = new HashMap<Class<? extends Throwable>, Method>(); 157 158 ReflectionUtils.doWithMethods(handlerType, new ReflectionUtils.MethodCallback() { 159 @Override 160 public void doWith(Method method) { 161 method = ClassUtils.getMostSpecificMethod(method, handlerType); 162 List<Class<? extends Throwable>> handledExceptions = getHandledExceptions(method); 163 for (Class<? extends Throwable> handledException : handledExceptions) { 164 if (handledException.isAssignableFrom(thrownExceptionType)) { 165 if (!matchedHandlers.containsKey(handledException)) { 166 matchedHandlers.put(handledException, method); 167 } 168 else { 169 Method oldMappedMethod = matchedHandlers.get(handledException); 170 if (!oldMappedMethod.equals(method)) { 171 throw new IllegalStateException( 172 "Ambiguous exception handler mapped for " + handledException + "]: {" + 173 oldMappedMethod + ", " + method + "}."); 174 } 175 } 176 } 177 } 178 } 179 }); 180 181 handlerMethod = getBestMatchingMethod(matchedHandlers, thrownException); 182 handlers.put(thrownExceptionType, (handlerMethod == null ? NO_METHOD_FOUND : handlerMethod)); 183 return handlerMethod; 184 } 185 186 /** 187 * Returns all the exception classes handled by the given method. 188 * <p>Default implementation looks for exceptions in the {@linkplain ExceptionHandler#value() annotation}, 189 * or - if that annotation element is empty - any exceptions listed in the method parameters if the 190 * method is annotated with {@code @ExceptionHandler}. 191 * @param method the method 192 * @return the handled exceptions 193 */ 194 @SuppressWarnings("unchecked") 195 protected List<Class<? extends Throwable>> getHandledExceptions(Method method) { 196 List<Class<? extends Throwable>> result = new ArrayList<Class<? extends Throwable>>(); 197 ExceptionHandler exceptionHandler = AnnotationUtils.findAnnotation(method, ExceptionHandler.class); 198 if (exceptionHandler != null) { 199 if (!ObjectUtils.isEmpty(exceptionHandler.value())) { 200 result.addAll(Arrays.asList(exceptionHandler.value())); 201 } 202 else { 203 for (Class<?> param : method.getParameterTypes()) { 204 if (Throwable.class.isAssignableFrom(param)) { 205 result.add((Class<? extends Throwable>) param); 206 } 207 } 208 } 209 } 210 return result; 211 } 212 213 /** 214 * Uses the {@link ExceptionDepthComparator} to find the best matching method. 215 * @return the best matching method, or {@code null} if none found 216 */ 217 private Method getBestMatchingMethod( 218 Map<Class<? extends Throwable>, Method> resolverMethods, Exception thrownException) { 219 220 if (resolverMethods.isEmpty()) { 221 return null; 222 } 223 Class<? extends Throwable> closestMatch = 224 ExceptionDepthComparator.findClosestMatch(resolverMethods.keySet(), thrownException); 225 Method method = resolverMethods.get(closestMatch); 226 return (method == null || NO_METHOD_FOUND == method ? null : method); 227 } 228 229 /** 230 * Resolves the arguments for the given method. Delegates to {@link #resolveCommonArgument}. 231 */ 232 private Object[] resolveHandlerArguments(Method handlerMethod, Object handler, 233 NativeWebRequest webRequest, Exception thrownException) throws Exception { 234 235 Class<?>[] paramTypes = handlerMethod.getParameterTypes(); 236 Object[] args = new Object[paramTypes.length]; 237 Class<?> handlerType = handler.getClass(); 238 for (int i = 0; i < args.length; i++) { 239 MethodParameter methodParam = new SynthesizingMethodParameter(handlerMethod, i); 240 GenericTypeResolver.resolveParameterType(methodParam, handlerType); 241 Class<?> paramType = methodParam.getParameterType(); 242 Object argValue = resolveCommonArgument(methodParam, webRequest, thrownException); 243 if (argValue != WebArgumentResolver.UNRESOLVED) { 244 args[i] = argValue; 245 } 246 else { 247 throw new IllegalStateException("Unsupported argument [" + paramType.getName() + 248 "] for @ExceptionHandler method: " + handlerMethod); 249 } 250 } 251 return args; 252 } 253 254 /** 255 * Resolves common method arguments. Delegates to registered 256 * {@link #setCustomArgumentResolver argumentResolvers} first, 257 * then checking {@link #resolveStandardArgument}. 258 * @param methodParameter the method parameter 259 * @param webRequest the request 260 * @param thrownException the exception thrown 261 * @return the argument value, or {@link org.springframework.web.bind.support.WebArgumentResolver#UNRESOLVED} 262 */ 263 protected Object resolveCommonArgument(MethodParameter methodParameter, NativeWebRequest webRequest, 264 Exception thrownException) throws Exception { 265 266 // Invoke custom argument resolvers if present... 267 if (this.customArgumentResolvers != null) { 268 for (WebArgumentResolver argumentResolver : this.customArgumentResolvers) { 269 Object value = argumentResolver.resolveArgument(methodParameter, webRequest); 270 if (value != WebArgumentResolver.UNRESOLVED) { 271 return value; 272 } 273 } 274 } 275 276 // Resolution of standard parameter types... 277 Class<?> paramType = methodParameter.getParameterType(); 278 Object value = resolveStandardArgument(paramType, webRequest, thrownException); 279 if (value != WebArgumentResolver.UNRESOLVED && !ClassUtils.isAssignableValue(paramType, value)) { 280 throw new IllegalStateException("Standard argument type [" + paramType.getName() + 281 "] resolved to incompatible value of type [" + (value != null ? value.getClass() : null) + 282 "]. Consider declaring the argument type in a less specific fashion."); 283 } 284 return value; 285 } 286 287 /** 288 * Resolves standard method arguments. The default implementation handles {@link NativeWebRequest}, 289 * {@link ServletRequest}, {@link ServletResponse}, {@link HttpSession}, {@link Principal}, 290 * {@link Locale}, request {@link InputStream}, request {@link Reader}, response {@link OutputStream}, 291 * response {@link Writer}, and the given {@code thrownException}. 292 * @param parameterType the method parameter type 293 * @param webRequest the request 294 * @param thrownException the exception thrown 295 * @return the argument value, or {@link org.springframework.web.bind.support.WebArgumentResolver#UNRESOLVED} 296 */ 297 protected Object resolveStandardArgument(Class<?> parameterType, NativeWebRequest webRequest, 298 Exception thrownException) throws Exception { 299 300 if (parameterType.isInstance(thrownException)) { 301 return thrownException; 302 } 303 else if (WebRequest.class.isAssignableFrom(parameterType)) { 304 return webRequest; 305 } 306 307 PortletRequest request = webRequest.getNativeRequest(PortletRequest.class); 308 PortletResponse response = webRequest.getNativeResponse(PortletResponse.class); 309 310 if (PortletRequest.class.isAssignableFrom(parameterType)) { 311 return request; 312 } 313 else if (PortletResponse.class.isAssignableFrom(parameterType)) { 314 return response; 315 } 316 else if (PortletSession.class.isAssignableFrom(parameterType)) { 317 return request.getPortletSession(); 318 } 319 else if (PortletPreferences.class.isAssignableFrom(parameterType)) { 320 return request.getPreferences(); 321 } 322 else if (PortletMode.class.isAssignableFrom(parameterType)) { 323 return request.getPortletMode(); 324 } 325 else if (WindowState.class.isAssignableFrom(parameterType)) { 326 return request.getWindowState(); 327 } 328 else if (PortalContext.class.isAssignableFrom(parameterType)) { 329 return request.getPortalContext(); 330 } 331 else if (Principal.class.isAssignableFrom(parameterType)) { 332 return request.getUserPrincipal(); 333 } 334 else if (Locale.class == parameterType) { 335 return request.getLocale(); 336 } 337 else if (InputStream.class.isAssignableFrom(parameterType)) { 338 if (!(request instanceof ClientDataRequest)) { 339 throw new IllegalStateException("InputStream can only get obtained for Action/ResourceRequest"); 340 } 341 return ((ClientDataRequest) request).getPortletInputStream(); 342 } 343 else if (Reader.class.isAssignableFrom(parameterType)) { 344 if (!(request instanceof ClientDataRequest)) { 345 throw new IllegalStateException("Reader can only get obtained for Action/ResourceRequest"); 346 } 347 return ((ClientDataRequest) request).getReader(); 348 } 349 else if (OutputStream.class.isAssignableFrom(parameterType)) { 350 if (!(response instanceof MimeResponse)) { 351 throw new IllegalStateException("OutputStream can only get obtained for Render/ResourceResponse"); 352 } 353 return ((MimeResponse) response).getPortletOutputStream(); 354 } 355 else if (Writer.class.isAssignableFrom(parameterType)) { 356 if (!(response instanceof MimeResponse)) { 357 throw new IllegalStateException("Writer can only get obtained for Render/ResourceResponse"); 358 } 359 return ((MimeResponse) response).getWriter(); 360 } 361 else if (Event.class == parameterType) { 362 if (!(request instanceof EventRequest)) { 363 throw new IllegalStateException("Event can only get obtained from EventRequest"); 364 } 365 return ((EventRequest) request).getEvent(); 366 } 367 else { 368 return WebArgumentResolver.UNRESOLVED; 369 } 370 } 371 372 private Object doInvokeMethod(Method method, Object target, Object[] args) throws Exception { 373 ReflectionUtils.makeAccessible(method); 374 try { 375 return method.invoke(target, args); 376 } 377 catch (InvocationTargetException ex) { 378 ReflectionUtils.rethrowException(ex.getTargetException()); 379 } 380 throw new IllegalStateException("Should never get here"); 381 } 382 383 @SuppressWarnings("unchecked") 384 private ModelAndView getModelAndView(Object returnValue) { 385 if (returnValue instanceof ModelAndView) { 386 return (ModelAndView) returnValue; 387 } 388 else if (returnValue instanceof Model) { 389 return new ModelAndView().addAllObjects(((Model) returnValue).asMap()); 390 } 391 else if (returnValue instanceof Map) { 392 return new ModelAndView().addAllObjects((Map<String, Object>) returnValue); 393 } 394 else if (returnValue instanceof View) { 395 return new ModelAndView(returnValue); 396 } 397 else if (returnValue instanceof String) { 398 return new ModelAndView((String) returnValue); 399 } 400 else if (returnValue == null) { 401 return new ModelAndView(); 402 } 403 else { 404 throw new IllegalArgumentException("Invalid handler method return value: " + returnValue); 405 } 406 } 407 408}