/*
 * $Id: RequestUtils.java 560654 2007-07-29 01:54:02Z niallp $
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.struts.util;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.struts.Globals;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionMapping;
import org.apache.struts.action.ActionServlet;
import org.apache.struts.action.ActionServletWrapper;
import org.apache.struts.config.ActionConfig;
import org.apache.struts.config.FormBeanConfig;
import org.apache.struts.config.ForwardConfig;
import org.apache.struts.config.ModuleConfig;
import org.apache.struts.upload.FormFile;
import org.apache.struts.upload.MultipartRequestHandler;
import org.apache.struts.upload.MultipartRequestWrapper;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URL;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Locale;
import java.util.Map;

/**
 * <p>General purpose utility methods related to processing a servlet request
 * in the Struts controller framework.</p>
 *
 * @version $Rev: 560654 $ $Date: 2007-07-28 20:54:02 -0500 (Sat, 28 Jul 2007) $
 */
public class RequestUtils {
    // ------------------------------------------------------- Static Variables

    /**
     * <p>Commons Logging instance.</p>
     */
    protected static Log log = LogFactory.getLog(RequestUtils.class);

    // --------------------------------------------------------- Public Methods

    /**
     * <p>Create and return an absolute URL for the specified context-relative
     * path, based on the server and context information in the specified
     * request.</p>
     *
     * @param request The servlet request we are processing
     * @param path    The context-relative path (must start with '/')
     * @return absolute URL based on context-relative path
     * @throws MalformedURLException if we cannot create an absolute URL
     */
    public static URL absoluteURL(HttpServletRequest request, String path)
        throws MalformedURLException {
        return (new URL(serverURL(request), request.getContextPath() + path));
    }

    /**
     * <p>Return the <code>Class</code> object for the specified fully
     * qualified class name, from this web application's class loader.</p>
     *
     * @param className Fully qualified class name to be loaded
     * @return Class object
     * @throws ClassNotFoundException if the class cannot be found
     */
    public static Class applicationClass(String className)
        throws ClassNotFoundException {
        return applicationClass(className, null);
    }

    /**
     * <p>Return the <code>Class</code> object for the specified fully
     * qualified class name, from this web application's class loader.</p>
     *
     * @param className   Fully qualified class name to be loaded
     * @param classLoader The desired classloader to use
     * @return Class object
     * @throws ClassNotFoundException if the class cannot be found
     */
    public static Class applicationClass(String className,
        ClassLoader classLoader)
        throws ClassNotFoundException {
        if (classLoader == null) {
            // Look up the class loader to be used
            classLoader = Thread.currentThread().getContextClassLoader();

            if (classLoader == null) {
                classLoader = RequestUtils.class.getClassLoader();
            }
        }

        // Attempt to load the specified class
        return (classLoader.loadClass(className));
    }

    /**
     * <p>Return a new instance of the specified fully qualified class name,
     * after loading the class from this web application's class loader. The
     * specified class <strong>MUST</strong> have a public zero-arguments
     * constructor.</p>
     *
     * @param className Fully qualified class name to use
     * @return new instance of class
     * @throws ClassNotFoundException if the class cannot be found
     * @throws IllegalAccessException if the class or its constructor is not
     *                                accessible
     * @throws InstantiationException if this class represents an abstract
     *                                class, an interface, an array class, a
     *                                primitive type, or void
     * @throws InstantiationException if this class has no zero-arguments
     *                                constructor
     */
    public static Object applicationInstance(String className)
        throws ClassNotFoundException, IllegalAccessException,
            InstantiationException {
        return applicationInstance(className, null);
    }

    /**
     * <p>Return a new instance of the specified fully qualified class name,
     * after loading the class from this web application's class loader. The
     * specified class <strong>MUST</strong> have a public zero-arguments
     * constructor.</p>
     *
     * @param className   Fully qualified class name to use
     * @param classLoader The desired classloader to use
     * @return new instance of class
     * @throws ClassNotFoundException if the class cannot be found
     * @throws IllegalAccessException if the class or its constructor is not
     *                                accessible
     * @throws InstantiationException if this class represents an abstract
     *                                class, an interface, an array class, a
     *                                primitive type, or void
     * @throws InstantiationException if this class has no zero-arguments
     *                                constructor
     */
    public static Object applicationInstance(String className,
        ClassLoader classLoader)
        throws ClassNotFoundException, IllegalAccessException,
            InstantiationException {
        return (applicationClass(className, classLoader).newInstance());
    }

