/*
 * Copyright (C) 2002-2004 Red Hat Inc. All Rights Reserved.
 *
 * The contents of this file are subject to the CCM Public
 * License (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.redhat.com/licenses/ccmpl.html.
 *
 * Software distributed under the License is distributed on an
 * "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express
 * or implied. See the License for the specific language
 * governing rights and limitations under the License.
 *
 */
package com.arsdigita.web;

import com.arsdigita.dispatcher.DispatcherHelper;
import com.arsdigita.kernel.security.Util;
import com.arsdigita.util.Assert;
import com.arsdigita.util.servlet.HttpHost;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.apache.log4j.Logger;

/**
 * <p>URL models a future request according to the servlet worldview.
 * Its principal uses are two:
 *
 *   <ul>
 *     <li>To expose all the parts of a URL.  To a servlet's way of
 *     thinking, these are the scheme, server name, server port,
 *     context path, servlet path, path info, and parameters.</li>
 *
 *     <li>To generate URLs in a consistent and complete way in one
 *     place.</li>
 *   </ul>
 * </p>
 *
 * <p>Each URL has the following accessors, here set next to an
 * example URL instance,
 * <code>http://example.com:8080/ccmapp/forum/index.jsp?cat=2&cat=5</code>:</p>
 *
 * <p>Atomic parts:
 *
 * <pre><blockquote>
 *               getScheme() -> "http"
 *           getServerName() -> "example.com"
 *           getServerPort() -> 8080
 *          getContextPath() -> "/ccmapp"
 *          getServletPath() -> "/forum"
 *             getPathInfo() -> "/index.jsp"
 *       getParameter("cat") -> "2"
 * getParameterValues("cat") -> {"2", "5"}
 * </blockquote></pre>
 *
 * </p>
 *
 * <p>Composite parts:
 *
 * <pre><blockquote>
 *                toString() -> "/ccmapp/forum/index.jsp?cat=2&cat=5"
 *                  getURL() -> "http://example.com:8080/ccmapp/forum/index.jsp?cat=2&cat=5
 *            getServerURI() -> "http://example.com:8080"   // No trailing "/"
 *           getRequestURI() -> "/ccmapp/forum/index.jsp"
 *          getQueryString() -> "cat=2&cat=5"               // No leading "?"
 *         getParameterMap() -> {cat={"2", "5"}}
 * </blockquote></pre>
 *
 * </p>
 *
 * <p>The <code>toString()</code> method returns a URL suitable for
 * use in hyperlinks; since in the common case, the scheme, server
 * name, and port are best left off, <code>toString()</code> omits
 * them.  The <code>getURL()</code> method returns a
 * <code>String</code> URL which is fully qualified.  Both
 * <code>getURL()</code> and <code>getServerURI()</code> omit the port
 * from their return values if the server port is the default, port
 * 80.</p>
 *
 * <p>Creating URLs will usually be done via one of the static create
 * methods:</p>
 *
 * <p><code>URL.root()</code> creates a URL pointing at the server's
 * root path, "/".</p>
 *
 * <p><code>URL.request(req, params)</code> creates a URL reflecting
 * the request the client made but using the passed-in parameters
 * instead.</p>
 *
 * <p><code>URL.there(req, path, params)</code> and its variants
 * produce URLs that go through the CCM main dispatcher.  The variant
 * <code>URL.there(req, app, pathInfo, params)</code> dispatches to
 * <code>pathInfo</code> under the specified application.  The variant
 * <code>URL.here(req, pathInfo, params)</code> dispatches to
 * <code>pathInfo</code> under the current application.</p>
 *
 * <p><code>URL.excursion(req, path, params)</code> produces URLs that
 * go through the dispatcher to a destination but also encode and
 * store the origin.  This is used by <code>LoginSignal</code> and
 * <code>ReturnSignal</code> to implement UI excursions.</p>
 *
 * <p>All static create methods taking an
 * <code>HttpServletRequest</code> (1) preserve the request's scheme,
 * server name, and port and (2) run parameter listeners if the URL's
 * parameter map is not null.</p>
 *
 * <p>Those methods not taking an <code>HttpServletRequest</code> use
 * the scheme, server name, and port defined in
 * <code>WebConfig</code>.</p>
 *
 * <p>All static create methods taking a <code>ParameterMap</code>
 * take null to mean no query string at all.  URLs defined this way
 * will have no query string and no "?".</p>
 *
 * <p>Those methods not taking a <code>ParameterMap</code> argument implicitly
 * create an empty parameter map.  Note that this is different from
 * creating a URL with a null parameter map, which produces a URL with
 * no query string.</p>
 *
 * @see com.arsdigita.web.ParameterMap
 * @see com.arsdigita.web.DispatcherServlet
 * @see com.arsdigita.web.LoginSignal
 * @see com.arsdigita.web.ReturnSignal
 * @see com.arsdigita.web.WebConfig
 * @see com.arsdigita.web.Application
 * @author Justin Ross &lt;<a href="mailto:jross@redhat.com">jross@redhat.com</a>&gt;
 * @version $Id: //core-platform/dev/src/com/arsdigita/web/URL.java#33 $
 */
