/*
* Copyright 2002-2008 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.handler;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.BeansException;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.PathMatcher;
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.util.UrlPathHelper;
/**
* Abstract base class for URL-mapped {@link org.springframework.web.servlet.HandlerMapping}
* implementations. Provides infrastructure for mapping handlers to URLs and configurable
* URL lookup. For information on the latter, see "alwaysUseFullPath" property.
*
* <p>Supports direct matches, e.g. a registered "/test" matches "/test", and
* various Ant-style pattern matches, e.g. a registered "/t*" pattern matches
* both "/test" and "/team", "/test/*" matches all paths in the "/test" directory,
* "/test/**" matches all paths below "/test". For details, see the
* {@link org.springframework.util.AntPathMatcher AntPathMatcher} javadoc.
*
* <p>Will search all path patterns to find the most exact match for the
* current request path. The most exact match is defined as the longest
* path pattern that matches the current request path.
*
* @author Juergen Hoeller
* @since 16.04.2003
* @see #setAlwaysUseFullPath
* @see #setUrlDecode
* @see org.springframework.util.AntPathMatcher
*/
public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping {
private UrlPathHelper urlPathHelper = new UrlPathHelper();
private PathMatcher pathMatcher = new AntPathMatcher();
private Object rootHandler;
private boolean lazyInitHandlers = false;
private final Map handlerMap = new LinkedHashMap();
/**
* 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 MethodNameResolvers.
* @see org.springframework.web.servlet.mvc.multiaction.AbstractUrlMethodNameResolver#setUrlPathHelper
*/
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;
}
/**
* Return the PathMatcher implementation to use for matching URL paths
* against registered URL patterns.
*/
public PathMatcher getPathMatcher() {
return this.pathMatcher;
}
/**
* Set the root handler for this handler mapping, that is,
* the handler to be registered for the root path ("/").
* <p>Default is <code>null</code>, indicating no root handler.
*/
public void setRootHandler(Object rootHandler) {
this.rootHandler = rootHandler;
}
/**
* Return the root handler for this handler mapping (registered for "/"),
* or <code>null</code> if none.
*/
public Object getRootHandler() {
return this.rootHandler;
}
/**
* Set whether to lazily initialize handlers. Only applicable to
* singleton handlers, as prototypes are always lazily initialized.
* Default is "false", as eager initialization allows for more efficiency
* through referencing the controller objects directly.
* <p>If you want to allow your controllers to be lazily initialized,
* make them "lazy-init" and set this flag to true. Just making them
* "lazy-init" will not work, as they are initialized through the
* references from the handler mapping in this case.
*/
public void setLazyInitHandlers(boolean lazyInitHandlers) {
this.lazyInitHandlers = lazyInitHandlers;
}
/**
* Look up a handler for the URL path of the given request.
* @param request current HTTP request
* @return the handler instance, or <code>null</code> if none found
*/
protected Object getHandlerInternal(HttpServletRequest request) throws Exception {
String lookupPath = this.urlPathHelper.getLookupPathForRequest(request);
Object handler = lookupHandler(lookupPath, request);
if (handler == null) {
// We need to care for the default handler directly, since we need to
// expose the PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE for it as well.
Object rawHandler = null;
if ("/".equals(lookupPath)) {
rawHandler = getRootHandler();
}
if (rawHandler == null) {
rawHandler = getDefaultHandler();
}
if (rawHandler != null) {
validateHandler(rawHandler, request);
handler = buildPathExposingHandler(rawHandler, lookupPath);
}
}
if (handler != null && logger.isDebugEnabled()) {
logger.debug("Mapping [" + lookupPath + "] to handler '" + handler + "'");
}
else if (handler == null && logger.isTraceEnabled()) {
logger.trace("No handler mapping found for [" + lookupPath + "]");
}
return handler;
}
/**
* Look up a handler instance for the given URL path.
* <p>Supports direct matches, e.g. a registered "/test" matches "/test",
* and various Ant-style pattern matches, e.g. a registered "/t*" matches
* both "/test" and "/team". For details, see the AntPathMatcher class.
* <p>Looks for the most exact pattern, where most exact is defined as
* the longest path pattern.
* @param urlPath URL the bean is mapped to
* @param request current HTTP request (to expose the path within the mapping to)
* @return the associated handler instance, or <code>null</code> if not found
* @see #exposePathWithinMapping
* @see org.springframework.util.AntPathMatcher
*/
protected Object lookupHandler(String urlPath, HttpServletRequest request) throws Exception {
// Direct match?
Object handler = this.handlerMap.get(urlPath);
if (handler != null) {
validateHandler(handler, request);
return buildPathExposingHandler(handler, urlPath);
}
// Pattern match?
String bestPathMatch = null;
for (Iterator it = this.handlerMap.keySet().iterator(); it.hasNext();) {
String registeredPath = (String) it.next();
if (getPathMatcher().match(registeredPath, urlPath) &&
(bestPathMatch == null || bestPathMatch.length() < registeredPath.length())) {
bestPathMatch = registeredPath;
}
}
if (bestPathMatch != null) {
handler = this.handlerMap.get(bestPathMatch);
validateHandler(handler, request);
String pathWithinMapping = getPathMatcher().extractPathWithinPattern(bestPathMatch, urlPath);
return buildPathExposingHandler(handler, pathWithinMapping);
}
// No handler found...
return null;
}
/**
* Validate the given handler against the current request.
* <p>The default implementation is empty. Can be overridden in subclasses,
* for example to enforce specific preconditions expressed in URL mappings.
* @param handler the handler object to validate
* @param request current HTTP request
* @throws Exception if validation failed
*/
protected void validateHandler(Object handler, HttpServletRequest request) throws Exception {
}
/**
* Build a handler object for the given raw handler, exposing the actual
* handler as well as the {@link #PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE}
* before executing the handler.
* <p>The default implementation builds a {@link HandlerExecutionChain}
* with a special interceptor that exposes the path attribute.
* @param rawHandler the raw handler to expose
* @param pathWithinMapping the path to expose before executing the handler
* @return the final handler object
*/
protected Object buildPathExposingHandler(Object rawHandler, String pathWithinMapping) {
// Bean name or resolved handler?
if (rawHandler instanceof String) {
String handlerName = (String) rawHandler;
rawHandler = getApplicationContext().getBean(handlerName);
}
HandlerExecutionChain chain = new HandlerExecutionChain(rawHandler);
chain.addInterceptor(new PathExposingHandlerInterceptor(pathWithinMapping));
return chain;
}
/**
* Expose the path within the current mapping as request attribute.
* @param pathWithinMapping the path within the current mapping
* @param request the request to expose the path to
* @see #PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE
*/
protected void exposePathWithinMapping(String pathWithinMapping, HttpServletRequest request) {
request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, pathWithinMapping);
}
/**
* Register the specified handler for the given URL paths.
* @param urlPaths the URLs that the bean should be mapped to
* @param beanName the name of the handler bean
* @throws BeansException if the handler couldn't be registered
* @throws IllegalStateException if there is a conflicting handler registered
*/
protected void registerHandler(String[] urlPaths, String beanName) throws BeansException, IllegalStateException {
Assert.notNull(urlPaths, "URL path array must not be null");
for (int j = 0; j < urlPaths.length; j++) {
registerHandler(urlPaths[j], beanName);
}
}
/**
* Register the specified handler for the given URL path.
* @param urlPath the URL the bean should be mapped to
* @param handler the handler instance or handler bean name String
* (a bean name will automatically be resolved into the corresponding handler bean)
* @throws BeansException if the handler couldn't be registered
* @throws IllegalStateException if there is a conflicting handler registered
*/
protected void registerHandler(String urlPath, Object handler) throws BeansException, IllegalStateException {
Assert.notNull(urlPath, "URL path must not be null");
Assert.notNull(handler, "Handler object must not be null");
Object resolvedHandler = handler;
// Eagerly resolve handler if referencing singleton via name.
if (!this.lazyInitHandlers && handler instanceof String) {
String handlerName = (String) handler;
if (getApplicationContext().isSingleton(handlerName)) {
resolvedHandler = getApplicationContext().getBean(handlerName);
}
}
Object mappedHandler = this.handlerMap.get(urlPath);
if (mappedHandler != null) {
if (mappedHandler != resolvedHandler) {
throw new IllegalStateException(
"Cannot map handler [" + handler + "] to URL path [" + urlPath +
"]: There is already handler [" + resolvedHandler + "] mapped.");
}
}
else {
if (urlPath.equals("/")) {
if (logger.isDebugEnabled()) {
logger.debug("Root mapping to handler [" + resolvedHandler + "]");
}
setRootHandler(resolvedHandler);
}
else if (urlPath.equals("/*")) {
if (logger.isDebugEnabled()) {
logger.debug("Default mapping to handler [" + resolvedHandler + "]");
}
setDefaultHandler(resolvedHandler);
}
else {
this.handlerMap.put(urlPath, resolvedHandler);
if (logger.isDebugEnabled()) {
logger.debug("Mapped URL path [" + urlPath + "] onto handler [" + resolvedHandler + "]");
}
}
}
}
/**
* Return the registered handlers as an unmodifiable Map, with the registered path
* as key and the handler object (or handler bean name in case of a lazy-init handler)
* as value.
* @see #getDefaultHandler()
*/
public final Map getHandlerMap() {
return Collections.unmodifiableMap(this.handlerMap);
}
/**
* Special interceptor for exposing the
* {@link AbstractUrlHandlerMapping#PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE} attribute.
* @link AbstractUrlHandlerMapping#exposePathWithinMapping
*/
private class PathExposingHandlerInterceptor extends HandlerInterceptorAdapter {
private final String pathWithinMapping;
public PathExposingHandlerInterceptor(String pathWithinMapping) {
this.pathWithinMapping = pathWithinMapping;
}
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
exposePathWithinMapping(this.pathWithinMapping, request);
return true;
}
}
}