/* * Copyright 2014-2015 Jakub Jirutka <jakub@jirutka.cz>. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cz.jirutka.spring.exhandler; import cz.jirutka.spring.exhandler.handlers.RestExceptionHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.accept.FixedContentNegotiationStrategy; import org.springframework.web.accept.HeaderContentNegotiationStrategy; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.method.support.HandlerMethodReturnValueHandler; import org.springframework.web.method.support.ModelAndViewContainer; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver; import org.springframework.web.servlet.mvc.method.annotation.HttpEntityMethodProcessor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.lang.reflect.Method; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import static cz.jirutka.spring.exhandler.support.HttpMessageConverterUtils.getDefaultHttpMessageConverters; import static org.springframework.http.MediaType.APPLICATION_XML; import static org.springframework.web.servlet.HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE; /** * A {@link org.springframework.web.servlet.HandlerExceptionResolver HandlerExceptionResolver} * for RESTful APIs that resolves exceptions through the provided {@link RestExceptionHandler * RestExceptionHandlers}. * * @see #builder() * @see RestHandlerExceptionResolverBuilder * @see RestHandlerExceptionResolverFactoryBean */ public class RestHandlerExceptionResolver extends AbstractHandlerExceptionResolver implements InitializingBean { private static final Logger LOG = LoggerFactory.getLogger(RestHandlerExceptionResolver.class); private final MethodParameter returnTypeMethodParam; private List<HttpMessageConverter<?>> messageConverters = getDefaultHttpMessageConverters(); private Map<Class<? extends Exception>, RestExceptionHandler> handlers = new LinkedHashMap<>(); private MediaType defaultContentType = APPLICATION_XML; private ContentNegotiationManager contentNegotiationManager; // package visibility for tests HandlerMethodReturnValueHandler responseProcessor; // package visibility for tests HandlerMethodReturnValueHandler fallbackResponseProcessor; /** * Returns a builder to build and configure instance of {@code RestHandlerExceptionResolver}. */ public static RestHandlerExceptionResolverBuilder builder() { return new RestHandlerExceptionResolverBuilder(); } public RestHandlerExceptionResolver() { Method method = ClassUtils.getMethod( RestExceptionHandler.class, "handleException", Exception.class, HttpServletRequest.class); returnTypeMethodParam = new MethodParameter(method, -1); // This method caches the resolved value, so it's convenient to initialize it // only once here. returnTypeMethodParam.getGenericParameterType(); } @Override public void afterPropertiesSet() { if (contentNegotiationManager == null) { contentNegotiationManager = new ContentNegotiationManager( new HeaderContentNegotiationStrategy(), new FixedContentNegotiationStrategy(defaultContentType)); } responseProcessor = new HttpEntityMethodProcessor(messageConverters, contentNegotiationManager); fallbackResponseProcessor = new HttpEntityMethodProcessor(messageConverters, new ContentNegotiationManager(new FixedContentNegotiationStrategy(defaultContentType))); } @Override protected ModelAndView doResolveException( HttpServletRequest request, HttpServletResponse response, Object handler, Exception exception) { ResponseEntity<?> entity; try { entity = handleException(exception, request); } catch (NoExceptionHandlerFoundException ex) { LOG.warn("No exception handler found to handle exception: {}", exception.getClass().getName()); return null; } try { processResponse(entity, new ServletWebRequest(request, response)); } catch (Exception ex) { LOG.error("Failed to process error response: {}", entity, ex); return null; } return new ModelAndView(); } protected ResponseEntity<?> handleException(Exception exception, HttpServletRequest request) { // See http://stackoverflow.com/a/12979543/2217862 // This attribute is never set in MockMvc, so it's not covered in integration test. request.removeAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); RestExceptionHandler<Exception, ?> handler = resolveExceptionHandler(exception.getClass()); LOG.debug("Handling exception {} with response factory: {}", exception.getClass().getName(), handler); return handler.handleException(exception, request); } @SuppressWarnings("unchecked") protected RestExceptionHandler<Exception, ?> resolveExceptionHandler(Class<? extends Exception> exceptionClass) { for (Class clazz = exceptionClass; clazz != Throwable.class; clazz = clazz.getSuperclass()) { if (handlers.containsKey(clazz)) { return handlers.get(clazz); } } throw new NoExceptionHandlerFoundException(); } protected void processResponse(ResponseEntity<?> entity, NativeWebRequest webRequest) throws Exception { // XXX: Create MethodParameter from the actually used subclass of RestExceptionHandler? MethodParameter methodParameter = new MethodParameter(returnTypeMethodParam); ModelAndViewContainer mavContainer = new ModelAndViewContainer(); try { responseProcessor.handleReturnValue(entity, methodParameter, mavContainer, webRequest); } catch (HttpMediaTypeNotAcceptableException ex) { LOG.debug("Requested media type is not supported, falling back to default one"); fallbackResponseProcessor.handleReturnValue(entity, methodParameter, mavContainer, webRequest); } } //////// Accessors //////// // Note: We're not using Lombok in this class to make it clear for debugging. public List<HttpMessageConverter<?>> getMessageConverters() { return messageConverters; } public void setMessageConverters(List<HttpMessageConverter<?>> messageConverters) { Assert.notNull(messageConverters, "messageConverters must not be null"); this.messageConverters = messageConverters; } public ContentNegotiationManager getContentNegotiationManager() { return this.contentNegotiationManager; } public void setContentNegotiationManager(ContentNegotiationManager contentNegotiationManager) { this.contentNegotiationManager = contentNegotiationManager != null ? contentNegotiationManager : new ContentNegotiationManager(); } public MediaType getDefaultContentType() { return defaultContentType; } public void setDefaultContentType(MediaType defaultContentType) { this.defaultContentType = defaultContentType; } public Map<Class<? extends Exception>, RestExceptionHandler> getExceptionHandlers() { return handlers; } public void setExceptionHandlers(Map<Class<? extends Exception>, RestExceptionHandler> handlers) { this.handlers = handlers; } //////// Inner classes //////// public static class NoExceptionHandlerFoundException extends RuntimeException {} }