    /**
     * <p>Create (if necessary) and return an <code>ActionForm</code> instance
     * appropriate for this request.  If no <code>ActionForm</code> instance
     * is required, return <code>null</code>.</p>
     *
     * @param request      The servlet request we are processing
     * @param mapping      The action mapping for this request
     * @param moduleConfig The configuration for this module
     * @param servlet      The action servlet
     * @return ActionForm instance associated with this request
     */
    public static ActionForm createActionForm(HttpServletRequest request,
        ActionMapping mapping, ModuleConfig moduleConfig, ActionServlet servlet) {
        // Is there a form bean associated with this mapping?
        String attribute = mapping.getAttribute();

        if (attribute == null) {
            return (null);
        }

        // Look up the form bean configuration information to use
        String name = mapping.getName();
        FormBeanConfig config = moduleConfig.findFormBeanConfig(name);

        if (config == null) {
            log.warn("No FormBeanConfig found under '" + name + "'");

            return (null);
        }

        ActionForm instance =
            lookupActionForm(request, attribute, mapping.getScope());

        // Can we recycle the existing form bean instance (if there is one)?
        if ((instance != null) && config.canReuse(instance)) {
            return (instance);
        }

        return createActionForm(config, servlet);
    }

    private static ActionForm lookupActionForm(HttpServletRequest request,
        String attribute, String scope) {
        // Look up any existing form bean instance
        if (log.isDebugEnabled()) {
            log.debug(" Looking for ActionForm bean instance in scope '"
                + scope + "' under attribute key '" + attribute + "'");
        }

        ActionForm instance = null;
        HttpSession session = null;

        if ("request".equals(scope)) {
            instance = (ActionForm) request.getAttribute(attribute);
        } else {
            session = request.getSession();
            instance = (ActionForm) session.getAttribute(attribute);
        }

        return (instance);
    }

    /**
     * <p>Create and return an <code>ActionForm</code> instance appropriate to
     * the information in <code>config</code>.</p>
     *
     * <p>Does not perform any checks to see if an existing ActionForm exists
     * which could be reused.</p>
     *
     * @param config  The configuration for the Form bean which is to be
     *                created.
     * @param servlet The action servlet
     * @return ActionForm instance associated with this request
     */
    public static ActionForm createActionForm(FormBeanConfig config,
        ActionServlet servlet) {
        if (config == null) {
            return (null);
        }

        ActionForm instance = null;

        // Create and return a new form bean instance
        try {
            instance = config.createActionForm(servlet);

            if (log.isDebugEnabled()) {
                log.debug(" Creating new "
                    + (config.getDynamic() ? "DynaActionForm" : "ActionForm")
                    + " instance of type '" + config.getType() + "'");
                log.trace(" --> " + instance);
            }
        } catch (Throwable t) {
            log.error(servlet.getInternal().getMessage("formBean",
                    config.getType()), t);
        }

        return (instance);
    }

    /**
     * <p>Retrieves the servlet mapping pattern for the specified {@link ActionServlet}.</p>
     *
     * @return the servlet mapping
     * @see Globals#SERVLET_KEY
     * @since Struts 1.3.6
     */
    public static String getServletMapping(ActionServlet servlet) {
        ServletContext servletContext = servlet.getServletConfig().getServletContext();
        return (String)servletContext.getAttribute(Globals.SERVLET_KEY);
    }

    /**
     * <p>Look up and return current user locale, based on the specified
     * parameters.</p>
     *
     * @param request The request used to lookup the Locale
     * @param locale  Name of the session attribute for our user's Locale.  If
     *                this is <code>null</code>, the default locale key is
     *                used for the lookup.
     * @return current user locale
     * @since Struts 1.2
     */
    public static Locale getUserLocale(HttpServletRequest request, String locale) {
        Locale userLocale = null;
        HttpSession session = request.getSession(false);

        if (locale == null) {
            locale = Globals.LOCALE_KEY;
        }

        // Only check session if sessions are enabled
        if (session != null) {
            userLocale = (Locale) session.getAttribute(locale);
        }

        if (userLocale == null) {
            // Returns Locale based on Accept-Language header or the server default
            userLocale = request.getLocale();
        }

        return userLocale;
    }