public class URL {
    public static final String versionId =
        "$Id: //core-platform/dev/src/com/arsdigita/web/URL.java#33 $" +
        "$Author: dennis $" +
        "$DateTime: 2004/04/07 16:07:11 $";

    private static final Logger s_log = Logger.getLogger(URL.class);

    private static final String INTERNAL_DIR = "/__ccm__";

    /**
     * The standard location for JSP files.
     */
    public static final String JSP_DIR = INTERNAL_DIR + "/jsp";

    /**
     * The standard location for static files.
     */
    public static final String STATIC_DIR = INTERNAL_DIR + "/static";

    /**
     * The standard location for XSL files.
     */
    public static final String XSL_DIR = INTERNAL_DIR + "/xsl";

    /**
     * The standard location for servlets.
     */
    public static final String SERVLET_DIR = INTERNAL_DIR + "/servlet";

    private static final ThreadLocal s_empty = new EmptyParameterMap();

    private StringBuffer m_url;
    private ParameterMap m_params;

    private int m_schemeEnd = -1;
    private int m_serverNameEnd = -1;
    private int m_serverPortEnd = -1;
    private int m_contextPathEnd = -1;
    private int m_servletPathEnd = -1;
    private int m_dispatcherPrefixEnd = -1;

    private void init(final String scheme,
                      final String serverName,
                      final int serverPort,
                      final String contextPath,
                      final String servletPath,
                      final String dispatcherPrefix,
                      final String pathInfo,
                      final ParameterMap params) {
        m_url = new StringBuffer(96);
        m_params = params;

        if (Assert.isAssertEnabled()) {
            Assert.assertNotNull(scheme, "String scheme");
            Assert.assertTrue(!scheme.equals(""),
                              "The scheme cannot be an empty string");

            Assert.assertNotNull(serverName, "String serverName");
            Assert.assertTrue(serverPort > 0,
                              "The serverPort must be greater than 0; " +
                              "I got " + serverPort);

            Assert.assertNotNull(contextPath, "String contextPath");

            if (contextPath.startsWith("/")) {
                Assert.assertTrue
                    (!contextPath.endsWith("/"),
                     "A contextPath starting with '/' must not end in '/'; " +
                     "I got '" + contextPath + "'");
            }

            Assert.assertNotNull(servletPath, "String servletPath");

            if (pathInfo != null) {
                Assert.assertTrue(pathInfo.startsWith("/"),
                                  "I expected a pathInfo starting with '/' " +
                                  "and got '" + pathInfo + "' instead");
            }
        }

        m_url.append(scheme);
        m_schemeEnd = m_url.length();

        m_url.append("://");

        m_url.append(serverName);
        m_serverNameEnd = m_url.length();

        if (serverPort != 80) {
            m_url.append(':');
            m_url.append(serverPort);
        }

        m_serverPortEnd = m_url.length();

        m_url.append(contextPath);
        m_contextPathEnd = m_url.length();

        if (dispatcherPrefix != null) {
            m_url.append(dispatcherPrefix);
        }

        m_dispatcherPrefixEnd = m_url.length();

        m_url.append(servletPath);
        m_servletPathEnd = m_url.length();

        if (pathInfo != null) {
            m_url.append(pathInfo);
        }

        if (Assert.isAssertEnabled()) {
            Assert.assertTrue(m_schemeEnd > -1);
            Assert.assertTrue(m_serverNameEnd > -1);
            Assert.assertTrue(m_serverPortEnd > -1);
            Assert.assertTrue(m_contextPathEnd > -1);
            Assert.assertTrue(m_servletPathEnd > -1);
        }
    }

