View Javadoc

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