    /**
     * <p>Populate the properties of the specified JavaBean from the specified
     * HTTP request, based on matching each parameter name against the
     * corresponding JavaBeans "property setter" methods in the bean's class.
     * Suitable conversion is done for argument types as described under
     * <code>convert()</code>.</p>
     *
     * @param bean    The JavaBean whose properties are to be set
     * @param request The HTTP request whose parameters are to be used to
     *                populate bean properties
     * @throws ServletException if an exception is thrown while setting
     *                          property values
     */
    public static void populate(Object bean, HttpServletRequest request)
        throws ServletException {
        populate(bean, null, null, request);
    }

    /**
     * <p>Populate the properties of the specified JavaBean from the specified
     * HTTP request, based on matching each parameter name (plus an optional
     * prefix and/or suffix) against the corresponding JavaBeans "property
     * setter" methods in the bean's class. Suitable conversion is done for
     * argument types as described under <code>setProperties</code>.</p>
     *
     * <p>If you specify a non-null <code>prefix</code> and a non-null
     * <code>suffix</code>, the parameter name must match
     * <strong>both</strong> conditions for its value(s) to be used in
     * populating bean properties. If the request's content type is
     * "multipart/form-data" and the method is "POST", the
     * <code>HttpServletRequest</code> object will be wrapped in a
     * <code>MultipartRequestWrapper</code object.</p>
     *
     * @param bean    The JavaBean whose properties are to be set
     * @param prefix  The prefix (if any) to be prepend to bean property names
     *                when looking for matching parameters
     * @param suffix  The suffix (if any) to be appended to bean property
     *                names when looking for matching parameters
     * @param request The HTTP request whose parameters are to be used to
     *                populate bean properties
     * @throws ServletException if an exception is thrown while setting
     *                          property values
     */
    public static void populate(Object bean, String prefix, String suffix,
        HttpServletRequest request)
        throws ServletException {
        // Build a list of relevant request parameters from this request
        HashMap properties = new HashMap();

        // Iterator of parameter names
        Enumeration names = null;

        // Map for multipart parameters
        Map multipartParameters = null;

        String contentType = request.getContentType();
        String method = request.getMethod();
        boolean isMultipart = false;

        if (bean instanceof ActionForm) {
            ((ActionForm) bean).setMultipartRequestHandler(null);
        }

        MultipartRequestHandler multipartHandler = null;
        if ((contentType != null)
            && (contentType.startsWith("multipart/form-data"))
            && (method.equalsIgnoreCase("POST"))) {
            // Get the ActionServletWrapper from the form bean
            ActionServletWrapper servlet;

            if (bean instanceof ActionForm) {
                servlet = ((ActionForm) bean).getServletWrapper();
            } else {
                throw new ServletException("bean that's supposed to be "
                    + "populated from a multipart request is not of type "
                    + "\"org.apache.struts.action.ActionForm\", but type "
                    + "\"" + bean.getClass().getName() + "\"");
            }

            // Obtain a MultipartRequestHandler
            multipartHandler = getMultipartHandler(request);

            if (multipartHandler != null) {
                isMultipart = true;

                // Set servlet and mapping info
                servlet.setServletFor(multipartHandler);
                multipartHandler.setMapping((ActionMapping) request
                    .getAttribute(Globals.MAPPING_KEY));

                // Initialize multipart request class handler
                multipartHandler.handleRequest(request);

                //stop here if the maximum length has been exceeded
                Boolean maxLengthExceeded =
                    (Boolean) request.getAttribute(MultipartRequestHandler.ATTRIBUTE_MAX_LENGTH_EXCEEDED);

                if ((maxLengthExceeded != null)
                    && (maxLengthExceeded.booleanValue())) {
                    ((ActionForm) bean).setMultipartRequestHandler(multipartHandler);
                    return;
                }

                //retrieve form values and put into properties
                multipartParameters =
                    getAllParametersForMultipartRequest(request,
                        multipartHandler);
                names = Collections.enumeration(multipartParameters.keySet());
            }
        }

        if (!isMultipart) {
            names = request.getParameterNames();
        }

        while (names.hasMoreElements()) {
            String name = (String) names.nextElement();
            String stripped = name;

            if (prefix != null) {
                if (!stripped.startsWith(prefix)) {
                    continue;
                }

                stripped = stripped.substring(prefix.length());
            }

            if (suffix != null) {
                if (!stripped.endsWith(suffix)) {
                    continue;
                }

                stripped =
                    stripped.substring(0, stripped.length() - suffix.length());
            }

            Object parameterValue = null;

            if (isMultipart) {
                parameterValue = multipartParameters.get(name);
                parameterValue = rationalizeMultipleFileProperty(bean, name, parameterValue);
            } else {
                parameterValue = request.getParameterValues(name);
            }

            // Populate parameters, except "standard" struts attributes
            // such as 'org.apache.struts.action.CANCEL'
            if (!(stripped.startsWith("org.apache.struts."))) {
                properties.put(stripped, parameterValue);
            }
        }

        // Set the corresponding properties of our bean
        try {
            BeanUtils.populate(bean, properties);
        } catch (Exception e) {
            throw new ServletException("BeanUtils.populate", e);
        } finally {
            if (multipartHandler != null) {
                // Set the multipart request handler for our ActionForm.
                // If the bean isn't an ActionForm, an exception would have been
                // thrown earlier, so it's safe to assume that our bean is
                // in fact an ActionForm.
                ((ActionForm) bean).setMultipartRequestHandler(multipartHandler);
            }
        }
    }