    /**
     * <p>Assembles a fully qualified URL from its fundamental pieces.
     * The contract of URL dictates that once <code>params</code> is
     * passed in to this constructor, no parameters should be added or
     * removed.  This is to make <code>URL</code> in practice a
     * read-only object.</p>
     *
     * @param scheme <code>"http"</code>, for example; see {@link
     * javax.servlet.ServletRequest#getScheme()}
     *
     * @param serverName a valid domain name, for example
     * <code>"ccm.redhat.com"</code>; see {@link
     * javax.servlet.ServletRequest#getServerName()}
     *
     * @param serverPort <code>8080</code>, for instance; see {@link
     * javax.servlet.ServletRequest#getServerPort()}
     *
     * @param contextPath the path to your web app; empty string
     * indicates the default context; any other values for contextPath
     * must start with <code>"/"</code> but not end in
     * <code>"/"</code>; contextPath cannot be null; see {@link
     * javax.servlet.http.HttpServletRequest#getContextPath()}
     *
     * @param servletPath the path to your servlet; empty string and
     * values starting with <code>"/"</code> are valid, but null is
     * not; see {@link
     * javax.servlet.http.HttpServletRequest#getServletPath()}
     *
     * @param pathInfo the path data remaining after the servlet path
     * but before the query string; pathInfo may be null; see {@link
     * javax.servlet.http.HttpServletRequest#getPathInfo()}
     *
     * @param params a <code>ParameterMap</code> representing a set of
     * query parameters
     *
     * @return a fully specified URL
     */
    public URL(final String scheme,
               final String serverName,
               final int serverPort,
               final String contextPath,
               final String servletPath,
               final String pathInfo,
               final ParameterMap params) {
        HttpServletRequest req = Web.getRequest();
        String dispatcherPrefix = req ==  null ? null :
            DispatcherHelper.getDispatcherPrefix(req);

        init(scheme,
             serverName,
             serverPort,
             contextPath,
             servletPath,
             dispatcherPrefix,
             pathInfo,
             params);
    }

    private URL(final HttpServletRequest sreq,
                final ParameterMap params) {
        final String dispatcherPrefix =
            DispatcherHelper.getDispatcherPrefix(sreq);
        final HttpHost host = new HttpHost(sreq);

        init(sreq.getScheme(),
             host.getName(),
             host.getPort(),
             sreq.getContextPath(),
             sreq.getServletPath(),
             dispatcherPrefix,
             sreq.getPathInfo(),
             params);
    }

    /**
     * <p>Produce a URL representation of the given request.</p>
     *
     * @param sreq an <code>HttpServletRequest</code> from which to copy
     *
     * @return a URL whose contents correspond to the request used to
     * create it
     */
    public URL(final HttpServletRequest sreq) {
        this(sreq, new ParameterMap(sreq));
    }

    /**
     * <p>Produces a short description of a URL suitable for
     * debugging.</p>
     *
     * @return a debugging representation of this URL
     */
    public final String toDebugString() {
        return super.toString() + " " +
            "[" +
            getScheme() + "," +
            getServerName() + "," +
            getServerPort() + "," +
            getContextPath() + "," +
            getServletPath() + "," +
            getDispatcherPrefix() + "," +
            getPathInfo() + "," +
            getQueryString() +
            "]";
    }

    /**
     * Returns a <code>String</code> representation of the URL, fully
     * qualified.  The port is omitted if it is the standard HTTP
     * port, 80.
     *
     * @return a <code>String</code> URL, with all of its parts
     */
    public final String getURL() {
        if (m_params == null) {
            return m_url.toString();
        } else {
            return m_url.toString() + m_params;
        }
    }

