/*
 * Copyright 2002-2009 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.web.servlet.mvc.annotation;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.Writer;
import java.lang.reflect.Method;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.config.BeanExpressionContext;
import org.springframework.beans.factory.config.BeanExpressionResolver;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.xml.SourceHttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.ui.ExtendedModelMap;
import org.springframework.ui.Model;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.PathMatcher;
import org.springframework.util.StringUtils;
import org.springframework.validation.support.BindingAwareModelMap;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.HttpSessionRequiredException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.annotation.support.HandlerMethodInvoker;
import org.springframework.web.bind.annotation.support.HandlerMethodResolver;
import org.springframework.web.bind.support.DefaultSessionAttributeStore;
import org.springframework.web.bind.support.SessionAttributeStore;
import org.springframework.web.bind.support.WebArgumentResolver;
import org.springframework.web.bind.support.WebBindingInitializer;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.RequestScope;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.servlet.HandlerAdapter;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.mvc.multiaction.InternalPathMethodNameResolver;
import org.springframework.web.servlet.mvc.multiaction.MethodNameResolver;
import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException;
import org.springframework.web.servlet.support.RequestContextUtils;
import org.springframework.web.servlet.support.WebContentGenerator;
import org.springframework.web.util.UrlPathHelper;
import org.springframework.web.util.WebUtils;

/**
 * Implementation of the {@link org.springframework.web.servlet.HandlerAdapter} interface that maps handler methods
 * based on HTTP paths, HTTP methods and request parameters expressed through the {@link RequestMapping} annotation.
 *
 * <p>Supports request parameter binding through the {@link RequestParam} annotation. Also supports the {@link
 * ModelAttribute} annotation for exposing model attribute values to the view, as well as {@link InitBinder} for binder
 * initialization methods and {@link SessionAttributes} for automatic session management of specific attributes.
 *
 * <p>This adapter can be customized through various bean properties. A common use case is to apply shared binder
 * initialization logic through a custom {@link #setWebBindingInitializer WebBindingInitializer}.
 *
 * @author Juergen Hoeller
 * @author Arjen Poutsma
 * @since 2.5
 * @see #setPathMatcher
 * @see #setMethodNameResolver
 * @see #setWebBindingInitializer
 * @see #setSessionAttributeStore
 */
public class AnnotationMethodHandlerAdapter extends WebContentGenerator implements HandlerAdapter, BeanFactoryAware {

	/**
	 * Log category to use when no mapped handler is found for a request.
	 * @see #pageNotFoundLogger
	 */
	public static final String PAGE_NOT_FOUND_LOG_CATEGORY = "org.springframework.web.servlet.PageNotFound";

	/**
	 * Additional logger to use when no mapped handler is found for a request.
	 * @see #PAGE_NOT_FOUND_LOG_CATEGORY
	 */
	protected static final Log pageNotFoundLogger = LogFactory.getLog(PAGE_NOT_FOUND_LOG_CATEGORY);

	private UrlPathHelper urlPathHelper = new UrlPathHelper();

	private PathMatcher pathMatcher = new AntPathMatcher();

	private MethodNameResolver methodNameResolver = new InternalPathMethodNameResolver();

	private WebBindingInitializer webBindingInitializer;

	private SessionAttributeStore sessionAttributeStore = new DefaultSessionAttributeStore();

	private int cacheSecondsForSessionAttributeHandlers = 0;

	private boolean synchronizeOnSession = false;

	private ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();

	private WebArgumentResolver[] customArgumentResolvers;

	private ModelAndViewResolver[] customModelAndViewResolvers;

	private HttpMessageConverter<?>[] messageConverters =
			new HttpMessageConverter[] {new ByteArrayHttpMessageConverter(), new StringHttpMessageConverter(),
					new FormHttpMessageConverter(), new SourceHttpMessageConverter()};

	private ConfigurableBeanFactory beanFactory;

	private BeanExpressionContext expressionContext;

	private final Map<Class<?>, ServletHandlerMethodResolver> methodResolverCache =
			new ConcurrentHashMap<Class<?>, ServletHandlerMethodResolver>();


	public AnnotationMethodHandlerAdapter() {
		// no restriction of HTTP methods by default
		super(false);
	}