    /**
     * <p>If the given form bean can accept multiple FormFile objects but the user only uploaded a single, then 
     * the property will not match the form bean type.  This method performs some simple checks to try to accommodate
     * that situation.</p>
     * @param bean
     * @param name
     * @param parameterValue
     * @return 
     * @throws ServletException if the introspection has any errors.
     */
    private static Object rationalizeMultipleFileProperty(Object bean, String name, Object parameterValue) throws ServletException {
        if (!(parameterValue instanceof FormFile)) {
            return parameterValue;
        }

        FormFile formFileValue = (FormFile) parameterValue;
        try {
            Class propertyType = PropertyUtils.getPropertyType(bean, name);

            if (List.class.isAssignableFrom(propertyType)) {
                ArrayList list = new ArrayList(1);
                list.add(formFileValue);
                return list;
            }

            if (propertyType.isArray() && propertyType.getComponentType().equals(FormFile.class)) {
                return new FormFile[] { formFileValue };
            }

        } catch (IllegalAccessException e) {
            throw new ServletException(e);
        } catch (InvocationTargetException e) {
            throw new ServletException(e);
        } catch (NoSuchMethodException e) {
            throw new ServletException(e);
        }

        // no changes
        return parameterValue;

    }

    /**
     * <p>Try to locate a multipart request handler for this request. First,
     * look for a mapping-specific handler stored for us under an attribute.
     * If one is not present, use the global multipart handler, if there is
     * one.</p>
     *
     * @param request The HTTP request for which the multipart handler should
     *                be found.
     * @return the multipart handler to use, or null if none is found.
     * @throws ServletException if any exception is thrown while attempting to
     *                          locate the multipart handler.
     */
    private static MultipartRequestHandler getMultipartHandler(
        HttpServletRequest request)
        throws ServletException {
        MultipartRequestHandler multipartHandler = null;
        String multipartClass =
            (String) request.getAttribute(Globals.MULTIPART_KEY);

        request.removeAttribute(Globals.MULTIPART_KEY);

        // Try to initialize the mapping specific request handler
        if (multipartClass != null) {
            try {
                multipartHandler =
                    (MultipartRequestHandler) applicationInstance(multipartClass);
            } catch (ClassNotFoundException cnfe) {
                log.error("MultipartRequestHandler class \"" + multipartClass
                    + "\" in mapping class not found, "
                    + "defaulting to global multipart class");
            } catch (InstantiationException ie) {
                log.error("InstantiationException when instantiating "
                    + "MultipartRequestHandler \"" + multipartClass + "\", "
                    + "defaulting to global multipart class, exception: "
                    + ie.getMessage());
            } catch (IllegalAccessException iae) {
                log.error("IllegalAccessException when instantiating "
                    + "MultipartRequestHandler \"" + multipartClass + "\", "
                    + "defaulting to global multipart class, exception: "
                    + iae.getMessage());
            }

            if (multipartHandler != null) {
                return multipartHandler;
            }
        }

        ModuleConfig moduleConfig =
            ModuleUtils.getInstance().getModuleConfig(request);

        multipartClass = moduleConfig.getControllerConfig().getMultipartClass();

        // Try to initialize the global request handler
        if (multipartClass != null) {
            try {
                multipartHandler =
                    (MultipartRequestHandler) applicationInstance(multipartClass);
            } catch (ClassNotFoundException cnfe) {
                throw new ServletException("Cannot find multipart class \""
                    + multipartClass + "\"" + ", exception: "
                    + cnfe.getMessage());
            } catch (InstantiationException ie) {
                throw new ServletException(
                    "InstantiationException when instantiating "
                    + "multipart class \"" + multipartClass + "\", exception: "
                    + ie.getMessage());
            } catch (IllegalAccessException iae) {
                throw new ServletException(
                    "IllegalAccessException when instantiating "
                    + "multipart class \"" + multipartClass + "\", exception: "
                    + iae.getMessage());
            }

            if (multipartHandler != null) {
                return multipartHandler;
            }
        }

        return multipartHandler;
    }