    /**
     * <p>Returns the scheme (sometimes called the protocol) of the
     * URL.  Examples are <code>"http"</code> and
     * <code>"https"</code>.</p>
     *
     * @see javax.servlet.ServletRequest#getScheme()
     * @return a <code>String</code> representing the URL's scheme
     */
    public final String getScheme() {
        return m_url.substring(0, m_schemeEnd);
    }

    /**
     * <p>Returns the domain name part of the URL.  For instance,
     * <code>"ccm.redhat.com"</code>.</p>
     *
     * @see javax.servlet.ServletRequest#getServerName()
     * @return a <code>String</code> representing the URL's server
     * name
     */
    public final String getServerName() {
        return m_url.substring(m_schemeEnd + 3, m_serverNameEnd);
    }

    /**
     * <p>Returns the port number of the URL.  <code>8080</code>, for
     * example.</p>
     *
     * @see javax.servlet.ServletRequest#getServerPort()
     * @return an <code>int</code> for the URL's port number
     */
    public final int getServerPort() {
        final String port = m_url.substring(m_serverNameEnd, m_serverPortEnd);

        if (port.equals("")) {
            return 80;
        } else {
            return Integer.parseInt(port.substring(1));
        }
    }

    /**
     * <p>Returns the server half of the URL, as opposed to the "file"
     * half.  For example, "http://ccm.redhat.com:8080".  Note that there
     * is no trailing slash; any characters following the server port
     * are considered part of the {@link #getRequestURI() request
     * URI}.</p>
     *
     * <p>This method has no equivalent in the Servlet API, but it is
     * similar in spirit to {@link
     * javax.servlet.http.HttpServletRequest#getRequestURI()}.</p>
     *
     * <p>It is defined to return
     *
     *   <blockquote><code>getScheme() + "://" + getServerName() + ":"
     *   + getServerPort()</code></blockquote>
     *
     * or, if the server port is 80,
     *
     *   <blockquote><code>getScheme() + "://" +
     *   getServerName()</code></blockquote>
     *
     * </p>
     *
     * @see #getRequestURI()
     * @return a <code>String</code> comprised of the scheme, server
     * name, and server port plus connecting bits
     */
    public final String getServerURI() {
        return m_url.substring(0, m_serverPortEnd);
    }

    /**
     * <p>Returns the context path of the URL.  The value cannot be
     * null, and values starting with <code>"/"</code> do not end in
     * <code>"/"</code>; empty string is a valid return value that
     * stands for the default web app.  Example values are
     * <code>""</code> and <code>"/ccm-app"</code>.</p>
     *
     * @see javax.servlet.http.HttpServletRequest#getContextPath()
     * @return a <code>String</code> path to a web app context
     */
    public final String getContextPath() {
        return m_url.substring(m_serverPortEnd, m_contextPathEnd);
    }

    /**
     * <p>Experimental</p>
     * <p>Returns the dispatcher prefix of this request as
     * set by the InternalPrefixerServlet
     */
    public final String getDispatcherPrefix() {
        if (m_dispatcherPrefixEnd < m_servletPathEnd) {
            //there is no dispatcher prefix
            return "";
        } else {
            return m_url.substring(m_servletPathEnd, m_dispatcherPrefixEnd);
        }
    }

    /**
     * <p>Returns the servlet path of the URL.  The value cannot be
     * null.</p>
     *
     * @see javax.servlet.http.HttpServletRequest#getServletPath()
     * @return a <code>String</code> path to a servlet
     */
    public final String getServletPath() {
        return m_url.substring(m_dispatcherPrefixEnd, m_servletPathEnd);
    }

    /**
     * <p>Returns the servlet-local path data of the URL.  The value
     * may be null.  If it is not null, the value begins with a "/".
     * Examples are <code>null</code>, <code>"/"</code>, and
     * <code>"/remove.jsp"</code>.</p>
     *
     * @see javax.servlet.http.HttpServletRequest#getPathInfo()
     * @return a <code>String</code> of path data addressed to a
     * servlet
     */
    public final String getPathInfo() {
        final String pathInfo = m_url.substring(m_servletPathEnd);

        if (pathInfo.equals("")) {
            return null;
        } else {
            return pathInfo;
        }
    }

