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