    /**
     * <p>Create a <code>Map</code> containing all of the parameters supplied
     * for a multipart request, keyed by parameter name. In addition to text
     * and file elements from the multipart body, query string parameters are
     * included as well.</p>
     *
     * @param request          The (wrapped) HTTP request whose parameters are
     *                         to be added to the map.
     * @param multipartHandler The multipart handler used to parse the
     *                         request.
     * @return the map containing all parameters for this multipart request.
     */
    private static Map getAllParametersForMultipartRequest(
        HttpServletRequest request, MultipartRequestHandler multipartHandler) {
        Map parameters = new HashMap();
        Hashtable elements = multipartHandler.getAllElements();
        Enumeration e = elements.keys();

        while (e.hasMoreElements()) {
            String key = (String) e.nextElement();

            parameters.put(key, elements.get(key));
        }

        if (request instanceof MultipartRequestWrapper) {
            request =
                (HttpServletRequest) ((MultipartRequestWrapper) request)
                .getRequest();
            e = request.getParameterNames();

            while (e.hasMoreElements()) {
                String key = (String) e.nextElement();

                parameters.put(key, request.getParameterValues(key));
            }
        } else {
            log.debug("Gathering multipart parameters for unwrapped request");
        }

        return parameters;
    }

    /**
     * <p>Compute the printable representation of a URL, leaving off the
     * scheme/host/port part if no host is specified. This will typically be
     * the case for URLs that were originally created from relative or
     * context-relative URIs.</p>
     *
     * @param url URL to render in a printable representation
     * @return printable representation of a URL
     */
    public static String printableURL(URL url) {
        if (url.getHost() != null) {
            return (url.toString());
        }

        String file = url.getFile();
        String ref = url.getRef();

        if (ref == null) {
            return (file);
        } else {
            StringBuffer sb = new StringBuffer(file);

            sb.append('#');
            sb.append(ref);

            return (sb.toString());
        }
    }

    /**
     * <p>Return the context-relative URL that corresponds to the specified
     * {@link ActionConfig}, relative to the module associated with the
     * current modules's {@link ModuleConfig}.</p>
     *
     * @param request The servlet request we are processing
     * @param action  ActionConfig to be evaluated
     * @param pattern URL pattern used to map the controller servlet
     * @return context-relative URL relative to the module
     * @since Struts 1.1
     */
    public static String actionURL(HttpServletRequest request,
        ActionConfig action, String pattern) {
        StringBuffer sb = new StringBuffer();

        if (pattern.endsWith("/*")) {
            sb.append(pattern.substring(0, pattern.length() - 2));
            sb.append(action.getPath());
        } else if (pattern.startsWith("*.")) {
            ModuleConfig appConfig =
                ModuleUtils.getInstance().getModuleConfig(request);

            sb.append(appConfig.getPrefix());
            sb.append(action.getPath());
            sb.append(pattern.substring(1));
        } else {
            throw new IllegalArgumentException(pattern);
        }

        return sb.toString();
    }