    /**
     * <p>Returns the "file" part of the URL, in contrast to the
     * {@link #getServerURI() server part}.  The value cannot be null
     * and always starts with a "/".  For example,
     * <code>"/ccm/forum/thread.jsp"</code>.</p>
     *
     * <p>This method is defined to return the equivalent of
     * <code>getContextPath() + getServletPath() +
     * getPathInfo()</code>.</p>
     *
     * @see javax.servlet.http.HttpServletRequest#getRequestURI()
     * @return a <code>String</code> comprised of the context path,
     * servlet path, and path info
     */
    public final String getRequestURI() {
        return m_url.substring(m_serverPortEnd);
    }

    /**
     * <p>Returns the query string of the URL.  If the URL was
     * constructed with a null <code>ParameterMap</code>, this method
     * returns null.  If the URL was constructed with an empty
     * <code>ParameterMap</code>, this method returns the empty
     * string.  Example values are <code>null</code>, <code>""</code>,
     * and <code>"ticket-id=56&amp;user-id=24"</code>.</p>
     *
     * @see javax.servlet.http.HttpServletRequest#getQueryString()
     * @return a <code>String</code> representing the query parameters
     * of the URL
     */
    public final String getQueryString() {
        if (m_params == null) {
            return null;
        } else {
            return m_params.getQueryString();
        }
    }

    /**
     * <p>Returns the value of one query parameter.  If the URL was
     * constructed with a null <code>ParameterMap</code>, this method
     * returns null.  If the parameter requested has multiple values,
     * this method will only return the first; use {@link
     * #getParameterValues(String)} to get all of the values.</p>
     *
     * @see javax.servlet.http.HttpServletRequest#getParameter(String)
     * @param name the name of the parameter to fetch
     * @return the <code>String</code> value of the parameter
     */
    public final String getParameter(final String name) {
        if (m_params == null) {
            return null;
        } else {
            return m_params.getParameter(name);
        }
    }

    /**
     * <p>Returns the values for a parameter.  If the URL was
     * constructed with a null <code>ParameterMap</code>, this method
     * returns null.</p>
     *
     * @see
     * javax.servlet.http.HttpServletRequest#getParameterValues(String)
     * @param name the name of the parameter to get
     * @return a <code>String[]</code> of values for the parameter
     */
    public final String[] getParameterValues(final String name) {
        if (m_params == null) {
            return null;
        } else {
            return m_params.getParameterValues(name);
        }
    }

    /**
     * <p>Returns an immutable map of the query parameters.  The map's
     * keys are <code>String</code>s and the map's values are
     * <code>String[]</code>s.  If the URL was constructed with a null
     * <code>ParameterMap</code>, this method returns null.</p>
     *
     * @see javax.servlet.http.HttpServletRequest#getParameterMap()
     * @return a <code>Map</code> of the URL's query parameters
     */
    public final Map getParameterMap() {
        if (m_params == null) {
            return null;
        } else {
            return m_params.getParameterMap();
        }
    }

    /**
     * <p>Creates a URL to the site's root path.  For example,
     * <code>http://somewhere.net/</code>.</p>
     *
     * @return a <code>URL</code> to your server's root path
     */
    public static final URL root() {
        final WebConfig config = Web.getConfig();

        URL url = new URL
            (config.getDefaultScheme(),
             config.getServer().getName(),
             config.getServer().getPort(),
             "",
             "/",
             null,
             null);

        return url;
    }

