View Javadoc

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