    /**
     * <p>Return the context-relative URL that corresponds to the specified
     * <code>ForwardConfig</code>. The URL is calculated based on the
     * properties of the {@link ForwardConfig} instance as follows:</p>
     *
     * <ul>
     *
     *
     * <li>If the <code>contextRelative</code> property is set, it is assumed
     * that the <code>path</code> property contains a path that is already
     * context-relative:
     *
     * <ul>
     *
     * <li>If the <code>path</code> property value starts with a slash, it is
     * returned unmodified.</li> <li>If the <code>path</code> property value
     * does not start with a slash, a slash is prepended.</li>
     *
     * </ul></li>
     *
     * <li>Acquire the <code>forwardPattern</code> property from the
     * <code>ControllerConfig</code> for the application module used to
     * process this request. If no pattern was configured, default to a
     * pattern of <code>$M$P</code>, which is compatible with the hard-coded
     * mapping behavior in Struts 1.0.</li>
     *
     * <li>Process the acquired <code>forwardPattern</code>, performing the
     * following substitutions:
     *
     * <ul>
     *
     * <li><strong>$M</strong> - Replaced by the module prefix for the
     * application module processing this request.</li>
     *
     * <li><strong>$P</strong> - Replaced by the <code>path</code> property of
     * the specified {@link ForwardConfig}, prepended with a slash if it does
     * not start with one.</li>
     *
     * <li><strong>$$</strong> - Replaced by a single dollar sign
     * character.</li>
     *
     * <li><strong>$x</strong> (where "x" is any charater not listed above) -
     * Silently omit these two characters from the result value.  (This has
     * the side effect of causing all other $+letter combinations to be
     * reserved.)</li>
     *
     * </ul></li>
     *
     * </ul>
     *
     * @param request The servlet request we are processing
     * @param forward ForwardConfig to be evaluated
     * @return context-relative URL
     * @since Struts 1.1
     */
    public static String forwardURL(HttpServletRequest request,
        ForwardConfig forward) {
        return forwardURL(request, forward, null);
    }

    /**
     * <p>Return the context-relative URL that corresponds to the specified
     * <code>ForwardConfig</code>. The URL is calculated based on the
     * properties of the {@link ForwardConfig} instance as follows:</p>
     *
     * <ul>
     *
     * <li>If the <code>contextRelative</code> property is set, it is assumed
     * that the <code>path</code> property contains a path that is already
     * context-relative: <ul>
     *
     * <li>If the <code>path</code> property value starts with a slash, it is
     * returned unmodified.</li> <li>If the <code>path</code> property value
     * does not start with a slash, a slash is prepended.</li>
     *
     * </ul></li>
     *
     * <li>Acquire the <code>forwardPattern</code> property from the
     * <code>ControllerConfig</code> for the application module used to
     * process this request. If no pattern was configured, default to a
     * pattern of <code>$M$P</code>, which is compatible with the hard-coded
     * mapping behavior in Struts 1.0.</li>
     *
     * <li>Process the acquired <code>forwardPattern</code>, performing the
     * following substitutions: <ul> <li><strong>$M</strong> - Replaced by the
     * module prefix for the application module processing this request.</li>
     *
     * <li><strong>$P</strong> - Replaced by the <code>path</code> property of
     * the specified {@link ForwardConfig}, prepended with a slash if it does
     * not start with one.</li>
     *
     * <li><strong>$$</strong> - Replaced by a single dollar sign
     * character.</li>
     *
     * <li><strong>$x</strong> (where "x" is any charater not listed above) -
     * Silently omit these two characters from the result value.  (This has
     * the side effect of causing all other $+letter combinations to be
     * reserved.)</li>
     *
     * </ul></li></ul>
     *
     * @param request      The servlet request we are processing
     * @param forward      ForwardConfig to be evaluated
     * @param moduleConfig Base forward on this module config.
     * @return context-relative URL
     * @since Struts 1.2
     */
    public static String forwardURL(HttpServletRequest request,
        ForwardConfig forward, ModuleConfig moduleConfig) {
        //load the current moduleConfig, if null
        if (moduleConfig == null) {
            moduleConfig = ModuleUtils.getInstance().getModuleConfig(request);
        }

        String path = forward.getPath();

        //load default prefix
        String prefix = moduleConfig.getPrefix();

        //override prefix if supplied by forward
        if (forward.getModule() != null) {
            prefix = forward.getModule();

            if ("/".equals(prefix)) {
                prefix = "";
            }
        }

        StringBuffer sb = new StringBuffer();

        // Calculate a context relative path for this ForwardConfig
        String forwardPattern =
            moduleConfig.getControllerConfig().getForwardPattern();

        if (forwardPattern == null) {
            // Performance optimization for previous default behavior
            sb.append(prefix);

            // smoothly insert a '/' if needed
            if (!path.startsWith("/")) {
                sb.append("/");
            }

            sb.append(path);
        } else {
            boolean dollar = false;

            for (int i = 0; i < forwardPattern.length(); i++) {
                char ch = forwardPattern.charAt(i);

                if (dollar) {
                    switch (ch) {
                    case 'M':
                        sb.append(prefix);

                        break;

                    case 'P':

                        // add '/' if needed
                        if (!path.startsWith("/")) {
                            sb.append("/");
                        }

                        sb.append(path);

                        break;

                    case '$':
                        sb.append('$');

                        break;

                    default:
                        ; // Silently swallow
                    }

                    dollar = false;

                    continue;
                } else if (ch == '$') {
                    dollar = true;
                } else {
                    sb.append(ch);
                }
            }
        }

        return (sb.toString());
    }