    /**
     * <p>Creates a URL using the elements of the user's original
     * request but with the given set of parameters instead of the
     * original ones.</p>
     *
     * @param sreq the servlet request
     * @param params a <code>ParameterMap</code> of params to replace
     * those of the request
     * @return a <code>URL</code> representing the original request
     * except for its parameters
     */
    public static final URL request(final HttpServletRequest sreq,
                                    final ParameterMap params) {
        if (params != null) {
            params.runListeners(sreq);
        }

        final URL url = Web.getContext().getRequestURL();

        if (url == null) {
            // If the URL is being generated outside of a WebContext,
            // use the request to fill out the URL.

            return new URL( sreq, params );
        } else {
            return new URL
                (url.getScheme(),
                 url.getServerName(),
                 url.getServerPort(),
                 url.getContextPath(),
                 url.getServletPath(),
                 url.getPathInfo(),
                 params);
        }
    }

    /**
     * <p>Creates a URL to <code>path</code> under the CCM main
     * dispatcher and with the given parameters.  A null
     * <code>ParameterMap</code> indicates that the URL has no query
     * string at all.  If the parameter map is not null, its parameter
     * listeners are run and may further edit the parameter map.</p>
     *
     * @see com.arsdigita.web.DispatcherServlet
     * @param sreq the servlet request
     * @param path a <code>String</code> path to which to dispatch
     * @param params a <code>ParameterMap</code> of parameters to use;
     * this value may be null
     * @return a <code>URL</code> with a path to dispatch to
     */
    public static final URL there(final HttpServletRequest sreq,
                                  final String path,
                                  final ParameterMap params) {
        final WebConfig config = Web.getConfig();

        Assert.assertNotNull(sreq, "HttpServletRequest sreq");
        Assert.assertNotNull(config, "WebConfig config");

        if (params != null) {
            params.runListeners(sreq);
        }

        final HttpHost host = new HttpHost(sreq);

        return new URL
            (sreq.getScheme(),
             host.getName(),
             host.getPort(),
             config.getDispatcherContextPath(),
             config.getDispatcherServletPath(),
             path,
             params);
    }

    /**
     * <p>Creates a URL with no local parameters to <code>path</code>
     * under the CCM main dispatcher.  This method implicitly creates
     * an empty parameter map (not a null one); this empty map may be
     * altered by parameter listeners, for instance to include global
     * parameters.</p>
     *
     * @param sreq the servlet request
     * @param path a <code>String</code> path to dispatch to
     * @return a <code>URL</code> to a path under the dispatcher and
     * with an empty parameter map
     */
    public static final URL there(final HttpServletRequest sreq,
                                  final String path) {
        final WebConfig config = Web.getConfig();

        Assert.assertNotNull(sreq, "HttpServletRequest sreq");
        Assert.assertNotNull(config, "WebConfig config");

        final HttpHost host = new HttpHost(sreq);

        return new URL
            (sreq.getScheme(),
             host.getName(),
             host.getPort(),
             config.getDispatcherContextPath(),
             config.getDispatcherServletPath(),
             path,
             (ParameterMap) s_empty.get());
    }

    /**
     * <p>Creates a URL to <code>pathInfo</code> under the specified
     * application and using the given parameters.  The parmeter map
     * argument may be null, indicating that the URL has no query
     * string.</p>
     *
     * @param sreq the servlet request
     * @param app the <code>Application</code> to dispatch to
     * @param pathInfo a <code>String</code> of extra path info for
     * the application
     * @param params a <code>ParameterMap</code> of parameters to use
     * @return a <code>URL</code> to an application with a particular
     * <code>pathInfo</code>
     */
    public static final URL there(final HttpServletRequest sreq,
                                  final Application app,
                                  final String pathInfo,
                                  final ParameterMap params) {
        if (Assert.isAssertEnabled() && pathInfo != null) {
            Assert.assertTrue(pathInfo.startsWith("/"),
                              "pathInfo, if not null, must " +
                              "start with a slash");
        }

        if (pathInfo == null) {
            return URL.there(sreq, app.getPath(), params);
        } else {
            return URL.there(sreq, app.getPath() + pathInfo, params);
        }
    }

