/*
 * $Id: RequestUtils.java 727180 2008-12-16 21:54:10Z 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.*;
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 jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;

/**
 * <p>General purpose utility methods related to processing a servlet request
 * in the Struts controller framework.</p>
 *
 * @version $Rev: 727180 $ $Date: 2008-12-17 00:54:10 +0300 (Ср., 17 дек. 2008) $
 */
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/>
   * <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/>
   * <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>Populates the parameters of the specified ActionRedirect from
   * the specified HTTP request.</p>
   *
   * @param redirect The ActionRedirect whose parameters are to be set
   * @param request  The HTTP request whose parameters are to be used
   * @since Struts 1.4
   */
  public static void populate(ActionRedirect redirect, HttpServletRequest request) {
    assert (redirect != null) : "redirect is required";
    assert (request != null) : "request is required";

    Enumeration e = request.getParameterNames();
    while (e.hasMoreElements()) {
      String name = (String) e.nextElement();
      String[] values = request.getParameterValues(name);
      redirect.addParameter(name, values);
    }
  }

  /**
   * <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 (propertyType == null) {
        return parameterValue;
      }

      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 + "\"", cnfe);
      } catch (InstantiationException ie) {
        throw new ServletException(
            "InstantiationException when instantiating "
                + "multipart class \"" + multipartClass + "\"", ie);
      } catch (IllegalAccessException iae) {
        throw new ServletException(
            "IllegalAccessException when instantiating "
                + "multipart class \"" + multipartClass + "\"", iae);
      }

      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>
   * <p/>
   * <ul>
   * <p/>
   * <p/>
   * <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:
   * <p/>
   * <ul>
   * <p/>
   * <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>
   * <p/>
   * </ul></li>
   * <p/>
   * <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>
   * <p/>
   * <li>Process the acquired <code>forwardPattern</code>, performing the
   * following substitutions:
   * <p/>
   * <ul>
   * <p/>
   * <li><strong>$M</strong> - Replaced by the module prefix for the
   * application module processing this request.</li>
   * <p/>
   * <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>
   * <p/>
   * <li><strong>$$</strong> - Replaced by a single dollar sign
   * character.</li>
   * <p/>
   * <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>
   * <p/>
   * </ul></li>
   * <p/>
   * </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>
   * <p/>
   * <ul>
   * <p/>
   * <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>
   * <p/>
   * <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>
   * <p/>
   * </ul></li>
   * <p/>
   * <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>
   * <p/>
   * <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>
   * <p/>
   * <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>
   * <p/>
   * <li><strong>$$</strong> - Replaced by a single dollar sign
   * character.</li>
   * <p/>
   * <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>
   * <p/>
   * </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();
  }

  /**
   * Determines whether the current request is forwarded.
   *
   * @param request current HTTP request
   * @return true if the request is forwarded; otherwise false
   * @since Struts 1.4
   */
  public static boolean isRequestForwarded(HttpServletRequest request) {
    return (request.getAttribute("jakarta.servlet.forward.request_uri") != null);
  }

  /**
   * Determines whether the current request is included.
   *
   * @param request current HTTP request
   * @return true if the request is included; otherwise false
   * @since Struts 1.4
   */
  public static boolean isRequestIncluded(HttpServletRequest request) {
    return (request.getAttribute("jakarta.servlet.include.request_uri") != null);
  }

  /**
   * Verifies whether current request is forwarded from one action to
   * another or not.
   *
   * @param request current HTTP request
   * @return true if the request is chained; otherwise false
   * @since Struts 1.4
   */
  public static boolean isRequestChained(HttpServletRequest request) {
    return (request.getAttribute(Globals.CHAIN_KEY) != null);
  }
}