    /**
     * <p>Return the URL representing the current request. This is equivalent
     * to <code>HttpServletRequest.getRequestURL</code> in Servlet 2.3.</p>
     *
     * @param request The servlet request we are processing
     * @return URL representing the current request
     * @throws MalformedURLException if a URL cannot be created
     */
    public static URL requestURL(HttpServletRequest request)
        throws MalformedURLException {
        StringBuffer url = requestToServerUriStringBuffer(request);

        return (new URL(url.toString()));
    }

    /**
     * <p>Return the URL representing the scheme, server, and port number of
     * the current request. Server-relative URLs can be created by simply
     * appending the server-relative path (starting with '/') to this.</p>
     *
     * @param request The servlet request we are processing
     * @return URL representing the scheme, server, and port number of the
     *         current request
     * @throws MalformedURLException if a URL cannot be created
     */
    public static URL serverURL(HttpServletRequest request)
        throws MalformedURLException {
        StringBuffer url = requestToServerStringBuffer(request);

        return (new URL(url.toString()));
    }

    /**
     * <p>Return the string representing the scheme, server, and port number
     * of the current request. Server-relative URLs can be created by simply
     * appending the server-relative path (starting with '/') to this.</p>
     *
     * @param request The servlet request we are processing
     * @return URL representing the scheme, server, and port number of the
     *         current request
     * @since Struts 1.2.0
     */
    public static StringBuffer requestToServerUriStringBuffer(
        HttpServletRequest request) {
        StringBuffer serverUri =
            createServerUriStringBuffer(request.getScheme(),
                request.getServerName(), request.getServerPort(),
                request.getRequestURI());

        return serverUri;
    }

    /**
     * <p>Return <code>StringBuffer</code> representing the scheme, server,
     * and port number of the current request. Server-relative URLs can be
     * created by simply appending the server-relative path (starting with
     * '/') to this.</p>
     *
     * @param request The servlet request we are processing
     * @return URL representing the scheme, server, and port number of the
     *         current request
     * @since Struts 1.2.0
     */
    public static StringBuffer requestToServerStringBuffer(
        HttpServletRequest request) {
        return createServerStringBuffer(request.getScheme(),
            request.getServerName(), request.getServerPort());
    }

    /**
     * <p>Return <code>StringBuffer</code> representing the scheme, server,
     * and port number of the current request.</p>
     *
     * @param scheme The scheme name to use
     * @param server The server name to use
     * @param port   The port value to use
     * @return StringBuffer in the form scheme: server: port
     * @since Struts 1.2.0
     */
    public static StringBuffer createServerStringBuffer(String scheme,
        String server, int port) {
        StringBuffer url = new StringBuffer();

        if (port < 0) {
            port = 80; // Work around java.net.URL bug
        }

        url.append(scheme);
        url.append("://");
        url.append(server);

        if ((scheme.equals("http") && (port != 80))
            || (scheme.equals("https") && (port != 443))) {
            url.append(':');
            url.append(port);
        }

        return url;
    }