    /**
     * <p>Creates a URL with no local parameters to
     * <code>pathInfo</code> under the specified application.
     *
     * @param sreq the servlet request
     * @param app the <code>Application</code> to dispatch to
     * @param pathInfo a <code>String</code> of extra path info for
     * the application
     * @return a <code>URL</code> to an application with a particular
     * <code>pathInfo</code>
     */
    public static final URL there(final HttpServletRequest sreq,
                                  final Application app,
                                  final String pathInfo) {
        if (Assert.isAssertEnabled() && pathInfo != null) {
            Assert.assertTrue(pathInfo.startsWith("/"),
                              "pathInfo, if not null, must " +
                              "start with a slash");
        }

        if (pathInfo == null) {
            return URL.there(sreq, app.getPath());
        } else {
            return URL.there(sreq, app.getPath() + pathInfo);
        }
    }

    /**
     * <p>Creates a URL with local parameters.</p>
     *
     * <p>This function <b>should not be used</b> unless you really don't have an
     * <code>HttpServletRequest</code> object as it will ignore any Host header
     * given by the client.</p>
     */
    public static final URL there(final String path,
                                  final ParameterMap params) {
        final WebConfig config = Web.getConfig();

        return new URL
            (config.getDefaultScheme(),
             config.getServer().getName(),
             config.getServer().getPort(),
             config.getDispatcherContextPath(),
             config.getDispatcherServletPath(),
             path,
             params);
    }

    /**
     * <p>Create a URL with local parameters to <code>pathInfo</code> under the
     * specified application.</p>
     *
     * <p>This function <b>should not be used</b> unless you really don't have an
     * <code>HttpServletRequest</code> object as it will ignore any Host header
     * given by the client.</p>
     */
    public static final URL there(final Application app,
                                  final String pathInfo,
                                  final ParameterMap params) {
        return URL.there(app.getPath() + pathInfo, params);
    }

    public static final URL here(final HttpServletRequest sreq,
                                 final String pathInfo,
                                 final ParameterMap params) {
        final Application app = Web.getContext().getApplication();

        Assert.assertNotNull(app, "Application app");

        return URL.there(sreq, app, pathInfo, params);
    }

    public static final URL here(final HttpServletRequest sreq,
                                 final String pathInfo) {
        final Application app = Web.getContext().getApplication();

        Assert.assertNotNull(app, "Application app");

        return URL.there(sreq, app, pathInfo);
    }

    public static final URL excursion(final HttpServletRequest sreq,
                                      final String path,
                                      final ParameterMap params) {
        if (s_log.isDebugEnabled()) {
            s_log.debug("Creating excursion URL to " + path);
        }

        final URL url = URL.there(sreq, path, params);

        params.setParameter("return_url", Web.getContext().getRequestURL());

        return url;
    }

    public static final URL excursion(final HttpServletRequest sreq,
                                      final String path) {
        return URL.excursion(sreq, path, new ParameterMap());
    }

    static final URL login(final HttpServletRequest sreq) {
        return URL.excursion(sreq, Util.getSecurityHelper().getLoginURL(sreq),
                             (ParameterMap) s_empty.get());
    }

    final String getReturnURL() {
        return getParameter("return_url");
    }

    /**
     * Returns a <code>String</code> representation of the URL
     * suitable for use as a hyperlink.  The scheme, server name, and
     * port are omitted.
     *
     * @return a <code>String</code> URL
     */
    public final String toString() {
        if (m_params == null) {
            return m_url.substring(m_serverPortEnd);
        } else {
            return m_url.substring(m_serverPortEnd) + m_params;
        }
    }

    public static final String getDispatcherPath() {
        final WebConfig config = Web.getConfig();
        final HttpServletRequest req = Web.getRequest();

        final String context = config.getDispatcherContextPath();
        final String servlet = config.getDispatcherServletPath();

        if (req == null) {
            return context + servlet;
        } else {
            final String prefix = DispatcherHelper.getDispatcherPrefix(req);

            if (prefix == null) {
                return context + servlet;
            } else {
                return context + prefix + servlet;
            }
        }
    }

    private static class EmptyParameterMap extends InternalRequestLocal {
        protected final Object initialValue() {
            return new ParameterMap();
        }

        protected final void prepareValue(final HttpServletRequest sreq) {
            ((ParameterMap) get()).runListeners(sreq);
        }

        protected final void clearValue() {
            ((ParameterMap) get()).clear();
        }
    }
}
