/*
* Copyright 2002-2010 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.util;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.Properties;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.util.StringUtils;
/**
* Helper class for URL path matching. Provides support for URL paths in
* RequestDispatcher includes and support for consistent URL decoding.
*
* <p>Used by {@link org.springframework.web.servlet.handler.AbstractUrlHandlerMapping},
* {@link org.springframework.web.servlet.mvc.multiaction.AbstractUrlMethodNameResolver}
* and {@link org.springframework.web.servlet.support.RequestContext} for path matching
* and/or URI determination.
*
* @author Juergen Hoeller
* @author Rob Harrop
* @since 14.01.2004
*/
public class UrlPathHelper {
/**
* Special WebSphere request attribute, indicating the original request URI.
* Preferable over the standard Servlet 2.4 forward attribute on WebSphere,
* simply because we need the very first URI in the request forwarding chain.
*/
private static final String WEBSPHERE_URI_ATTRIBUTE = "com.ibm.websphere.servlet.uri_non_decoded";
private static final Log logger = LogFactory.getLog(UrlPathHelper.class);
static volatile Boolean websphereComplianceFlag;
private boolean alwaysUseFullPath = false;
private boolean urlDecode = true;
private String defaultEncoding = WebUtils.DEFAULT_CHARACTER_ENCODING;
/**
* Set if URL lookup should always use full path within current servlet
* context. Else, the path within the current servlet mapping is used
* if applicable (i.e. in the case of a ".../*" servlet mapping in web.xml).
* Default is "false".
*/
public void setAlwaysUseFullPath(boolean alwaysUseFullPath) {
this.alwaysUseFullPath = 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).
* <p>Default is "true", as of Spring 2.5.
* @see #getServletPath
* @see #getContextPath
* @see #getRequestUri
* @see WebUtils#DEFAULT_CHARACTER_ENCODING
* @see javax.servlet.ServletRequest#getCharacterEncoding()
* @see java.net.URLDecoder#decode(String, String)
*/
public void setUrlDecode(boolean urlDecode) {
this.urlDecode = urlDecode;
}
/**
* Set the default character encoding to use for URL decoding.
* Default is ISO-8859-1, according to the Servlet spec.
* <p>If the request specifies a character encoding itself, the request
* encoding will override this setting. This also allows for generically
* overriding the character encoding in a filter that invokes the
* <code>ServletRequest.setCharacterEncoding</code> method.
* @param defaultEncoding the character encoding to use
* @see #determineEncoding
* @see javax.servlet.ServletRequest#getCharacterEncoding()
* @see javax.servlet.ServletRequest#setCharacterEncoding(String)
* @see WebUtils#DEFAULT_CHARACTER_ENCODING
*/
public void setDefaultEncoding(String defaultEncoding) {
this.defaultEncoding = defaultEncoding;
}
/**
* Return the default character encoding to use for URL decoding.
*/
protected String getDefaultEncoding() {
return this.defaultEncoding;
}
/**
* Return the mapping lookup path for the given request, within the current
* servlet mapping if applicable, else within the web application.
* <p>Detects include request URL if called within a RequestDispatcher include.
* @param request current HTTP request
* @return the lookup path
* @see #getPathWithinApplication
* @see #getPathWithinServletMapping
*/
public String getLookupPathForRequest(HttpServletRequest request) {
// Always use full path within current servlet context?
if (this.alwaysUseFullPath) {
return getPathWithinApplication(request);
}
// Else, use path within current servlet mapping if applicable
String rest = getPathWithinServletMapping(request);
if (!"".equals(rest)) {
return rest;
}
else {
return getPathWithinApplication(request);
}
}
/**
* Return the path within the servlet mapping for the given request,
* i.e. the part of the request's URL beyond the part that called the servlet,
* or "" if the whole URL has been used to identify the servlet.
* <p>Detects include request URL if called within a RequestDispatcher include.
* <p>E.g.: servlet mapping = "/test/*"; request URI = "/test/a" -> "/a".
* <p>E.g.: servlet mapping = "/test"; request URI = "/test" -> "".
* <p>E.g.: servlet mapping = "/*.test"; request URI = "/a.test" -> "".
* @param request current HTTP request
* @return the path within the servlet mapping, or ""
*/
public String getPathWithinServletMapping(HttpServletRequest request) {
String pathWithinApp = getPathWithinApplication(request);
String servletPath = getServletPath(request);
if (pathWithinApp.startsWith(servletPath)) {
// Normal case: URI contains servlet path.
return pathWithinApp.substring(servletPath.length());
}
else {
// Special case: URI is different from servlet path.
// Can happen e.g. with index page: URI="/", servletPath="/index.html"
// Use path info if available, as it indicates an index page within
// a servlet mapping. Otherwise, use the full servlet path.
String pathInfo = request.getPathInfo();
return (pathInfo != null ? pathInfo : servletPath);
}
}
/**
* Return the path within the web application for the given request.
* <p>Detects include request URL if called within a RequestDispatcher include.
* @param request current HTTP request
* @return the path within the web application
*/
public String getPathWithinApplication(HttpServletRequest request) {
String contextPath = getContextPath(request);
String requestUri = getRequestUri(request);
if (StringUtils.startsWithIgnoreCase(requestUri, contextPath)) {
// Normal case: URI contains context path.
String path = requestUri.substring(contextPath.length());
return (StringUtils.hasText(path) ? path : "/");
}
else {
// Special case: rather unusual.
return requestUri;
}
}
/**
* Return the request URI for the given request, detecting an include request
* URL if called within a RequestDispatcher include.
* <p>As the value returned by <code>request.getRequestURI()</code> is <i>not</i>
* decoded by the servlet container, this method will decode it.
* <p>The URI that the web container resolves <i>should</i> be correct, but some
* containers like JBoss/Jetty incorrectly include ";" strings like ";jsessionid"
* in the URI. This method cuts off such incorrect appendices.
* @param request current HTTP request
* @return the request URI
*/
public String getRequestUri(HttpServletRequest request) {
String uri = (String) request.getAttribute(WebUtils.INCLUDE_REQUEST_URI_ATTRIBUTE);
if (uri == null) {
uri = request.getRequestURI();
}
return decodeAndCleanUriString(request, uri);
}
/**
* Return the context path for the given request, detecting an include request
* URL if called within a RequestDispatcher include.
* <p>As the value returned by <code>request.getContextPath()</code> is <i>not</i>
* decoded by the servlet container, this method will decode it.
* @param request current HTTP request
* @return the context path
*/
public String getContextPath(HttpServletRequest request) {
String contextPath = (String) request.getAttribute(WebUtils.INCLUDE_CONTEXT_PATH_ATTRIBUTE);
if (contextPath == null) {
contextPath = request.getContextPath();
}
if ("/".equals(contextPath)) {
// Invalid case, but happens for includes on Jetty: silently adapt it.
contextPath = "";
}
return decodeRequestString(request, contextPath);
}
/**
* Return the servlet path for the given request, regarding an include request
* URL if called within a RequestDispatcher include.
* <p>As the value returned by <code>request.getServletPath()</code> is already
* decoded by the servlet container, this method will not attempt to decode it.
* @param request current HTTP request
* @return the servlet path
*/
public String getServletPath(HttpServletRequest request) {
String servletPath = (String) request.getAttribute(WebUtils.INCLUDE_SERVLET_PATH_ATTRIBUTE);
if (servletPath == null) {
servletPath = request.getServletPath();
}
if (servletPath.length() > 1 && servletPath.endsWith("/") &&
shouldRemoveTrailingServletPathSlash(request)) {
// On WebSphere, in non-compliant mode, for a "/foo/" case that would be "/foo"
// on all other servlet containers: removing trailing slash, proceeding with
// that remaining slash as final lookup path...
servletPath = servletPath.substring(0, servletPath.length() - 1);
}
return servletPath;
}
/**
* Return the request URI for the given request. If this is a forwarded request,
* correctly resolves to the request URI of the original request.
*/
public String getOriginatingRequestUri(HttpServletRequest request) {
String uri = (String) request.getAttribute(WEBSPHERE_URI_ATTRIBUTE);
if (uri == null) {
uri = (String) request.getAttribute(WebUtils.FORWARD_REQUEST_URI_ATTRIBUTE);
if (uri == null) {
uri = request.getRequestURI();
}
}
return decodeAndCleanUriString(request, uri);
}
/**
* Return the context path for the given request, detecting an include request
* URL if called within a RequestDispatcher include.
* <p>As the value returned by <code>request.getContextPath()</code> is <i>not</i>
* decoded by the servlet container, this method will decode it.
* @param request current HTTP request
* @return the context path
*/
public String getOriginatingContextPath(HttpServletRequest request) {
String contextPath = (String) request.getAttribute(WebUtils.FORWARD_CONTEXT_PATH_ATTRIBUTE);
if (contextPath == null) {
contextPath = request.getContextPath();
}
return decodeRequestString(request, contextPath);
}
/**
* Return the query string part of the given request's URL. If this is a forwarded request,
* correctly resolves to the query string of the original request.
* @param request current HTTP request
* @return the query string
*/
public String getOriginatingQueryString(HttpServletRequest request) {
String queryString = (String) request.getAttribute(WebUtils.FORWARD_QUERY_STRING_ATTRIBUTE);
if (queryString == null) {
queryString = request.getQueryString();
}
return queryString;
}
/**
* Decode the supplied URI string and strips any extraneous portion after a ';'.
*/
private String decodeAndCleanUriString(HttpServletRequest request, String uri) {
uri = decodeRequestString(request, uri);
int semicolonIndex = uri.indexOf(';');
return (semicolonIndex != -1 ? uri.substring(0, semicolonIndex) : uri);
}
/**
* Decode the given source string with a URLDecoder. The encoding will be taken
* from the request, falling back to the default "ISO-8859-1".
* <p>The default implementation uses <code>URLDecoder.decode(input, enc)</code>.
* @param request current HTTP request
* @param source the String to decode
* @return the decoded String
* @see WebUtils#DEFAULT_CHARACTER_ENCODING
* @see javax.servlet.ServletRequest#getCharacterEncoding
* @see java.net.URLDecoder#decode(String, String)
* @see java.net.URLDecoder#decode(String)
*/
public String decodeRequestString(HttpServletRequest request, String source) {
if (this.urlDecode) {
String enc = determineEncoding(request);
try {
return UriUtils.decode(source, enc);
}
catch (UnsupportedEncodingException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Could not decode request string [" + source + "] with encoding '" + enc +
"': falling back to platform default encoding; exception message: " + ex.getMessage());
}
return URLDecoder.decode(source);
}
}
return source;
}
/**
* Determine the encoding for the given request.
* Can be overridden in subclasses.
* <p>The default implementation checks the request encoding,
* falling back to the default encoding specified for this resolver.
* @param request current HTTP request
* @return the encoding for the request (never <code>null</code>)
* @see javax.servlet.ServletRequest#getCharacterEncoding()
* @see #setDefaultEncoding
*/
protected String determineEncoding(HttpServletRequest request) {
String enc = request.getCharacterEncoding();
if (enc == null) {
enc = getDefaultEncoding();
}
return enc;
}
private boolean shouldRemoveTrailingServletPathSlash(HttpServletRequest request) {
if (request.getAttribute(WEBSPHERE_URI_ATTRIBUTE) == null) {
// Regular servlet container: behaves as expected in any case,
// so the trailing slash is the result of a "/" url-pattern mapping.
// Don't remove that slash.
return false;
}
if (websphereComplianceFlag == null) {
ClassLoader classLoader = UrlPathHelper.class.getClassLoader();
String className = "com.ibm.ws.webcontainer.WebContainer";
String methodName = "getWebContainerProperties";
String propName = "com.ibm.ws.webcontainer.removetrailingservletpathslash";
boolean flag = false;
try {
Class<?> cl = classLoader.loadClass(className);
Properties prop = (Properties) cl.getMethod(methodName).invoke(null);
flag = Boolean.parseBoolean(prop.getProperty(propName));
}
catch (Throwable ex) {
if (logger.isDebugEnabled()) {
logger.debug("Could not introspect WebSphere web container properties: " + ex);
}
}
websphereComplianceFlag = flag;
}
// Don't bother if WebSphere is configured to be fully Servlet compliant.
// However, if it is not compliant, do remove the improper trailing slash!
return !websphereComplianceFlag;
}
}
|