Open Source Repository

Home /spring/spring-web-3.0.5 | Repository Home



org/springframework/web/util/UrlPathHelper.java
/*
 * 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 = (Stringrequest.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 = (Stringrequest.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 = (Stringrequest.getAttribute(WebUtils.INCLUDE_SERVLET_PATH_ATTRIBUTE);
    if (servletPath == null) {
      servletPath = request.getServletPath();
    }
    if (servletPath.length() && 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 = (Stringrequest.getAttribute(WEBSPHERE_URI_ATTRIBUTE);
    if (uri == null) {
      uri = (Stringrequest.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 = (Stringrequest.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 = (Stringrequest.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 != -? 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 = (Propertiescl.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;
  }

}