View Javadoc

1   /*
2    * $Id: RequestUtils.java 524895 2007-04-02 19:29:21Z germuska $
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: 524895 $ $Date: 2007-04-02 14:29:21 -0500 (Mon, 02 Apr 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)) return parameterValue;
499 
500     	FormFile formFileValue = (FormFile) parameterValue;
501     	try {
502 			Class propertyType = PropertyUtils.getPropertyType(bean, name);
503 
504 			if (propertyType.isAssignableFrom(List.class)) {
505 				ArrayList list = new ArrayList(1);
506 				list.add(formFileValue);
507 				return list;
508 			}
509 
510 			if (propertyType.isArray() && propertyType.getComponentType().equals(FormFile.class)) {
511 				return new FormFile[] { formFileValue };
512 			}
513 
514     	} catch (IllegalAccessException e) {
515 			throw new ServletException(e);
516 		} catch (InvocationTargetException e) {
517 			throw new ServletException(e);
518 		} catch (NoSuchMethodException e) {
519 			throw new ServletException(e);
520 		}
521     	
522 		// no changes
523     	return parameterValue;
524     	
525 	}
526 
527 	/**
528      * <p>Try to locate a multipart request handler for this request. First,
529      * look for a mapping-specific handler stored for us under an attribute.
530      * If one is not present, use the global multipart handler, if there is
531      * one.</p>
532      *
533      * @param request The HTTP request for which the multipart handler should
534      *                be found.
535      * @return the multipart handler to use, or null if none is found.
536      * @throws ServletException if any exception is thrown while attempting to
537      *                          locate the multipart handler.
538      */
539     private static MultipartRequestHandler getMultipartHandler(
540         HttpServletRequest request)
541         throws ServletException {
542         MultipartRequestHandler multipartHandler = null;
543         String multipartClass =
544             (String) request.getAttribute(Globals.MULTIPART_KEY);
545 
546         request.removeAttribute(Globals.MULTIPART_KEY);
547 
548         // Try to initialize the mapping specific request handler
549         if (multipartClass != null) {
550             try {
551                 multipartHandler =
552                     (MultipartRequestHandler) applicationInstance(multipartClass);
553             } catch (ClassNotFoundException cnfe) {
554                 log.error("MultipartRequestHandler class \"" + multipartClass
555                     + "\" in mapping class not found, "
556                     + "defaulting to global multipart class");
557             } catch (InstantiationException ie) {
558                 log.error("InstantiationException when instantiating "
559                     + "MultipartRequestHandler \"" + multipartClass + "\", "
560                     + "defaulting to global multipart class, exception: "
561                     + ie.getMessage());
562             } catch (IllegalAccessException iae) {
563                 log.error("IllegalAccessException when instantiating "
564                     + "MultipartRequestHandler \"" + multipartClass + "\", "
565                     + "defaulting to global multipart class, exception: "
566                     + iae.getMessage());
567             }
568 
569             if (multipartHandler != null) {
570                 return multipartHandler;
571             }
572         }
573 
574         ModuleConfig moduleConfig =
575             ModuleUtils.getInstance().getModuleConfig(request);
576 
577         multipartClass = moduleConfig.getControllerConfig().getMultipartClass();
578 
579         // Try to initialize the global request handler
580         if (multipartClass != null) {
581             try {
582                 multipartHandler =
583                     (MultipartRequestHandler) applicationInstance(multipartClass);
584             } catch (ClassNotFoundException cnfe) {
585                 throw new ServletException("Cannot find multipart class \""
586                     + multipartClass + "\"" + ", exception: "
587                     + cnfe.getMessage());
588             } catch (InstantiationException ie) {
589                 throw new ServletException(
590                     "InstantiationException when instantiating "
591                     + "multipart class \"" + multipartClass + "\", exception: "
592                     + ie.getMessage());
593             } catch (IllegalAccessException iae) {
594                 throw new ServletException(
595                     "IllegalAccessException when instantiating "
596                     + "multipart class \"" + multipartClass + "\", exception: "
597                     + iae.getMessage());
598             }
599 
600             if (multipartHandler != null) {
601                 return multipartHandler;
602             }
603         }
604 
605         return multipartHandler;
606     }
607 
608     /**
609      * <p>Create a <code>Map</code> containing all of the parameters supplied
610      * for a multipart request, keyed by parameter name. In addition to text
611      * and file elements from the multipart body, query string parameters are
612      * included as well.</p>
613      *
614      * @param request          The (wrapped) HTTP request whose parameters are
615      *                         to be added to the map.
616      * @param multipartHandler The multipart handler used to parse the
617      *                         request.
618      * @return the map containing all parameters for this multipart request.
619      */
620     private static Map getAllParametersForMultipartRequest(
621         HttpServletRequest request, MultipartRequestHandler multipartHandler) {
622         Map parameters = new HashMap();
623         Hashtable elements = multipartHandler.getAllElements();
624         Enumeration e = elements.keys();
625 
626         while (e.hasMoreElements()) {
627             String key = (String) e.nextElement();
628 
629             parameters.put(key, elements.get(key));
630         }
631 
632         if (request instanceof MultipartRequestWrapper) {
633             request =
634                 (HttpServletRequest) ((MultipartRequestWrapper) request)
635                 .getRequest();
636             e = request.getParameterNames();
637 
638             while (e.hasMoreElements()) {
639                 String key = (String) e.nextElement();
640 
641                 parameters.put(key, request.getParameterValues(key));
642             }
643         } else {
644             log.debug("Gathering multipart parameters for unwrapped request");
645         }
646 
647         return parameters;
648     }
649 
650     /**
651      * <p>Compute the printable representation of a URL, leaving off the
652      * scheme/host/port part if no host is specified. This will typically be
653      * the case for URLs that were originally created from relative or
654      * context-relative URIs.</p>
655      *
656      * @param url URL to render in a printable representation
657      * @return printable representation of a URL
658      */
659     public static String printableURL(URL url) {
660         if (url.getHost() != null) {
661             return (url.toString());
662         }
663 
664         String file = url.getFile();
665         String ref = url.getRef();
666 
667         if (ref == null) {
668             return (file);
669         } else {
670             StringBuffer sb = new StringBuffer(file);
671 
672             sb.append('#');
673             sb.append(ref);
674 
675             return (sb.toString());
676         }
677     }
678 
679     /**
680      * <p>Return the context-relative URL that corresponds to the specified
681      * {@link ActionConfig}, relative to the module associated with the
682      * current modules's {@link ModuleConfig}.</p>
683      *
684      * @param request The servlet request we are processing
685      * @param action  ActionConfig to be evaluated
686      * @param pattern URL pattern used to map the controller servlet
687      * @return context-relative URL relative to the module
688      * @since Struts 1.1
689      */
690     public static String actionURL(HttpServletRequest request,
691         ActionConfig action, String pattern) {
692         StringBuffer sb = new StringBuffer();
693 
694         if (pattern.endsWith("/*")) {
695             sb.append(pattern.substring(0, pattern.length() - 2));
696             sb.append(action.getPath());
697         } else if (pattern.startsWith("*.")) {
698             ModuleConfig appConfig =
699                 ModuleUtils.getInstance().getModuleConfig(request);
700 
701             sb.append(appConfig.getPrefix());
702             sb.append(action.getPath());
703             sb.append(pattern.substring(1));
704         } else {
705             throw new IllegalArgumentException(pattern);
706         }
707 
708         return sb.toString();
709     }
710 
711     /**
712      * <p>Return the context-relative URL that corresponds to the specified
713      * <code>ForwardConfig</code>. The URL is calculated based on the
714      * properties of the {@link ForwardConfig} instance as follows:</p>
715      *
716      * <ul>
717      *
718      *
719      * <li>If the <code>contextRelative</code> property is set, it is assumed
720      * that the <code>path</code> property contains a path that is already
721      * context-relative:
722      *
723      * <ul>
724      *
725      * <li>If the <code>path</code> property value starts with a slash, it is
726      * returned unmodified.</li> <li>If the <code>path</code> property value
727      * does not start with a slash, a slash is prepended.</li>
728      *
729      * </ul></li>
730      *
731      * <li>Acquire the <code>forwardPattern</code> property from the
732      * <code>ControllerConfig</code> for the application module used to
733      * process this request. If no pattern was configured, default to a
734      * pattern of <code>$M$P</code>, which is compatible with the hard-coded
735      * mapping behavior in Struts 1.0.</li>
736      *
737      * <li>Process the acquired <code>forwardPattern</code>, performing the
738      * following substitutions:
739      *
740      * <ul>
741      *
742      * <li><strong>$M</strong> - Replaced by the module prefix for the
743      * application module processing this request.</li>
744      *
745      * <li><strong>$P</strong> - Replaced by the <code>path</code> property of
746      * the specified {@link ForwardConfig}, prepended with a slash if it does
747      * not start with one.</li>
748      *
749      * <li><strong>$$</strong> - Replaced by a single dollar sign
750      * character.</li>
751      *
752      * <li><strong>$x</strong> (where "x" is any charater not listed above) -
753      * Silently omit these two characters from the result value.  (This has
754      * the side effect of causing all other $+letter combinations to be
755      * reserved.)</li>
756      *
757      * </ul></li>
758      *
759      * </ul>
760      *
761      * @param request The servlet request we are processing
762      * @param forward ForwardConfig to be evaluated
763      * @return context-relative URL
764      * @since Struts 1.1
765      */
766     public static String forwardURL(HttpServletRequest request,
767         ForwardConfig forward) {
768         return forwardURL(request, forward, null);
769     }
770 
771     /**
772      * <p>Return the context-relative URL that corresponds to the specified
773      * <code>ForwardConfig</code>. The URL is calculated based on the
774      * properties of the {@link ForwardConfig} instance as follows:</p>
775      *
776      * <ul>
777      *
778      * <li>If the <code>contextRelative</code> property is set, it is assumed
779      * that the <code>path</code> property contains a path that is already
780      * context-relative: <ul>
781      *
782      * <li>If the <code>path</code> property value starts with a slash, it is
783      * returned unmodified.</li> <li>If the <code>path</code> property value
784      * does not start with a slash, a slash is prepended.</li>
785      *
786      * </ul></li>
787      *
788      * <li>Acquire the <code>forwardPattern</code> property from the
789      * <code>ControllerConfig</code> for the application module used to
790      * process this request. If no pattern was configured, default to a
791      * pattern of <code>$M$P</code>, which is compatible with the hard-coded
792      * mapping behavior in Struts 1.0.</li>
793      *
794      * <li>Process the acquired <code>forwardPattern</code>, performing the
795      * following substitutions: <ul> <li><strong>$M</strong> - Replaced by the
796      * module prefix for the application module processing this request.</li>
797      *
798      * <li><strong>$P</strong> - Replaced by the <code>path</code> property of
799      * the specified {@link ForwardConfig}, prepended with a slash if it does
800      * not start with one.</li>
801      *
802      * <li><strong>$$</strong> - Replaced by a single dollar sign
803      * character.</li>
804      *
805      * <li><strong>$x</strong> (where "x" is any charater not listed above) -
806      * Silently omit these two characters from the result value.  (This has
807      * the side effect of causing all other $+letter combinations to be
808      * reserved.)</li>
809      *
810      * </ul></li></ul>
811      *
812      * @param request      The servlet request we are processing
813      * @param forward      ForwardConfig to be evaluated
814      * @param moduleConfig Base forward on this module config.
815      * @return context-relative URL
816      * @since Struts 1.2
817      */
818     public static String forwardURL(HttpServletRequest request,
819         ForwardConfig forward, ModuleConfig moduleConfig) {
820         //load the current moduleConfig, if null
821         if (moduleConfig == null) {
822             moduleConfig = ModuleUtils.getInstance().getModuleConfig(request);
823         }
824 
825         String path = forward.getPath();
826 
827         //load default prefix
828         String prefix = moduleConfig.getPrefix();
829 
830         //override prefix if supplied by forward
831         if (forward.getModule() != null) {
832             prefix = forward.getModule();
833 
834             if ("/".equals(prefix)) {
835                 prefix = "";
836             }
837         }
838 
839         StringBuffer sb = new StringBuffer();
840 
841         // Calculate a context relative path for this ForwardConfig
842         String forwardPattern =
843             moduleConfig.getControllerConfig().getForwardPattern();
844 
845         if (forwardPattern == null) {
846             // Performance optimization for previous default behavior
847             sb.append(prefix);
848 
849             // smoothly insert a '/' if needed
850             if (!path.startsWith("/")) {
851                 sb.append("/");
852             }
853 
854             sb.append(path);
855         } else {
856             boolean dollar = false;
857 
858             for (int i = 0; i < forwardPattern.length(); i++) {
859                 char ch = forwardPattern.charAt(i);
860 
861                 if (dollar) {
862                     switch (ch) {
863                     case 'M':
864                         sb.append(prefix);
865 
866                         break;
867 
868                     case 'P':
869 
870                         // add '/' if needed
871                         if (!path.startsWith("/")) {
872                             sb.append("/");
873                         }
874 
875                         sb.append(path);
876 
877                         break;
878 
879                     case '$':
880                         sb.append('$');
881 
882                         break;
883 
884                     default:
885                         ; // Silently swallow
886                     }
887 
888                     dollar = false;
889 
890                     continue;
891                 } else if (ch == '$') {
892                     dollar = true;
893                 } else {
894                     sb.append(ch);
895                 }
896             }
897         }
898 
899         return (sb.toString());
900     }
901 
902     /**
903      * <p>Return the URL representing the current request. This is equivalent
904      * to <code>HttpServletRequest.getRequestURL</code> in Servlet 2.3.</p>
905      *
906      * @param request The servlet request we are processing
907      * @return URL representing the current request
908      * @throws MalformedURLException if a URL cannot be created
909      */
910     public static URL requestURL(HttpServletRequest request)
911         throws MalformedURLException {
912         StringBuffer url = requestToServerUriStringBuffer(request);
913 
914         return (new URL(url.toString()));
915     }
916 
917     /**
918      * <p>Return the URL representing the scheme, server, and port number of
919      * the current request. Server-relative URLs can be created by simply
920      * appending the server-relative path (starting with '/') to this.</p>
921      *
922      * @param request The servlet request we are processing
923      * @return URL representing the scheme, server, and port number of the
924      *         current request
925      * @throws MalformedURLException if a URL cannot be created
926      */
927     public static URL serverURL(HttpServletRequest request)
928         throws MalformedURLException {
929         StringBuffer url = requestToServerStringBuffer(request);
930 
931         return (new URL(url.toString()));
932     }
933 
934     /**
935      * <p>Return the string representing the scheme, server, and port number
936      * of the current request. Server-relative URLs can be created by simply
937      * appending the server-relative path (starting with '/') to this.</p>
938      *
939      * @param request The servlet request we are processing
940      * @return URL representing the scheme, server, and port number of the
941      *         current request
942      * @since Struts 1.2.0
943      */
944     public static StringBuffer requestToServerUriStringBuffer(
945         HttpServletRequest request) {
946         StringBuffer serverUri =
947             createServerUriStringBuffer(request.getScheme(),
948                 request.getServerName(), request.getServerPort(),
949                 request.getRequestURI());
950 
951         return serverUri;
952     }
953 
954     /**
955      * <p>Return <code>StringBuffer</code> representing the scheme, server,
956      * and port number of the current request. Server-relative URLs can be
957      * created by simply appending the server-relative path (starting with
958      * '/') to this.</p>
959      *
960      * @param request The servlet request we are processing
961      * @return URL representing the scheme, server, and port number of the
962      *         current request
963      * @since Struts 1.2.0
964      */
965     public static StringBuffer requestToServerStringBuffer(
966         HttpServletRequest request) {
967         return createServerStringBuffer(request.getScheme(),
968             request.getServerName(), request.getServerPort());
969     }
970 
971     /**
972      * <p>Return <code>StringBuffer</code> representing the scheme, server,
973      * and port number of the current request.</p>
974      *
975      * @param scheme The scheme name to use
976      * @param server The server name to use
977      * @param port   The port value to use
978      * @return StringBuffer in the form scheme: server: port
979      * @since Struts 1.2.0
980      */
981     public static StringBuffer createServerStringBuffer(String scheme,
982         String server, int port) {
983         StringBuffer url = new StringBuffer();
984 
985         if (port < 0) {
986             port = 80; // Work around java.net.URL bug
987         }
988 
989         url.append(scheme);
990         url.append("://");
991         url.append(server);
992 
993         if ((scheme.equals("http") && (port != 80))
994             || (scheme.equals("https") && (port != 443))) {
995             url.append(':');
996             url.append(port);
997         }
998 
999         return url;
1000     }
1001 
1002     /**
1003      * <p>Return <code>StringBuffer</code> representing the scheme, server,
1004      * and port number of the current request.</p>
1005      *
1006      * @param scheme The scheme name to use
1007      * @param server The server name to use
1008      * @param port   The port value to use
1009      * @param uri    The uri value to use
1010      * @return StringBuffer in the form scheme: server: port
1011      * @since Struts 1.2.0
1012      */
1013     public static StringBuffer createServerUriStringBuffer(String scheme,
1014         String server, int port, String uri) {
1015         StringBuffer serverUri = createServerStringBuffer(scheme, server, port);
1016 
1017         serverUri.append(uri);
1018 
1019         return serverUri;
1020     }
1021 
1022     /**
1023      * <p>Returns the true path of the destination action if the specified forward
1024      * is an action-aliased URL. This method version forms the URL based on
1025      * the current request; selecting the current module if the forward does not
1026      * explicitly contain a module path.</p>
1027      *
1028      * @param forward the forward config
1029      * @param request the current request
1030      * @param servlet the servlet handling the current request
1031      * @return the context-relative URL of the action if the forward has an action identifier; otherwise <code>null</code>.
1032      * @since Struts 1.3.6
1033      */
1034     public static String actionIdURL(ForwardConfig forward, HttpServletRequest request, ActionServlet servlet) {
1035         ModuleConfig moduleConfig = null;
1036         if (forward.getModule() != null) {
1037             String prefix = forward.getModule();
1038             moduleConfig = ModuleUtils.getInstance().getModuleConfig(prefix, servlet.getServletContext());
1039         } else {
1040             moduleConfig = ModuleUtils.getInstance().getModuleConfig(request);
1041         }
1042         return actionIdURL(forward.getPath(), moduleConfig, servlet);
1043     }
1044 
1045     /**
1046      * <p>Returns the true path of the destination action if the specified forward
1047      * is an action-aliased URL. This method version forms the URL based on
1048      * the specified module.
1049      *
1050      * @param originalPath the action-aliased path
1051      * @param moduleConfig the module config for this request
1052      * @param servlet the servlet handling the current request
1053      * @return the context-relative URL of the action if the path has an action identifier; otherwise <code>null</code>.
1054      * @since Struts 1.3.6
1055      */
1056     public static String actionIdURL(String originalPath, ModuleConfig moduleConfig, ActionServlet servlet) {
1057         if (originalPath.startsWith("http") || originalPath.startsWith("/")) {
1058             return null;
1059         }
1060 
1061         // Split the forward path into the resource and query string;
1062         // it is possible a forward (or redirect) has added parameters.
1063         String actionId = null;
1064         String qs = null;
1065         int qpos = originalPath.indexOf("?");
1066         if (qpos == -1) {
1067             actionId = originalPath;
1068         } else {
1069             actionId = originalPath.substring(0, qpos);
1070             qs = originalPath.substring(qpos);
1071         }
1072 
1073         // Find the action of the given actionId
1074         ActionConfig actionConfig = moduleConfig.findActionConfigId(actionId);
1075         if (actionConfig == null) {
1076             if (log.isDebugEnabled()) {
1077                 log.debug("No actionId found for " + actionId);
1078             }
1079             return null;
1080         }
1081 
1082         String path = actionConfig.getPath();
1083         String mapping = RequestUtils.getServletMapping(servlet);
1084         StringBuffer actionIdPath = new StringBuffer();
1085 
1086         // Form the path based on the servlet mapping pattern
1087         if (mapping.startsWith("*")) {
1088             actionIdPath.append(path);
1089             actionIdPath.append(mapping.substring(1));
1090         } else if (mapping.startsWith("/")) {  // implied ends with a *
1091             mapping = mapping.substring(0, mapping.length() - 1);
1092             if (mapping.endsWith("/") && path.startsWith("/")) {
1093                 actionIdPath.append(mapping);
1094                 actionIdPath.append(path.substring(1));
1095             } else {
1096                 actionIdPath.append(mapping);
1097                 actionIdPath.append(path);
1098             }
1099         } else {
1100             log.warn("Unknown servlet mapping pattern");
1101             actionIdPath.append(path);
1102         }
1103 
1104         // Lastly add any query parameters (the ? is part of the query string)
1105         if (qs != null) {
1106             actionIdPath.append(qs);
1107         }
1108 
1109         // Return the path
1110         if (log.isDebugEnabled()) {
1111             log.debug(originalPath + " unaliased to " + actionIdPath.toString());
1112         }
1113         return actionIdPath.toString();
1114     }
1115 }