	/**
	 * Set if URL lookup should always use the full path within the current servlet context. Else, the path within the
	 * current servlet mapping is used if applicable (that is, in the case of a ".../*" servlet mapping in web.xml).
	 * <p>Default is "false".
	 * @see org.springframework.web.util.UrlPathHelper#setAlwaysUseFullPath
	 */
	public void setAlwaysUseFullPath(boolean alwaysUseFullPath) {
		this.urlPathHelper.setAlwaysUseFullPath(alwaysUseFullPath);
	}

	/**
	 * Set if context path and request URI should be URL-decoded. Both are returned <i>undecoded</i> by the Servlet API, in
	 * contrast to the servlet path. <p>Uses either the request encoding or the default encoding according to the Servlet
	 * spec (ISO-8859-1).
	 * @see org.springframework.web.util.UrlPathHelper#setUrlDecode
	 */
	public void setUrlDecode(boolean urlDecode) {
		this.urlPathHelper.setUrlDecode(urlDecode);
	}

	/**
	 * Set the UrlPathHelper to use for resolution of lookup paths. <p>Use this to override the default UrlPathHelper with
	 * a custom subclass, or to share common UrlPathHelper settings across multiple HandlerMappings and HandlerAdapters.
	 */
	public void setUrlPathHelper(UrlPathHelper urlPathHelper) {
		Assert.notNull(urlPathHelper, "UrlPathHelper must not be null");
		this.urlPathHelper = urlPathHelper;
	}

	/**
	 * Set the PathMatcher implementation to use for matching URL paths against registered URL patterns. Default is
	 * AntPathMatcher.
	 * @see org.springframework.util.AntPathMatcher
	 */
	public void setPathMatcher(PathMatcher pathMatcher) {
		Assert.notNull(pathMatcher, "PathMatcher must not be null");
		this.pathMatcher = pathMatcher;
	}

	/**
	 * Set the MethodNameResolver to use for resolving default handler methods (carrying an empty
	 * <code>@RequestMapping</code> annotation).
	 * <p>Will only kick in when the handler method cannot be resolved uniquely
	 * through the annotation metadata already.
	 */
	public void setMethodNameResolver(MethodNameResolver methodNameResolver) {
		this.methodNameResolver = methodNameResolver;
	}

	/**
	 * Specify a WebBindingInitializer which will apply pre-configured configuration to every
	 * DataBinder that this controller uses.
	 */
	public void setWebBindingInitializer(WebBindingInitializer webBindingInitializer) {
		this.webBindingInitializer = webBindingInitializer;
	}

	/**
	 * Specify the strategy to store session attributes with.
	 * <p>Default is {@link org.springframework.web.bind.support.DefaultSessionAttributeStore},
	 * storing session attributes in the HttpSession, using the same attribute name as in the model.
	 */
	public void setSessionAttributeStore(SessionAttributeStore sessionAttributeStore) {
		Assert.notNull(sessionAttributeStore, "SessionAttributeStore must not be null");
		this.sessionAttributeStore = sessionAttributeStore;
	}

	/**
	 * Cache content produced by <code>@SessionAttributes</code> annotated handlers for the given number of seconds.
	 * Default is 0, preventing caching completely.
	 * <p>In contrast to the "cacheSeconds" property which will apply to all
	 * general handlers (but not to <code>@SessionAttributes</code> annotated handlers), this setting will apply to
	 * <code>@SessionAttributes</code> annotated handlers only.
	 * @see #setCacheSeconds
	 * @see org.springframework.web.bind.annotation.SessionAttributes
	 */
	public void setCacheSecondsForSessionAttributeHandlers(int cacheSecondsForSessionAttributeHandlers) {
		this.cacheSecondsForSessionAttributeHandlers = cacheSecondsForSessionAttributeHandlers;
	}