    /**
     * <p>Return <code>StringBuffer</code> representing the scheme, server,
     * and port number of the current request.</p>
     *
     * @param scheme The scheme name to use
     * @param server The server name to use
     * @param port   The port value to use
     * @param uri    The uri value to use
     * @return StringBuffer in the form scheme: server: port
     * @since Struts 1.2.0
     */
    public static StringBuffer createServerUriStringBuffer(String scheme,
        String server, int port, String uri) {
        StringBuffer serverUri = createServerStringBuffer(scheme, server, port);

        serverUri.append(uri);

        return serverUri;
    }

    /**
     * <p>Returns the true path of the destination action if the specified forward
     * is an action-aliased URL. This method version forms the URL based on
     * the current request; selecting the current module if the forward does not
     * explicitly contain a module path.</p>
     *
     * @param forward the forward config
     * @param request the current request
     * @param servlet the servlet handling the current request
     * @return the context-relative URL of the action if the forward has an action identifier; otherwise <code>null</code>.
     * @since Struts 1.3.6
     */
    public static String actionIdURL(ForwardConfig forward, HttpServletRequest request, ActionServlet servlet) {
        ModuleConfig moduleConfig = null;
        if (forward.getModule() != null) {
            String prefix = forward.getModule();
            moduleConfig = ModuleUtils.getInstance().getModuleConfig(prefix, servlet.getServletContext());
        } else {
            moduleConfig = ModuleUtils.getInstance().getModuleConfig(request);
        }
        return actionIdURL(forward.getPath(), moduleConfig, servlet);
    }

    /**
     * <p>Returns the true path of the destination action if the specified forward
     * is an action-aliased URL. This method version forms the URL based on
     * the specified module.
     *
     * @param originalPath the action-aliased path
     * @param moduleConfig the module config for this request
     * @param servlet the servlet handling the current request
     * @return the context-relative URL of the action if the path has an action identifier; otherwise <code>null</code>.
     * @since Struts 1.3.6
     */
    public static String actionIdURL(String originalPath, ModuleConfig moduleConfig, ActionServlet servlet) {
        if (originalPath.startsWith("http") || originalPath.startsWith("/")) {
            return null;
        }

        // Split the forward path into the resource and query string;
        // it is possible a forward (or redirect) has added parameters.
        String actionId = null;
        String qs = null;
        int qpos = originalPath.indexOf("?");
        if (qpos == -1) {
            actionId = originalPath;
        } else {
            actionId = originalPath.substring(0, qpos);
            qs = originalPath.substring(qpos);
        }

        // Find the action of the given actionId
        ActionConfig actionConfig = moduleConfig.findActionConfigId(actionId);
        if (actionConfig == null) {
            if (log.isDebugEnabled()) {
                log.debug("No actionId found for " + actionId);
            }
            return null;
        }

        String path = actionConfig.getPath();
        String mapping = RequestUtils.getServletMapping(servlet);
        StringBuffer actionIdPath = new StringBuffer();

        // Form the path based on the servlet mapping pattern
        if (mapping.startsWith("*")) {
            actionIdPath.append(path);
            actionIdPath.append(mapping.substring(1));
        } else if (mapping.startsWith("/")) {  // implied ends with a *
            mapping = mapping.substring(0, mapping.length() - 1);
            if (mapping.endsWith("/") && path.startsWith("/")) {
                actionIdPath.append(mapping);
                actionIdPath.append(path.substring(1));
            } else {
                actionIdPath.append(mapping);
                actionIdPath.append(path);
            }
        } else {
            log.warn("Unknown servlet mapping pattern");
            actionIdPath.append(path);
        }

        // Lastly add any query parameters (the ? is part of the query string)
        if (qs != null) {
            actionIdPath.append(qs);
        }

        // Return the path
        if (log.isDebugEnabled()) {
            log.debug(originalPath + " unaliased to " + actionIdPath.toString());
        }
        return actionIdPath.toString();
    }
}
