View Javadoc

1   /*
2    * $Id: RequestUtils.java 471754 2006-11-06 14:55:09Z husted $
3    *
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *  http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  package org.apache.struts.util;
22  
23  import org.apache.commons.beanutils.BeanUtils;
24  import org.apache.commons.logging.Log;
25  import org.apache.commons.logging.LogFactory;
26  import org.apache.struts.Globals;
27  import org.apache.struts.action.ActionForm;
28  import org.apache.struts.action.ActionMapping;
29  import org.apache.struts.action.ActionServlet;
30  import org.apache.struts.action.ActionServletWrapper;
31  import org.apache.struts.config.ActionConfig;
32  import org.apache.struts.config.FormBeanConfig;
33  import org.apache.struts.config.ForwardConfig;
34  import org.apache.struts.config.ModuleConfig;
35  import org.apache.struts.upload.MultipartRequestHandler;
36  import org.apache.struts.upload.MultipartRequestWrapper;
37  
38  import javax.servlet.ServletContext;
39  import javax.servlet.ServletException;
40  import javax.servlet.http.HttpServletRequest;
41  import javax.servlet.http.HttpSession;
42  
43  import java.net.MalformedURLException;
44  import java.net.URL;
45  
46  import java.util.Collections;
47  import java.util.Enumeration;
48  import java.util.HashMap;
49  import java.util.Hashtable;
50  import java.util.Locale;
51  import java.util.Map;
52  
53  /**
54   * <p>General purpose utility methods related to processing a servlet request
55   * in the Struts controller framework.</p>
56   *
57   * @version $Rev: 471754 $ $Date: 2006-11-06 08:55:09 -0600 (Mon, 06 Nov 2006) $
58   */
59  public class RequestUtils {
60      // ------------------------------------------------------- Static Variables
61  
62      /**
63       * <p>Commons Logging instance.</p>
64       */
65      protected static Log log = LogFactory.getLog(RequestUtils.class);
66  
67      // --------------------------------------------------------- Public Methods
68  
69      /**
70       * <p>Create and return an absolute URL for the specified context-relative
71       * path, based on the server and context information in the specified
72       * request.</p>
73       *
74       * @param request The servlet request we are processing
75       * @param path    The context-relative path (must start with '/')
76       * @return absolute URL based on context-relative path
77       * @throws MalformedURLException if we cannot create an absolute URL
78       */
79      public static URL absoluteURL(HttpServletRequest request, String path)
80          throws MalformedURLException {
81          return (new URL(serverURL(request), request.getContextPath() + path));
82      }
83  
84      /**
85       * <p>Return the <code>Class</code> object for the specified fully
86       * qualified class name, from this web application's class loader.</p>
87       *
88       * @param className Fully qualified class name to be loaded
89       * @return Class object
90       * @throws ClassNotFoundException if the class cannot be found
91       */
92      public static Class applicationClass(String className)
93          throws ClassNotFoundException {
94          return applicationClass(className, null);
95      }
96  
97      /**
98       * <p>Return the <code>Class</code> object for the specified fully
99       * qualified class name, from this web application's class loader.</p>
100      *
101      * @param className   Fully qualified class name to be loaded
102      * @param classLoader The desired classloader to use
103      * @return Class object
104      * @throws ClassNotFoundException if the class cannot be found
105      */
106     public static Class applicationClass(String className,
107         ClassLoader classLoader)
108         throws ClassNotFoundException {
109         if (classLoader == null) {
110             // Look up the class loader to be used
111             classLoader = Thread.currentThread().getContextClassLoader();
112 
113             if (classLoader == null) {
114                 classLoader = RequestUtils.class.getClassLoader();
115             }
116         }
117 
118         // Attempt to load the specified class
119         return (classLoader.loadClass(className));
120     }
121 
122     /**
123      * <p>Return a new instance of the specified fully qualified class name,
124      * after loading the class from this web application's class loader. The
125      * specified class <strong>MUST</strong> have a public zero-arguments
126      * constructor.</p>
127      *
128      * @param className Fully qualified class name to use
129      * @return new instance of class
130      * @throws ClassNotFoundException if the class cannot be found
131      * @throws IllegalAccessException if the class or its constructor is not
132      *                                accessible
133      * @throws InstantiationException if this class represents an abstract
134      *                                class, an interface, an array class, a
135      *                                primitive type, or void
136      * @throws InstantiationException if this class has no zero-arguments
137      *                                constructor
138      */
139     public static Object applicationInstance(String className)
140         throws ClassNotFoundException, IllegalAccessException,
141             InstantiationException {
142         return applicationInstance(className, null);
143     }
144 
145     /**
146      * <p>Return a new instance of the specified fully qualified class name,
147      * after loading the class from this web application's class loader. The
148      * specified class <strong>MUST</strong> have a public zero-arguments
149      * constructor.</p>
150      *
151      * @param className   Fully qualified class name to use
152      * @param classLoader The desired classloader to use
153      * @return new instance of class
154      * @throws ClassNotFoundException if the class cannot be found
155      * @throws IllegalAccessException if the class or its constructor is not
156      *                                accessible
157      * @throws InstantiationException if this class represents an abstract
158      *                                class, an interface, an array class, a
159      *                                primitive type, or void
160      * @throws InstantiationException if this class has no zero-arguments
161      *                                constructor
162      */
163     public static Object applicationInstance(String className,
164         ClassLoader classLoader)
165         throws ClassNotFoundException, IllegalAccessException,
166             InstantiationException {
167         return (applicationClass(className, classLoader).newInstance());
168     }
169 
170     /**
171      * <p>Create (if necessary) and return an <code>ActionForm</code> instance
172      * appropriate for this request.  If no <code>ActionForm</code> instance
173      * is required, return <code>null</code>.</p>
174      *
175      * @param request      The servlet request we are processing
176      * @param mapping      The action mapping for this request
177      * @param moduleConfig The configuration for this module
178      * @param servlet      The action servlet
179      * @return ActionForm instance associated with this request
180      */
181     public static ActionForm createActionForm(HttpServletRequest request,
182         ActionMapping mapping, ModuleConfig moduleConfig, ActionServlet servlet) {
183         // Is there a form bean associated with this mapping?
184         String attribute = mapping.getAttribute();
185 
186         if (attribute == null) {
187             return (null);
188         }
189 
190         // Look up the form bean configuration information to use
191         String name = mapping.getName();
192         FormBeanConfig config = moduleConfig.findFormBeanConfig(name);
193 
194         if (config == null) {
195             log.warn("No FormBeanConfig found under '" + name + "'");
196 
197             return (null);
198         }
199 
200         ActionForm instance =
201             lookupActionForm(request, attribute, mapping.getScope());
202 
203         // Can we recycle the existing form bean instance (if there is one)?
204         if ((instance != null) && config.canReuse(instance)) {
205             return (instance);
206         }
207 
208         return createActionForm(config, servlet);
209     }
210 
211     private static ActionForm lookupActionForm(HttpServletRequest request,
212         String attribute, String scope) {
213         // Look up any existing form bean instance
214         if (log.isDebugEnabled()) {
215             log.debug(" Looking for ActionForm bean instance in scope '"
216                 + scope + "' under attribute key '" + attribute + "'");
217         }
218 
219         ActionForm instance = null;
220         HttpSession session = null;
221 
222         if ("request".equals(scope)) {
223             instance = (ActionForm) request.getAttribute(attribute);
224         } else {
225             session = request.getSession();
226             instance = (ActionForm) session.getAttribute(attribute);
227         }
228 
229         return (instance);
230     }
231 
232     /**
233      * <p>Create and return an <code>ActionForm</code> instance appropriate to
234      * the information in <code>config</code>.</p>
235      *
236      * <p>Does not perform any checks to see if an existing ActionForm exists
237      * which could be reused.</p>
238      *
239      * @param config  The configuration for the Form bean which is to be
240      *                created.
241      * @param servlet The action servlet
242      * @return ActionForm instance associated with this request
243      */
244     public static ActionForm createActionForm(FormBeanConfig config,
245         ActionServlet servlet) {
246         if (config == null) {
247             return (null);
248         }
249 
250         ActionForm instance = null;
251 
252         // Create and return a new form bean instance
253         try {
254             instance = config.createActionForm(servlet);
255 
256             if (log.isDebugEnabled()) {
257                 log.debug(" Creating new "
258                     + (config.getDynamic() ? "DynaActionForm" : "ActionForm")
259                     + " instance of type '" + config.getType() + "'");
260                 log.trace(" --> " + instance);
261             }
262         } catch (Throwable t) {
263             log.error(servlet.getInternal().getMessage("formBean",
264                     config.getType()), t);
265         }
266 
267         return (instance);
268     }
269 
270     /**
271      * <p>Retrieves the servlet mapping pattern for the specified {@link ActionServlet}.</p>
272      *
273      * @return the servlet mapping
274      * @see Globals#SERVLET_KEY
275      * @since Struts 1.3.6
276      */
277     public static String getServletMapping(ActionServlet servlet) {
278         ServletContext servletContext = servlet.getServletConfig().getServletContext();
279         return (String)servletContext.getAttribute(Globals.SERVLET_KEY);
280     }
281 
282     /**
283      * <p>Look up and return current user locale, based on the specified
284      * parameters.</p>
285      *
286      * @param request The request used to lookup the Locale
287      * @param locale  Name of the session attribute for our user's Locale.  If
288      *                this is <code>null</code>, the default locale key is
289      *                used for the lookup.
290      * @return current user locale
291      * @since Struts 1.2
292      */
293     public static Locale getUserLocale(HttpServletRequest request, String locale) {
294         Locale userLocale = null;
295         HttpSession session = request.getSession(false);
296 
297         if (locale == null) {
298             locale = Globals.LOCALE_KEY;
299         }
300 
301         // Only check session if sessions are enabled
302         if (session != null) {
303             userLocale = (Locale) session.getAttribute(locale);
304         }
305 
306         if (userLocale == null) {
307             // Returns Locale based on Accept-Language header or the server default
308             userLocale = request.getLocale();
309         }
310 
311         return userLocale;
312     }
313 
314     /**
315      * <p>Populate the properties of the specified JavaBean from the specified
316      * HTTP request, based on matching each parameter name against the
317      * corresponding JavaBeans "property setter" methods in the bean's class.
318      * Suitable conversion is done for argument types as described under
319      * <code>convert()</code>.</p>
320      *
321      * @param bean    The JavaBean whose properties are to be set
322      * @param request The HTTP request whose parameters are to be used to
323      *                populate bean properties
324      * @throws ServletException if an exception is thrown while setting
325      *                          property values
326      */
327     public static void populate(Object bean, HttpServletRequest request)
328         throws ServletException {
329         populate(bean, null, null, request);
330     }
331 
332     /**
333      * <p>Populate the properties of the specified JavaBean from the specified
334      * HTTP request, based on matching each parameter name (plus an optional
335      * prefix and/or suffix) against the corresponding JavaBeans "property
336      * setter" methods in the bean's class. Suitable conversion is done for
337      * argument types as described under <code>setProperties</code>.</p>
338      *
339      * <p>If you specify a non-null <code>prefix</code> and a non-null
340      * <code>suffix</code>, the parameter name must match
341      * <strong>both</strong> conditions for its value(s) to be used in
342      * populating bean properties. If the request's content type is
343      * "multipart/form-data" and the method is "POST", the
344      * <code>HttpServletRequest</code> object will be wrapped in a
345      * <code>MultipartRequestWrapper</code object.</p>
346      *
347      * @param bean    The JavaBean whose properties are to be set
348      * @param prefix  The prefix (if any) to be prepend to bean property names
349      *                when looking for matching parameters
350      * @param suffix  The suffix (if any) to be appended to bean property
351      *                names when looking for matching parameters
352      * @param request The HTTP request whose parameters are to be used to
353      *                populate bean properties
354      * @throws ServletException if an exception is thrown while setting
355      *                          property values
356      */
357     public static void populate(Object bean, String prefix, String suffix,
358         HttpServletRequest request)
359         throws ServletException {
360         // Build a list of relevant request parameters from this request
361         HashMap properties = new HashMap();
362 
363         // Iterator of parameter names
364         Enumeration names = null;
365 
366         // Map for multipart parameters
367         Map multipartParameters = null;
368 
369         String contentType = request.getContentType();
370         String method = request.getMethod();
371         boolean isMultipart = false;
372 
373         if (bean instanceof ActionForm) {
374             ((ActionForm) bean).setMultipartRequestHandler(null);
375         }
376 
377         MultipartRequestHandler multipartHandler = null;
378         if ((contentType != null)
379             && (contentType.startsWith("multipart/form-data"))
380             && (method.equalsIgnoreCase("POST"))) {
381             // Get the ActionServletWrapper from the form bean
382             ActionServletWrapper servlet;
383 
384             if (bean instanceof ActionForm) {
385                 servlet = ((ActionForm) bean).getServletWrapper();
386             } else {
387                 throw new ServletException("bean that's supposed to be "
388                     + "populated from a multipart request is not of type "
389                     + "\"org.apache.struts.action.ActionForm\", but type "
390                     + "\"" + bean.getClass().getName() + "\"");
391             }
392 
393             // Obtain a MultipartRequestHandler
394             multipartHandler = getMultipartHandler(request);
395 
396             if (multipartHandler != null) {
397                 isMultipart = true;
398 
399                 // Set servlet and mapping info
400                 servlet.setServletFor(multipartHandler);
401                 multipartHandler.setMapping((ActionMapping) request
402                     .getAttribute(Globals.MAPPING_KEY));
403 
404                 // Initialize multipart request class handler
405                 multipartHandler.handleRequest(request);
406 
407                 //stop here if the maximum length has been exceeded
408                 Boolean maxLengthExceeded =
409                     (Boolean) request.getAttribute(MultipartRequestHandler.ATTRIBUTE_MAX_LENGTH_EXCEEDED);
410 
411                 if ((maxLengthExceeded != null)
412                     && (maxLengthExceeded.booleanValue())) {
413                     ((ActionForm) bean).setMultipartRequestHandler(multipartHandler);
414                     return;
415                 }
416 
417                 //retrieve form values and put into properties
418                 multipartParameters =
419                     getAllParametersForMultipartRequest(request,
420                         multipartHandler);
421                 names = Collections.enumeration(multipartParameters.keySet());
422             }
423         }
424 
425         if (!isMultipart) {
426             names = request.getParameterNames();
427         }
428 
429         while (names.hasMoreElements()) {
430             String name = (String) names.nextElement();
431             String stripped = name;
432 
433             if (prefix != null) {
434                 if (!stripped.startsWith(prefix)) {
435                     continue;
436                 }
437 
438                 stripped = stripped.substring(prefix.length());
439             }
440 
441             if (suffix != null) {
442                 if (!stripped.endsWith(suffix)) {
443                     continue;
444                 }
445 
446                 stripped =
447                     stripped.substring(0, stripped.length() - suffix.length());
448             }
449 
450             Object parameterValue = null;
451 
452             if (isMultipart) {
453                 parameterValue = multipartParameters.get(name);
454             } else {
455                 parameterValue = request.getParameterValues(name);
456             }
457 
458             // Populate parameters, except "standard" struts attributes
459             // such as 'org.apache.struts.action.CANCEL'
460             if (!(stripped.startsWith("org.apache.struts."))) {
461                 properties.put(stripped, parameterValue);
462             }
463         }
464 
465         // Set the corresponding properties of our bean
466         try {
467             BeanUtils.populate(bean, properties);
468         } catch (Exception e) {
469             throw new ServletException("BeanUtils.populate", e);
470         } finally {
471             if (multipartHandler != null) {
472                 // Set the multipart request handler for our ActionForm.
473                 // If the bean isn't an ActionForm, an exception would have been
474                 // thrown earlier, so it's safe to assume that our bean is
475                 // in fact an ActionForm.
476                 ((ActionForm) bean).setMultipartRequestHandler(multipartHandler);
477             }
478         }
479     }
480 
481     /**
482      * <p>Try to locate a multipart request handler for this request. First,
483      * look for a mapping-specific handler stored for us under an attribute.
484      * If one is not present, use the global multipart handler, if there is
485      * one.</p>
486      *
487      * @param request The HTTP request for which the multipart handler should
488      *                be found.
489      * @return the multipart handler to use, or null if none is found.
490      * @throws ServletException if any exception is thrown while attempting to
491      *                          locate the multipart handler.
492      */
493     private static MultipartRequestHandler getMultipartHandler(
494         HttpServletRequest request)
495         throws ServletException {
496         MultipartRequestHandler multipartHandler = null;
497         String multipartClass =
498             (String) request.getAttribute(Globals.MULTIPART_KEY);
499 
500         request.removeAttribute(Globals.MULTIPART_KEY);
501 
502         // Try to initialize the mapping specific request handler
503         if (multipartClass != null) {
504             try {
505                 multipartHandler =
506                     (MultipartRequestHandler) applicationInstance(multipartClass);
507             } catch (ClassNotFoundException cnfe) {
508                 log.error("MultipartRequestHandler class \"" + multipartClass
509                     + "\" in mapping class not found, "
510                     + "defaulting to global multipart class");
511             } catch (InstantiationException ie) {
512                 log.error("InstantiationException when instantiating "
513                     + "MultipartRequestHandler \"" + multipartClass + "\", "
514                     + "defaulting to global multipart class, exception: "
515                     + ie.getMessage());
516             } catch (IllegalAccessException iae) {
517                 log.error("IllegalAccessException when instantiating "
518                     + "MultipartRequestHandler \"" + multipartClass + "\", "
519                     + "defaulting to global multipart class, exception: "
520                     + iae.getMessage());
521             }
522 
523             if (multipartHandler != null) {
524                 return multipartHandler;
525             }
526         }
527 
528         ModuleConfig moduleConfig =
529             ModuleUtils.getInstance().getModuleConfig(request);
530 
531         multipartClass = moduleConfig.getControllerConfig().getMultipartClass();
532 
533         // Try to initialize the global request handler
534         if (multipartClass != null) {
535             try {
536                 multipartHandler =
537                     (MultipartRequestHandler) applicationInstance(multipartClass);
538             } catch (ClassNotFoundException cnfe) {
539                 throw new ServletException("Cannot find multipart class \""
540                     + multipartClass + "\"" + ", exception: "
541                     + cnfe.getMessage());
542             } catch (InstantiationException ie) {
543                 throw new ServletException(
544                     "InstantiationException when instantiating "
545                     + "multipart class \"" + multipartClass + "\", exception: "
546                     + ie.getMessage());
547             } catch (IllegalAccessException iae) {
548                 throw new ServletException(
549                     "IllegalAccessException when instantiating "
550                     + "multipart class \"" + multipartClass + "\", exception: "
551                     + iae.getMessage());
552             }
553 
554             if (multipartHandler != null) {
555                 return multipartHandler;
556             }
557         }
558 
559         return multipartHandler;
560     }
561 
562     /**
563      * <p>Create a <code>Map</code> containing all of the parameters supplied
564      * for a multipart request, keyed by parameter name. In addition to text
565      * and file elements from the multipart body, query string parameters are
566      * included as well.</p>
567      *
568      * @param request          The (wrapped) HTTP request whose parameters are
569      *                         to be added to the map.
570      * @param multipartHandler The multipart handler used to parse the
571      *                         request.
572      * @return the map containing all parameters for this multipart request.
573      */
574     private static Map getAllParametersForMultipartRequest(
575         HttpServletRequest request, MultipartRequestHandler multipartHandler) {
576         Map parameters = new HashMap();
577         Hashtable elements = multipartHandler.getAllElements();
578         Enumeration e = elements.keys();
579 
580         while (e.hasMoreElements()) {
581             String key = (String) e.nextElement();
582 
583             parameters.put(key, elements.get(key));
584         }
585 
586         if (request instanceof MultipartRequestWrapper) {
587             request =
588                 (HttpServletRequest) ((MultipartRequestWrapper) request)
589                 .getRequest();
590             e = request.getParameterNames();
591 
592             while (e.hasMoreElements()) {
593                 String key = (String) e.nextElement();
594 
595                 parameters.put(key, request.getParameterValues(key));
596             }
597         } else {
598             log.debug("Gathering multipart parameters for unwrapped request");
599         }
600 
601         return parameters;
602     }
603 
604     /**
605      * <p>Compute the printable representation of a URL, leaving off the
606      * scheme/host/port part if no host is specified. This will typically be
607      * the case for URLs that were originally created from relative or
608      * context-relative URIs.</p>
609      *
610      * @param url URL to render in a printable representation
611      * @return printable representation of a URL
612      */
613     public static String printableURL(URL url) {
614         if (url.getHost() != null) {
615             return (url.toString());
616         }
617 
618         String file = url.getFile();
619         String ref = url.getRef();
620 
621         if (ref == null) {
622             return (file);
623         } else {
624             StringBuffer sb = new StringBuffer(file);
625 
626             sb.append('#');
627             sb.append(ref);
628 
629             return (sb.toString());
630         }
631     }
632 
633     /**
634      * <p>Return the context-relative URL that corresponds to the specified
635      * {@link ActionConfig}, relative to the module associated with the
636      * current modules's {@link ModuleConfig}.</p>
637      *
638      * @param request The servlet request we are processing
639      * @param action  ActionConfig to be evaluated
640      * @param pattern URL pattern used to map the controller servlet
641      * @return context-relative URL relative to the module
642      * @since Struts 1.1
643      */
644     public static String actionURL(HttpServletRequest request,
645         ActionConfig action, String pattern) {
646         StringBuffer sb = new StringBuffer();
647 
648         if (pattern.endsWith("/*")) {
649             sb.append(pattern.substring(0, pattern.length() - 2));
650             sb.append(action.getPath());
651         } else if (pattern.startsWith("*.")) {
652             ModuleConfig appConfig =
653                 ModuleUtils.getInstance().getModuleConfig(request);
654 
655             sb.append(appConfig.getPrefix());
656             sb.append(action.getPath());
657             sb.append(pattern.substring(1));
658         } else {
659             throw new IllegalArgumentException(pattern);
660         }
661 
662         return sb.toString();
663     }
664 
665     /**
666      * <p>Return the context-relative URL that corresponds to the specified
667      * <code>ForwardConfig</code>. The URL is calculated based on the
668      * properties of the {@link ForwardConfig} instance as follows:</p>
669      *
670      * <ul>
671      *
672      *
673      * <li>If the <code>contextRelative</code> property is set, it is assumed
674      * that the <code>path</code> property contains a path that is already
675      * context-relative:
676      *
677      * <ul>
678      *
679      * <li>If the <code>path</code> property value starts with a slash, it is
680      * returned unmodified.</li> <li>If the <code>path</code> property value
681      * does not start with a slash, a slash is prepended.</li>
682      *
683      * </ul></li>
684      *
685      * <li>Acquire the <code>forwardPattern</code> property from the
686      * <code>ControllerConfig</code> for the application module used to
687      * process this request. If no pattern was configured, default to a
688      * pattern of <code>$M$P</code>, which is compatible with the hard-coded
689      * mapping behavior in Struts 1.0.</li>
690      *
691      * <li>Process the acquired <code>forwardPattern</code>, performing the
692      * following substitutions:
693      *
694      * <ul>
695      *
696      * <li><strong>$M</strong> - Replaced by the module prefix for the
697      * application module processing this request.</li>
698      *
699      * <li><strong>$P</strong> - Replaced by the <code>path</code> property of
700      * the specified {@link ForwardConfig}, prepended with a slash if it does
701      * not start with one.</li>
702      *
703      * <li><strong>$$</strong> - Replaced by a single dollar sign
704      * character.</li>
705      *
706      * <li><strong>$x</strong> (where "x" is any charater not listed above) -
707      * Silently omit these two characters from the result value.  (This has
708      * the side effect of causing all other $+letter combinations to be
709      * reserved.)</li>
710      *
711      * </ul></li>
712      *
713      * </ul>
714      *
715      * @param request The servlet request we are processing
716      * @param forward ForwardConfig to be evaluated
717      * @return context-relative URL
718      * @since Struts 1.1
719      */
720     public static String forwardURL(HttpServletRequest request,
721         ForwardConfig forward) {
722         return forwardURL(request, forward, null);
723     }
724 
725     /**
726      * <p>Return the context-relative URL that corresponds to the specified
727      * <code>ForwardConfig</code>. The URL is calculated based on the
728      * properties of the {@link ForwardConfig} instance as follows:</p>
729      *
730      * <ul>
731      *
732      * <li>If the <code>contextRelative</code> property is set, it is assumed
733      * that the <code>path</code> property contains a path that is already
734      * context-relative: <ul>
735      *
736      * <li>If the <code>path</code> property value starts with a slash, it is
737      * returned unmodified.</li> <li>If the <code>path</code> property value
738      * does not start with a slash, a slash is prepended.</li>
739      *
740      * </ul></li>
741      *
742      * <li>Acquire the <code>forwardPattern</code> property from the
743      * <code>ControllerConfig</code> for the application module used to
744      * process this request. If no pattern was configured, default to a
745      * pattern of <code>$M$P</code>, which is compatible with the hard-coded
746      * mapping behavior in Struts 1.0.</li>
747      *
748      * <li>Process the acquired <code>forwardPattern</code>, performing the
749      * following substitutions: <ul> <li><strong>$M</strong> - Replaced by the
750      * module prefix for the application module processing this request.</li>
751      *
752      * <li><strong>$P</strong> - Replaced by the <code>path</code> property of
753      * the specified {@link ForwardConfig}, prepended with a slash if it does
754      * not start with one.</li>
755      *
756      * <li><strong>$$</strong> - Replaced by a single dollar sign
757      * character.</li>
758      *
759      * <li><strong>$x</strong> (where "x" is any charater not listed above) -
760      * Silently omit these two characters from the result value.  (This has
761      * the side effect of causing all other $+letter combinations to be
762      * reserved.)</li>
763      *
764      * </ul></li></ul>
765      *
766      * @param request      The servlet request we are processing
767      * @param forward      ForwardConfig to be evaluated
768      * @param moduleConfig Base forward on this module config.
769      * @return context-relative URL
770      * @since Struts 1.2
771      */
772     public static String forwardURL(HttpServletRequest request,
773         ForwardConfig forward, ModuleConfig moduleConfig) {
774         //load the current moduleConfig, if null
775         if (moduleConfig == null) {
776             moduleConfig = ModuleUtils.getInstance().getModuleConfig(request);
777         }
778 
779         String path = forward.getPath();
780 
781         //load default prefix
782         String prefix = moduleConfig.getPrefix();
783 
784         //override prefix if supplied by forward
785         if (forward.getModule() != null) {
786             prefix = forward.getModule();
787 
788             if ("/".equals(prefix)) {
789                 prefix = "";
790             }
791         }
792 
793         StringBuffer sb = new StringBuffer();
794 
795         // Calculate a context relative path for this ForwardConfig
796         String forwardPattern =
797             moduleConfig.getControllerConfig().getForwardPattern();
798 
799         if (forwardPattern == null) {
800             // Performance optimization for previous default behavior
801             sb.append(prefix);
802 
803             // smoothly insert a '/' if needed
804             if (!path.startsWith("/")) {
805                 sb.append("/");
806             }
807 
808             sb.append(path);
809         } else {
810             boolean dollar = false;
811 
812             for (int i = 0; i < forwardPattern.length(); i++) {
813                 char ch = forwardPattern.charAt(i);
814 
815                 if (dollar) {
816                     switch (ch) {
817                     case 'M':
818                         sb.append(prefix);
819 
820                         break;
821 
822                     case 'P':
823 
824                         // add '/' if needed
825                         if (!path.startsWith("/")) {
826                             sb.append("/");
827                         }
828 
829                         sb.append(path);
830 
831                         break;
832 
833                     case '$':
834                         sb.append('$');
835 
836                         break;
837 
838                     default:
839                         ; // Silently swallow
840                     }
841 
842                     dollar = false;
843 
844                     continue;
845                 } else if (ch == '$') {
846                     dollar = true;
847                 } else {
848                     sb.append(ch);
849                 }
850             }
851         }
852 
853         return (sb.toString());
854     }
855 
856     /**
857      * <p>Return the URL representing the current request. This is equivalent
858      * to <code>HttpServletRequest.getRequestURL</code> in Servlet 2.3.</p>
859      *
860      * @param request The servlet request we are processing
861      * @return URL representing the current request
862      * @throws MalformedURLException if a URL cannot be created
863      */
864     public static URL requestURL(HttpServletRequest request)
865         throws MalformedURLException {
866         StringBuffer url = requestToServerUriStringBuffer(request);
867 
868         return (new URL(url.toString()));
869     }
870 
871     /**
872      * <p>Return the URL representing the scheme, server, and port number of
873      * the current request. Server-relative URLs can be created by simply
874      * appending the server-relative path (starting with '/') to this.</p>
875      *
876      * @param request The servlet request we are processing
877      * @return URL representing the scheme, server, and port number of the
878      *         current request
879      * @throws MalformedURLException if a URL cannot be created
880      */
881     public static URL serverURL(HttpServletRequest request)
882         throws MalformedURLException {
883         StringBuffer url = requestToServerStringBuffer(request);
884 
885         return (new URL(url.toString()));
886     }
887 
888     /**
889      * <p>Return the string representing the scheme, server, and port number
890      * of the current request. Server-relative URLs can be created by simply
891      * appending the server-relative path (starting with '/') to this.</p>
892      *
893      * @param request The servlet request we are processing
894      * @return URL representing the scheme, server, and port number of the
895      *         current request
896      * @since Struts 1.2.0
897      */
898     public static StringBuffer requestToServerUriStringBuffer(
899         HttpServletRequest request) {
900         StringBuffer serverUri =
901             createServerUriStringBuffer(request.getScheme(),
902                 request.getServerName(), request.getServerPort(),
903                 request.getRequestURI());
904 
905         return serverUri;
906     }
907 
908     /**
909      * <p>Return <code>StringBuffer</code> representing the scheme, server,
910      * and port number of the current request. Server-relative URLs can be
911      * created by simply appending the server-relative path (starting with
912      * '/') to this.</p>
913      *
914      * @param request The servlet request we are processing
915      * @return URL representing the scheme, server, and port number of the
916      *         current request
917      * @since Struts 1.2.0
918      */
919     public static StringBuffer requestToServerStringBuffer(
920         HttpServletRequest request) {
921         return createServerStringBuffer(request.getScheme(),
922             request.getServerName(), request.getServerPort());
923     }
924 
925     /**
926      * <p>Return <code>StringBuffer</code> representing the scheme, server,
927      * and port number of the current request.</p>
928      *
929      * @param scheme The scheme name to use
930      * @param server The server name to use
931      * @param port   The port value to use
932      * @return StringBuffer in the form scheme: server: port
933      * @since Struts 1.2.0
934      */
935     public static StringBuffer createServerStringBuffer(String scheme,
936         String server, int port) {
937         StringBuffer url = new StringBuffer();
938 
939         if (port < 0) {
940             port = 80; // Work around java.net.URL bug
941         }
942 
943         url.append(scheme);
944         url.append("://");
945         url.append(server);
946 
947         if ((scheme.equals("http") && (port != 80))
948             || (scheme.equals("https") && (port != 443))) {
949             url.append(':');
950             url.append(port);
951         }
952 
953         return url;
954     }
955 
956     /**
957      * <p>Return <code>StringBuffer</code> representing the scheme, server,
958      * and port number of the current request.</p>
959      *
960      * @param scheme The scheme name to use
961      * @param server The server name to use
962      * @param port   The port value to use
963      * @param uri    The uri value to use
964      * @return StringBuffer in the form scheme: server: port
965      * @since Struts 1.2.0
966      */
967     public static StringBuffer createServerUriStringBuffer(String scheme,
968         String server, int port, String uri) {
969         StringBuffer serverUri = createServerStringBuffer(scheme, server, port);
970 
971         serverUri.append(uri);
972 
973         return serverUri;
974     }
975 
976     /**
977      * <p>Returns the true path of the destination action if the specified forward
978      * is an action-aliased URL. This method version forms the URL based on
979      * the current request; selecting the current module if the forward does not
980      * explicitly contain a module path.</p>
981      *
982      * @param forward the forward config
983      * @param request the current request
984      * @param servlet the servlet handling the current request
985      * @return the context-relative URL of the action if the forward has an action identifier; otherwise <code>null</code>.
986      * @since Struts 1.3.6
987      */
988     public static String actionIdURL(ForwardConfig forward, HttpServletRequest request, ActionServlet servlet) {
989         ModuleConfig moduleConfig = null;
990         if (forward.getModule() != null) {
991             String prefix = forward.getModule();
992             moduleConfig = ModuleUtils.getInstance().getModuleConfig(prefix, servlet.getServletContext());
993         } else {
994             moduleConfig = ModuleUtils.getInstance().getModuleConfig(request);
995         }
996         return actionIdURL(forward.getPath(), moduleConfig, servlet);
997     }
998 
999     /**
1000      * <p>Returns the true path of the destination action if the specified forward
1001      * is an action-aliased URL. This method version forms the URL based on
1002      * the specified module.
1003      *
1004      * @param originalPath the action-aliased path
1005      * @param moduleConfig the module config for this request
1006      * @param servlet the servlet handling the current request
1007      * @return the context-relative URL of the action if the path has an action identifier; otherwise <code>null</code>.
1008      * @since Struts 1.3.6
1009      */
1010     public static String actionIdURL(String originalPath, ModuleConfig moduleConfig, ActionServlet servlet) {
1011         if (originalPath.startsWith("http") || originalPath.startsWith("/")) {
1012             return null;
1013         }
1014 
1015         // Split the forward path into the resource and query string;
1016         // it is possible a forward (or redirect) has added parameters.
1017         String actionId = null;
1018         String qs = null;
1019         int qpos = originalPath.indexOf("?");
1020         if (qpos == -1) {
1021             actionId = originalPath;
1022         } else {
1023             actionId = originalPath.substring(0, qpos);
1024             qs = originalPath.substring(qpos);
1025         }
1026 
1027         // Find the action of the given actionId
1028         ActionConfig actionConfig = moduleConfig.findActionConfigId(actionId);
1029         if (actionConfig == null) {
1030             if (log.isDebugEnabled()) {
1031                 log.debug("No actionId found for " + actionId);
1032             }
1033             return null;
1034         }
1035 
1036         String path = actionConfig.getPath();
1037         String mapping = RequestUtils.getServletMapping(servlet);
1038         StringBuffer actionIdPath = new StringBuffer();
1039 
1040         // Form the path based on the servlet mapping pattern
1041         if (mapping.startsWith("*")) {
1042             actionIdPath.append(path);
1043             actionIdPath.append(mapping.substring(1));
1044         } else if (mapping.startsWith("/")) {  // implied ends with a *
1045             mapping = mapping.substring(0, mapping.length() - 1);
1046             if (mapping.endsWith("/") && path.startsWith("/")) {
1047                 actionIdPath.append(mapping);
1048                 actionIdPath.append(path.substring(1));
1049             } else {
1050                 actionIdPath.append(mapping);
1051                 actionIdPath.append(path);
1052             }
1053         } else {
1054             log.warn("Unknown servlet mapping pattern");
1055             actionIdPath.append(path);
1056         }
1057 
1058         // Lastly add any query parameters (the ? is part of the query string)
1059         if (qs != null) {
1060             actionIdPath.append(qs);
1061         }
1062 
1063         // Return the path
1064         if (log.isDebugEnabled()) {
1065             log.debug(originalPath + " unaliased to " + actionIdPath.toString());
1066         }
1067         return actionIdPath.toString();
1068     }
1069 }