	/**
	 * Set if controller execution should be synchronized on the session, to serialize
	 * parallel invocations from the same client.
	 * <p>More specifically, the execution of each handler method will get synchronized if this
	 * flag is "true". The best available session mutex will be used for the synchronization;
	 * ideally, this will be a mutex exposed by HttpSessionMutexListener.
	 * <p>The session mutex is guaranteed to be the same object during the entire lifetime of the
	 * session, available under the key defined by the <code>SESSION_MUTEX_ATTRIBUTE</code> constant.
	 * It serves as a safe reference to synchronize on for locking on the current session.
	 * <p>In many cases, the HttpSession reference itself a safe mutex as well, since it will
	 * always be the same object reference for the same active logical session. However, this is
	 * not guaranteed across different servlet containers; the only 100% safe way is a session mutex.
	 * @see org.springframework.web.util.HttpSessionMutexListener
	 * @see org.springframework.web.util.WebUtils#getSessionMutex(javax.servlet.http.HttpSession)
	 */
	public void setSynchronizeOnSession(boolean synchronizeOnSession) {
		this.synchronizeOnSession = synchronizeOnSession;
	}

	/**
	 * Set the ParameterNameDiscoverer to use for resolving method parameter names if needed (e.g. for default attribute
	 * names).
	 * <p>Default is a {@link org.springframework.core.LocalVariableTableParameterNameDiscoverer}.
	 */
	public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) {
		this.parameterNameDiscoverer = parameterNameDiscoverer;
	}

	/**
	 * Set a custom WebArgumentResolvers to use for special method parameter types. Such a custom WebArgumentResolver will kick
	 * in first, having a chance to resolve an argument value before the standard argument handling kicks in.
	 */
	public void setCustomArgumentResolver(WebArgumentResolver argumentResolver) {
		this.customArgumentResolvers = new WebArgumentResolver[]{argumentResolver};
	}

	/**
	 * Set one or more custom WebArgumentResolvers to use for special method parameter types. Any such custom WebArgumentResolver
	 * will kick in first, having a chance to resolve an argument value before the standard argument handling kicks in.
	 */
	public void setCustomArgumentResolvers(WebArgumentResolver[] argumentResolvers) {
		this.customArgumentResolvers = argumentResolvers;
	}

	/**
	 * Set a custom ModelAndViewResolvers to use for special method return types. Such a custom ModelAndViewResolver will kick
	 * in first, having a chance to resolve an return value before the standard ModelAndView handling kicks in.
	 */
	public void setCustomModelAndViewResolver(ModelAndViewResolver customModelAndViewResolver) {
		this.customModelAndViewResolvers = new ModelAndViewResolver[]{customModelAndViewResolver};
	}

	/**
	 * Set one or more custom ModelAndViewResolvers to use for special method return types. Any such custom ModelAndViewResolver
	 * will kick in first, having a chance to resolve an return value before the standard ModelAndView handling kicks in.
	 */
	public void setCustomModelAndViewResolvers(ModelAndViewResolver[] customModelAndViewResolvers) {
		this.customModelAndViewResolvers = customModelAndViewResolvers;
	}

	/**
	 * Set the message body converters to use. These converters are used to convert from and to HTTP requests and
	 * responses.
	 */
	public void setMessageConverters(HttpMessageConverter<?>[] messageConverters) {
		this.messageConverters = messageConverters;
	}

	public void setBeanFactory(BeanFactory beanFactory) {
		if (beanFactory instanceof ConfigurableBeanFactory) {
			this.beanFactory = (ConfigurableBeanFactory) beanFactory;
			this.expressionContext = new BeanExpressionContext(this.beanFactory, new RequestScope());
		}
	}


	public boolean supports(Object handler) {
		return getMethodResolver(handler).hasHandlerMethods();
	}

	public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {

		if (handler.getClass().getAnnotation(SessionAttributes.class) != null) {
			// Always prevent caching in case of session attribute management.
			checkAndPrepare(request, response, this.cacheSecondsForSessionAttributeHandlers, true);
			// Prepare cached set of session attributes names.
		}
		else {
			// Uses configured default cacheSeconds setting.
			checkAndPrepare(request, response, true);
		}

		// Execute invokeHandlerMethod in synchronized block if required.
		if (this.synchronizeOnSession) {
			HttpSession session = request.getSession(false);
			if (session != null) {
				Object mutex = WebUtils.getSessionMutex(session);
				synchronized (mutex) {
					return invokeHandlerMethod(request, response, handler);
				}
			}
		}

		return invokeHandlerMethod(request, response, handler);
	}

	protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {

		ServletHandlerMethodResolver methodResolver = getMethodResolver(handler);
		Method handlerMethod = methodResolver.resolveHandlerMethod(request);
		ServletHandlerMethodInvoker methodInvoker = new ServletHandlerMethodInvoker(methodResolver);
		ServletWebRequest webRequest = new ServletWebRequest(request, response);
		ExtendedModelMap implicitModel = new BindingAwareModelMap();

		Object result = methodInvoker.invokeHandlerMethod(handlerMethod, handler, webRequest, implicitModel);
		ModelAndView mav =
				methodInvoker.getModelAndView(handlerMethod, handler.getClass(), result, implicitModel, webRequest);
		methodInvoker.updateModelAttributes(handler, (mav != null ? mav.getModel() : null), implicitModel, webRequest);
		return mav;
	}

	public long getLastModified(HttpServletRequest request, Object handler) {
		return -1;
	}

	/**
	 * Build a HandlerMethodResolver for the given handler type.
	 */
	private ServletHandlerMethodResolver getMethodResolver(Object handler) {
		Class handlerClass = ClassUtils.getUserClass(handler);
		ServletHandlerMethodResolver resolver = this.methodResolverCache.get(handlerClass);
		if (resolver == null) {
			resolver = new ServletHandlerMethodResolver(handlerClass);
			this.methodResolverCache.put(handlerClass, resolver);
		}
		return resolver;
	}


	/**
	 * Servlet-specific subclass of {@link HandlerMethodResolver}.
	 */
	private class ServletHandlerMethodResolver extends HandlerMethodResolver {

		private ServletHandlerMethodResolver(Class<?> handlerType) {
			init(handlerType);
		}

		public Method resolveHandlerMethod(HttpServletRequest request) throws ServletException {
			String lookupPath = urlPathHelper.getLookupPathForRequest(request);
			Comparator<String> pathComparator = pathMatcher.getPatternComparator(lookupPath);
			Map<RequestMappingInfo, Method> targetHandlerMethods = new LinkedHashMap<RequestMappingInfo, Method>();
			Set<String> allowedMethods = new LinkedHashSet<String>(7);
			String resolvedMethodName = null;
			for (Method handlerMethod : getHandlerMethods()) {
				RequestMappingInfo mappingInfo = new RequestMappingInfo();
				RequestMapping mapping = AnnotationUtils.findAnnotation(handlerMethod, RequestMapping.class);
				mappingInfo.paths = mapping.value();
				if (!hasTypeLevelMapping() || !Arrays.equals(mapping.method(), getTypeLevelMapping().method())) {
					mappingInfo.methods = mapping.method();
				}
				if (!hasTypeLevelMapping() || !Arrays.equals(mapping.params(), getTypeLevelMapping().params())) {
					mappingInfo.params = mapping.params();
				}
				if (!hasTypeLevelMapping() || !Arrays.equals(mapping.headers(), getTypeLevelMapping().headers())) {
					mappingInfo.headers = mapping.headers();
				}
				boolean match = false;
				if (mappingInfo.paths.length > 0) {
					List<String> matchedPaths = new ArrayList<String>(mappingInfo.paths.length);
					for (String methodLevelPattern : mappingInfo.paths) {
						String matchedPattern = getMatchedPattern(methodLevelPattern, lookupPath);
						if (matchedPattern != null) {
							if (mappingInfo.matches(request)) {
								match = true;
								matchedPaths.add(matchedPattern);
							}
							else {
								for (RequestMethod requestMethod : mappingInfo.methods) {
									allowedMethods.add(requestMethod.toString());
								}
								break;
							}
						}
					}
					Collections.sort(matchedPaths, pathComparator);
					mappingInfo.matchedPaths = matchedPaths;
				}
				else {
					// No paths specified: parameter match sufficient.
					match = mappingInfo.matches(request);
					if (match && mappingInfo.methods.length == 0 && mappingInfo.params.length == 0 &&
							resolvedMethodName != null && !resolvedMethodName.equals(handlerMethod.getName())) {
						match = false;
					} else {
						for (RequestMethod requestMethod : mappingInfo.methods) {
							allowedMethods.add(requestMethod.toString());
						}						
					}
				}
				if (match) {
					Method oldMappedMethod = targetHandlerMethods.put(mappingInfo, handlerMethod);
					if (oldMappedMethod != null && oldMappedMethod != handlerMethod) {
						if (methodNameResolver != null && mappingInfo.paths.length == 0) {
							if (!oldMappedMethod.getName().equals(handlerMethod.getName())) {
								if (resolvedMethodName == null) {
									resolvedMethodName = methodNameResolver.getHandlerMethodName(request);
								}
								if (!resolvedMethodName.equals(oldMappedMethod.getName())) {
									oldMappedMethod = null;
								}
								if (!resolvedMethodName.equals(handlerMethod.getName())) {
									if (oldMappedMethod != null) {
										targetHandlerMethods.put(mappingInfo, oldMappedMethod);
										oldMappedMethod = null;
									}
									else {
										targetHandlerMethods.remove(mappingInfo);
									}
								}
							}
						}
						if (oldMappedMethod != null) {
							throw new IllegalStateException(
									"Ambiguous handler methods mapped for HTTP path '" + lookupPath + "': {" +
											oldMappedMethod + ", " + handlerMethod +
											"}. If you intend to handle the same path in multiple methods, then factor " +
											"them out into a dedicated handler class with that path mapped at the type level!");
						}
					}
				}
			}
			if (!targetHandlerMethods.isEmpty()) {
				List<RequestMappingInfo> matches = new ArrayList<RequestMappingInfo>(targetHandlerMethods.keySet());
				RequestMappingInfoComparator requestMappingInfoComparator =
						new RequestMappingInfoComparator(pathComparator);
				Collections.sort(matches, requestMappingInfoComparator);
				RequestMappingInfo bestMappingMatch = matches.get(0);
				String bestMatchedPath = bestMappingMatch.bestMatchedPath();
				if (bestMatchedPath != null) {
					extractHandlerMethodUriTemplates(bestMatchedPath, lookupPath, request);
				}
				return targetHandlerMethods.get(bestMappingMatch);
			}
			else {
				if (!allowedMethods.isEmpty()) {
					throw new HttpRequestMethodNotSupportedException(request.getMethod(),
							StringUtils.toStringArray(allowedMethods));
				}
				else {
					throw new NoSuchRequestHandlingMethodException(lookupPath, request.getMethod(),
							request.getParameterMap());
				}
			}
		}

		private String getMatchedPattern(String methodLevelPattern, String lookupPath) {
			if ((!hasTypeLevelMapping() || ObjectUtils.isEmpty(getTypeLevelMapping().value())) &&
					isPathMatchInternal(methodLevelPattern, lookupPath)) {
				return methodLevelPattern;
			}
			if (hasTypeLevelMapping()) {
				String[] typeLevelPatterns = getTypeLevelMapping().value();
				for (String typeLevelPattern : typeLevelPatterns) {
					if (!typeLevelPattern.startsWith("/")) {
						typeLevelPattern = "/" + typeLevelPattern;
					}
					String combinedPattern = pathMatcher.combine(typeLevelPattern, methodLevelPattern);
					if (isPathMatchInternal(combinedPattern, lookupPath)) {
						return combinedPattern;
					}
				}

			}
			return null;
		}

		private boolean isPathMatchInternal(String pattern, String lookupPath) {
			if (pattern.equals(lookupPath) || pathMatcher.match(pattern, lookupPath)) {
				return true;
			}
			boolean hasSuffix = pattern.indexOf('.') != -1;
			if (!hasSuffix && pathMatcher.match(pattern + ".*", lookupPath)) {
				return true;
			}
			boolean endsWithSlash = pattern.endsWith("/");
			if (!endsWithSlash && pathMatcher.match(pattern + "/", lookupPath)) {
				return true;
			}
			return false;
		}

		@SuppressWarnings("unchecked")
		private void extractHandlerMethodUriTemplates(String mappedPath,
				String lookupPath,
				HttpServletRequest request) {
			Map<String, String> variables = null;
			boolean hasSuffix = (mappedPath.indexOf('.') != -1);
			if (!hasSuffix && pathMatcher.match(mappedPath + ".*", lookupPath)) {
				String realPath = mappedPath + ".*";
				if (pathMatcher.match(realPath, lookupPath)) {
					variables = pathMatcher.extractUriTemplateVariables(realPath, lookupPath);
				}
			}
			if (variables == null && !mappedPath.startsWith("/")) {
				String realPath = "/**/" + mappedPath;
				if (pathMatcher.match(realPath, lookupPath)) {
					variables = pathMatcher.extractUriTemplateVariables(realPath, lookupPath);
				}
				else {
					realPath = realPath + ".*";
					if (pathMatcher.match(realPath, lookupPath)) {
						variables = pathMatcher.extractUriTemplateVariables(realPath, lookupPath);
					}
				}
			}
			if (!CollectionUtils.isEmpty(variables)) {
				Map<String, String> typeVariables =
						(Map<String, String>) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
				if (typeVariables != null) {
					variables.putAll(typeVariables);
				}
				request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, variables);
			}
		}
	}


	/**
	 * Servlet-specific subclass of {@link HandlerMethodInvoker}.
	 */
	private class ServletHandlerMethodInvoker extends HandlerMethodInvoker {

		private boolean responseArgumentUsed = false;

		private ServletHandlerMethodInvoker(HandlerMethodResolver resolver) {
			super(resolver, webBindingInitializer, sessionAttributeStore, parameterNameDiscoverer,
					customArgumentResolvers, messageConverters);
		}

		@Override
		protected void raiseMissingParameterException(String paramName, Class paramType) throws Exception {
			throw new MissingServletRequestParameterException(paramName, paramType.getName());
		}

		@Override
		protected void raiseSessionRequiredException(String message) throws Exception {
			throw new HttpSessionRequiredException(message);
		}

		@Override
		protected Object resolveDefaultValue(String value) {
			if (beanFactory == null) {
				return value;
			}
			String placeholdersResolved = beanFactory.resolveEmbeddedValue(value);
			BeanExpressionResolver exprResolver = beanFactory.getBeanExpressionResolver();
			if (exprResolver == null) {
				return value;
			}
			return exprResolver.evaluate(placeholdersResolved, expressionContext);
		}

		@Override
		protected HttpInputMessage createHttpInputMessage(NativeWebRequest webRequest) throws Exception {
			HttpServletRequest servletRequest = (HttpServletRequest) webRequest.getNativeRequest();
			return new ServletServerHttpRequest(servletRequest);
		}

		@Override
		protected Object resolveCookieValue(String cookieName, Class paramType, NativeWebRequest webRequest)
				throws Exception {

			HttpServletRequest servletRequest = (HttpServletRequest) webRequest.getNativeRequest();
			Cookie cookieValue = WebUtils.getCookie(servletRequest, cookieName);
			if (Cookie.class.isAssignableFrom(paramType)) {
				return cookieValue;
			}
			else if (cookieValue != null) {
				return cookieValue.getValue();
			}
			else {
				return null;
			}
		}

		@Override
		@SuppressWarnings({"unchecked"})
		protected String resolvePathVariable(String pathVarName, Class paramType, NativeWebRequest webRequest)
				throws Exception {

			HttpServletRequest servletRequest = (HttpServletRequest) webRequest.getNativeRequest();
			Map<String, String> uriTemplateVariables =
					(Map<String, String>) servletRequest.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
			if (uriTemplateVariables == null || !uriTemplateVariables.containsKey(pathVarName)) {
				throw new IllegalStateException(
						"Could not find @PathVariable [" + pathVarName + "] in @RequestMapping");
			}
			return uriTemplateVariables.get(pathVarName);
		}

		@Override
		protected Object resolveStandardArgument(Class parameterType, NativeWebRequest webRequest) throws Exception {

			HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
			HttpServletResponse response = (HttpServletResponse) webRequest.getNativeResponse();

			if (ServletRequest.class.isAssignableFrom(parameterType)) {
				return request;
			}
			else if (ServletResponse.class.isAssignableFrom(parameterType)) {
				this.responseArgumentUsed = true;
				return response;
			}
			else if (HttpSession.class.isAssignableFrom(parameterType)) {
				return request.getSession();
			}
			else if (Principal.class.isAssignableFrom(parameterType)) {
				return request.getUserPrincipal();
			}
			else if (Locale.class.equals(parameterType)) {
				return RequestContextUtils.getLocale(request);
			}
			else if (InputStream.class.isAssignableFrom(parameterType)) {
				return request.getInputStream();
			}
			else if (Reader.class.isAssignableFrom(parameterType)) {
				return request.getReader();
			}
			else if (OutputStream.class.isAssignableFrom(parameterType)) {
				this.responseArgumentUsed = true;
				return response.getOutputStream();
			}
			else if (Writer.class.isAssignableFrom(parameterType)) {
				this.responseArgumentUsed = true;
				return response.getWriter();
			}
			return super.resolveStandardArgument(parameterType, webRequest);
		}

		@SuppressWarnings("unchecked")
		public ModelAndView getModelAndView(Method handlerMethod,
				Class handlerType,
				Object returnValue,
				ExtendedModelMap implicitModel,
				ServletWebRequest webRequest) throws Exception {

			if (handlerMethod.isAnnotationPresent(ResponseStatus.class)) {
				ResponseStatus responseStatus = handlerMethod.getAnnotation(ResponseStatus.class);
				HttpServletResponse response = webRequest.getResponse();
				response.setStatus(responseStatus.value().value());
				responseArgumentUsed = true;
			}

			// Invoke custom resolvers if present...
			if (customModelAndViewResolvers != null) {
				for (ModelAndViewResolver mavResolver : customModelAndViewResolvers) {
					ModelAndView mav = mavResolver
							.resolveModelAndView(handlerMethod, handlerType, returnValue, implicitModel, webRequest);
					if (mav != ModelAndViewResolver.UNRESOLVED) {
						return mav;
					}
				}
			}

			if (returnValue != null && handlerMethod.isAnnotationPresent(ResponseBody.class)) {
				handleRequestBody(returnValue, webRequest);
			}

			if (returnValue instanceof ModelAndView) {
				ModelAndView mav = (ModelAndView) returnValue;
				mav.getModelMap().mergeAttributes(implicitModel);
				return mav;
			}
			else if (returnValue instanceof Model) {
				return new ModelAndView().addAllObjects(implicitModel).addAllObjects(((Model) returnValue).asMap());
			}
			else if (returnValue instanceof View) {
				return new ModelAndView((View) returnValue).addAllObjects(implicitModel);
			}
			else if (handlerMethod.isAnnotationPresent(ModelAttribute.class)) {
				addReturnValueAsModelAttribute(handlerMethod, handlerType, returnValue, implicitModel);
				return new ModelAndView().addAllObjects(implicitModel);
			}
			else if (returnValue instanceof Map) {
				return new ModelAndView().addAllObjects(implicitModel).addAllObjects((Map) returnValue);
			}
			else if (returnValue instanceof String) {
				return new ModelAndView((String) returnValue).addAllObjects(implicitModel);
			}
			else if (returnValue == null) {
				// Either returned null or was 'void' return.
				if (this.responseArgumentUsed || webRequest.isNotModified()) {
					return null;
				}
				else {
					// Assuming view name translation...
					return new ModelAndView().addAllObjects(implicitModel);
				}
			}
			else if (!BeanUtils.isSimpleProperty(returnValue.getClass())) {
				// Assume a single model attribute...
				addReturnValueAsModelAttribute(handlerMethod, handlerType, returnValue, implicitModel);
				return new ModelAndView().addAllObjects(implicitModel);
			}
			else {
				throw new IllegalArgumentException("Invalid handler method return value: " + returnValue);
			}
		}

		@SuppressWarnings("unchecked")
		private void handleRequestBody(Object returnValue, ServletWebRequest webRequest) throws ServletException, IOException {
			HttpInputMessage inputMessage = new ServletServerHttpRequest(webRequest.getRequest());
			List<MediaType> acceptedMediaTypes = inputMessage.getHeaders().getAccept();
			HttpOutputMessage outputMessage = new ServletServerHttpResponse(webRequest.getResponse());
			Class<?> returnValueType = returnValue.getClass();
			List<MediaType> allSupportedMediaTypes = new ArrayList<MediaType>();
			if (messageConverters != null) {
				for (HttpMessageConverter messageConverter : messageConverters) {
					allSupportedMediaTypes.addAll(messageConverter.getSupportedMediaTypes());
					if (messageConverter.supports(returnValueType)) {
						for (Object o : messageConverter.getSupportedMediaTypes()) {
							MediaType supportedMediaType = (MediaType) o;
							for (MediaType acceptedMediaType : acceptedMediaTypes) {
								if (supportedMediaType.includes(acceptedMediaType)) {
									messageConverter.write(returnValue, outputMessage);
									responseArgumentUsed = true;
									return;
								}
							}
						}
					}
				}
			}
			throw new HttpMediaTypeNotAcceptableException(allSupportedMediaTypes);
		}
	}


	static class RequestMappingInfo {

		String[] paths = new String[0];

		List<String> matchedPaths = Collections.emptyList();

		RequestMethod[] methods = new RequestMethod[0];

		String[] params = new String[0];

		String[] headers = new String[0];

		String bestMatchedPath() {
			return matchedPaths.isEmpty() ? null : matchedPaths.get(0);
		}

		public boolean matches(HttpServletRequest request) {
			return ServletAnnotationMappingUtils.checkRequestMethod(this.methods, request) &&
					ServletAnnotationMappingUtils.checkParameters(this.params, request) &&
					ServletAnnotationMappingUtils.checkHeaders(this.headers, request);
		}

		@Override
		public boolean equals(Object obj) {
			RequestMappingInfo other = (RequestMappingInfo) obj;
			return (Arrays.equals(this.paths, other.paths) && Arrays.equals(this.methods, other.methods) &&
					Arrays.equals(this.params, other.params) && Arrays.equals(this.headers, other.headers));
		}

		@Override
		public int hashCode() {
			return (Arrays.hashCode(this.paths) * 23 + Arrays.hashCode(this.methods) * 29 +
					Arrays.hashCode(this.params) * 31 + Arrays.hashCode(this.headers));
		}
	}


	/**
	 * Comparator capable of sorting {@link RequestMappingInfo}s (RHIs) so that sorting a list with this comparator will
	 * result in: <ul> <li>RHIs with {@linkplain RequestMappingInfo#matchedPaths better matched paths} take prescedence
	 * over those with a weaker match (as expressed by the {@linkplain PathMatcher#getPatternComparator(String) path
	 * pattern comparator}.) Typically, this means that patterns without wild cards and uri templates will be ordered
	 * before those without.</li> <li>RHIs with one single {@linkplain RequestMappingInfo#methods request method} will be
	 * ordered before those without a method, or with more than one method.</li> <li>RHIs with more {@linkplain
	 * RequestMappingInfo#params request parameters} will be ordered before those with less parameters</li> </ol>
	 */
	static class RequestMappingInfoComparator implements Comparator<RequestMappingInfo> {

		private final Comparator<String> pathComparator;

		RequestMappingInfoComparator(Comparator<String> pathComparator) {
			this.pathComparator = pathComparator;
		}

		public int compare(RequestMappingInfo info1, RequestMappingInfo info2) {
			int pathComparison = pathComparator.compare(info1.bestMatchedPath(), info2.bestMatchedPath());
			if (pathComparison != 0) {
				return pathComparison;
			}
			int info1ParamCount = info1.params.length;
			int info2ParamCount = info2.params.length;
			if (info1ParamCount != info2ParamCount) {
				return info2ParamCount - info1ParamCount;
			}
			int info1HeaderCount = info1.headers.length;
			int info2HeaderCount = info2.headers.length;
			if (info1HeaderCount != info2HeaderCount) {
				return info2HeaderCount - info1HeaderCount;
			}
			int info1MethodCount = info1.methods.length;
			int info2MethodCount = info2.methods.length;
			if (info1MethodCount == 0 && info2MethodCount > 0) {
				return 1;
			}
			else if (info2MethodCount == 0 && info1MethodCount > 0) {
				return -1;
			}
			else if (info1MethodCount == 1 & info2MethodCount > 1) {
				return -1;
			}
			else if (info2MethodCount == 1 & info1MethodCount > 1) {
				return 1;
			}
			return 0;
		}
	}

}
