/* * Copyright 2002-2011 the original author or authors. * * 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 org.springframework.test.web.server; import static org.springframework.test.web.AssertionErrors.fail; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Locale; import javax.servlet.http.HttpServletResponse; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.util.Assert; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.HandlerAdapter; import org.springframework.web.servlet.HandlerExceptionResolver; import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.View; import org.springframework.web.servlet.ViewResolver; /** * A more "lightweight" alternative to the {@link DispatcherServlet} re-purposed for testing Spring MVC applications * outside of a Servlet container environment in mind. Mimics the essential functionality of the DispatcherServlet * but does not always behave in identical ways. For example invoking afterCompletion() on a HandlerInterceptor is * not essential for integration testing since the same method can be unit tested. * * <p>Unlike the DispatcherServlet, the {@link MockDispatcher} is stateful. It records contextual information during * each invocation such as the request and the response, the mapped handler and handler interceptors, and the resulting * ModelAndView. The recorded information may then be matched against application-specific expectations as defined by * {@link MvcResultActions}. Previously recorded context is cleared at the start of every dispatch invocation. * * @NotThreadSafe */ public class MockDispatcher { private Log logger = LogFactory.getLog(getClass()); private final MvcSetup mvcSetup; private MockHttpServletRequest request; private MockHttpServletResponse response; private Object handler; private HandlerInterceptor[] interceptors; private ModelAndView mav; private Exception handlerException; /** * Create a {@link MockDispatcher} with the provided {@link MvcSetup}. */ MockDispatcher(MvcSetup setup) { this.mvcSetup = setup; } /** * Process the request by invoking Spring MVC components in the {@link MvcSetup} provided to the constructor. * The request may be partially processed if mapOnly is {@code true}. * */ public MvcResultActions dispatch(MockHttpServletRequest request, MockHttpServletResponse response, boolean mapOnly) { clear(); this.request = request; this.response = response; try { RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); doDispatch(mapOnly); } catch (Exception exception) { logger.error("Unhandled exception", exception); fail("Failed to dispatch Mock MVC request (check logs for stacktrace): " + exception); } finally { RequestContextHolder.resetRequestAttributes(); } return this.new ResultActionsAdapter(); } private void clear() { request = null; response = null; handler = null; interceptors = null; mav = null; } private void doDispatch(boolean mapOnly) throws Exception { try { initHandlerExecutionChain(); if (handler == null || mapOnly) { return; } List<HandlerInterceptor> interceptorList = (interceptors != null) ? Arrays.asList(interceptors) : new ArrayList<HandlerInterceptor>(); for (HandlerInterceptor interceptor : interceptorList) { if (!interceptor.preHandle(request, response, handler)) { return; } } HandlerAdapter adapter = getHandlerAdapter(); mav = adapter.handle(request, response, handler); updateDefaultViewName(); Collections.reverse(interceptorList); for (HandlerInterceptor interceptor : interceptorList) { interceptor.postHandle(request, response, handler, mav); } } catch (Exception exception) { processHandlerException(exception); updateDefaultViewName(); } if (mav == null) { return; } Locale locale = mvcSetup.getLocaleResolver().resolveLocale(request); response.setLocale(locale); View view = resolveView(locale); view.render(mav.getModel(), request, response); } private void initHandlerExecutionChain() throws Exception { for (HandlerMapping mapping : mvcSetup.getHandlerMappings()) { HandlerExecutionChain chain = mapping.getHandler(request); if (chain != null) { handler = chain.getHandler(); interceptors = chain.getInterceptors(); return; } } response.sendError(HttpServletResponse.SC_NOT_FOUND); } private HandlerAdapter getHandlerAdapter() { for (HandlerAdapter adapter : mvcSetup.getHandlerAdapters()) { if (adapter.supports(handler)) { return adapter; } } throw new IllegalStateException("No adapter for handler [" + handler + "]. Available adapters: [" + mvcSetup.getHandlerAdapters() + "]"); } private void updateDefaultViewName() throws Exception { if (mav != null && !mav.hasView()) { String viewName = mvcSetup.getViewNameTranslator().getViewName(request); mav.setViewName(viewName); } } private void processHandlerException(Exception exception) throws Exception { handlerException = exception; for (HandlerExceptionResolver resolver : mvcSetup.getExceptionResolvers()) { mav = resolver.resolveException(request, response, handler, exception); if (mav != null) { mav = mav.isEmpty() ? null : mav; return; } } throw exception; } private View resolveView(Locale locale) throws Exception { if (mav.isReference()) { for (ViewResolver viewResolver : mvcSetup.getViewResolvers()) { View view = viewResolver.resolveViewName(mav.getViewName(), locale); if (view != null) { return view; } } } View view = mav.getView(); Assert.isTrue(view != null, "Could not resolve view from ModelAndView: <" + mav + ">"); return view; } private class ResultActionsAdapter implements MvcResultActions { public MvcResultActions andExpect(MvcResultMatcher matcher) { matcher.match(request, response, handler, handlerException, mav); return this; } } }