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.action;
22  
23  import java.io.IOException;
24  import java.io.Serializable;
25  import java.util.HashMap;
26  import java.util.Locale;
27  
28  import jakarta.servlet.RequestDispatcher;
29  import jakarta.servlet.ServletContext;
30  import jakarta.servlet.ServletException;
31  import jakarta.servlet.http.HttpServletRequest;
32  import jakarta.servlet.http.HttpServletResponse;
33  import jakarta.servlet.http.HttpSession;
34  
35  import org.apache.struts.Globals;
36  import org.apache.struts.config.ActionConfig;
37  import org.apache.struts.config.ExceptionConfig;
38  import org.apache.struts.config.ForwardConfig;
39  import org.apache.struts.config.ModuleConfig;
40  import org.apache.struts.upload.MultipartRequestWrapper;
41  import org.apache.struts.util.MessageResources;
42  import org.apache.struts.util.RequestUtils;
43  import org.slf4j.Logger;
44  import org.slf4j.LoggerFactory;
45  
46  /**
47   * <p><strong>RequestProcessor</strong> contains the processing logic that the
48   * {@link ActionServlet} performs as it receives each servlet request from the
49   * container. You can customize the request processing behavior by subclassing
50   * this class and overriding the method(s) whose behavior you are interested
51   * in changing.</p>
52   *
53   * @version $Rev$ $Date$
54   * @since Struts 1.1
55   */
56  public class RequestProcessor implements Serializable {
57      private static final long serialVersionUID = -6430999735913386425L;
58  
59      // ----------------------------------------------------- Manifest Constants
60  
61      /**
62       * <p>The request attribute under which the path information is stored for
63       * processing during a <code>RequestDispatcher.include</code> call.</p>
64       */
65      public static final String INCLUDE_PATH_INFO =
66          "jakarta.servlet.include.path_info";
67  
68      /**
69       * <p>The request attribute under which the servlet path information is
70       * stored for processing during a <code>RequestDispatcher.include</code>
71       * call.</p>
72       */
73      public static final String INCLUDE_SERVLET_PATH =
74          "jakarta.servlet.include.servlet_path";
75  
76      /**
77       * The {@code Log} instance for this class.
78       */
79      private transient final Logger log =
80          LoggerFactory.getLogger(RequestProcessor.class);
81  
82      // ----------------------------------------------------- Instance Variables
83  
84      /**
85       * <p>The set of <code>Action</code> instances that have been created and
86       * initialized, keyed by the fully qualified Java class name of the
87       * <code>Action</code> class.</p>
88       */
89      protected HashMap<String, Action> actions = new HashMap<>();
90  
91      /**
92       * <p>The <code>ModuleConfiguration</code> with which we are
93       * associated.</p>
94       */
95      protected ModuleConfig moduleConfig = null;
96  
97      /**
98       * <p>The servlet with which we are associated.</p>
99       */
100     protected ActionServlet servlet = null;
101 
102     // --------------------------------------------------------- Public Methods
103 
104     /**
105      * <p>Clean up in preparation for a shutdown of this application.</p>
106      */
107     public void destroy() {
108         synchronized (this.actions) {
109             for (Action action : this.actions.values()) {
110                 action.setServlet(null);
111             }
112 
113             this.actions.clear();
114         }
115 
116         this.servlet = null;
117     }
118 
119     /**
120      * <p>Initialize this request processor instance.</p>
121      *
122      * @param servlet      The ActionServlet we are associated with
123      * @param moduleConfig The ModuleConfig we are associated with.
124      * @throws ServletException If an error occor during initialization
125      */
126     public void init(ActionServlet servlet, ModuleConfig moduleConfig)
127         throws ServletException {
128         synchronized (actions) {
129             actions.clear();
130         }
131 
132         this.servlet = servlet;
133         this.moduleConfig = moduleConfig;
134     }
135 
136     /**
137      * <p>Process an <code>HttpServletRequest</code> and create the
138      * corresponding <code>HttpServletResponse</code> or dispatch to another
139      * resource.</p>
140      *
141      * @param request  The servlet request we are processing
142      * @param response The servlet response we are creating
143      * @throws IOException      if an input/output error occurs
144      * @throws ServletException if a processing exception occurs
145      */
146     public void process(HttpServletRequest request, HttpServletResponse response)
147         throws IOException, ServletException {
148         // Wrap multipart requests with a special wrapper
149         request = processMultipart(request);
150 
151         // Identify the path component we will use to select a mapping
152         String path = processPath(request, response);
153 
154         if (path == null) {
155             return;
156         }
157 
158         log.debug("Processing a '{}' for path '{}'",
159             request.getMethod(), path);
160 
161         // Select a Locale for the current user if requested
162         processLocale(request, response);
163 
164         // Set the content type and no-caching headers if requested
165         processContent(request, response);
166         processNoCache(request, response);
167 
168         // General purpose preprocessing hook
169         if (!processPreprocess(request, response)) {
170             return;
171         }
172 
173         this.processCachedMessages(request, response);
174 
175         // Identify the mapping for this request
176         ActionMapping mapping = processMapping(request, response, path);
177 
178         if (mapping == null) {
179             return;
180         }
181 
182         // Check for any role required to perform this action
183         if (!processRoles(request, response, mapping)) {
184             return;
185         }
186 
187         // Process any ActionForm bean related to this request
188         ActionForm form = processActionForm(request, response, mapping);
189 
190         processPopulate(request, response, form, mapping);
191 
192         // Validate any fields of the ActionForm bean, if applicable
193         try {
194             if (!processValidate(request, response, form, mapping)) {
195                 return;
196             }
197         } catch (InvalidCancelException e) {
198             ActionForward forward = processException(request, response, e, form, mapping);
199             processForwardConfig(request, response, forward);
200             return;
201         } catch (IOException e) {
202             throw e;
203         } catch (ServletException e) {
204             throw e;
205         }
206 
207         // Process a forward or include specified by this mapping
208         if (!processForward(request, response, mapping)) {
209             return;
210         }
211 
212         if (!processInclude(request, response, mapping)) {
213             return;
214         }
215 
216         // Create or acquire the Action instance to process this request
217         Action action = processActionCreate(request, response, mapping);
218 
219         if (action == null) {
220             return;
221         }
222 
223         // Call the Action instance itself
224         ActionForward forward =
225             processActionPerform(request, response, action, form, mapping);
226 
227         // Process the returned ActionForward instance
228         processForwardConfig(request, response, forward);
229     }
230 
231     // ----------------------------------------------------- Processing Methods
232 
233     /**
234      * <p>Return an <code>Action</code> instance that will be used to process
235      * the current request, creating a new one if necessary.</p>
236      *
237      * @param request  The servlet request we are processing
238      * @param response The servlet response we are creating
239      * @param mapping  The mapping we are using
240      * @return An <code>Action</code> instance that will be used to process
241      *         the current request.
242      * @throws IOException if an input/output error occurs
243      */
244     protected Action processActionCreate(HttpServletRequest request,
245         HttpServletResponse response, ActionMapping mapping)
246         throws IOException {
247         // Acquire the Action instance we will be using (if there is one)
248         String className = mapping.getType();
249 
250         log.debug(" Looking for Action instance for class {}", className);
251 
252         // If there were a mapping property indicating whether
253         // an Action were a singleton or not ([true]),
254         // could we just instantiate and return a new instance here?
255         Action instance;
256 
257         synchronized (actions) {
258             // Return any existing Action instance of this class
259             instance = actions.get(className);
260 
261             if (instance != null) {
262                 log.trace("  Returning existing Action instance");
263 
264                 return (instance);
265             }
266 
267             // Create and return a new Action instance
268             log.trace("  Creating new Action instance");
269 
270             try {
271                 instance = (Action) RequestUtils.applicationInstance(className);
272 
273                 // Maybe we should propagate this exception
274                 // instead of returning null.
275             } catch (Exception e) {
276                 log.atError()
277                     .setMessage(() -> getInternal().getMessage("actionCreate",
278                         mapping.getPath(), mapping.toString()))
279                     .setCause(e).log();
280 
281                 response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
282                     getInternal().getMessage("actionCreate", mapping.getPath()));
283 
284                 return (null);
285             }
286 
287             actions.put(className, instance);
288 
289             if (instance.getServlet() == null) {
290                 instance.setServlet(this.servlet);
291             }
292         }
293 
294         return (instance);
295     }
296 
297     /**
298      * <p>Retrieve and return the <code>ActionForm</code> associated with this
299      * mapping, creating and retaining one if necessary. If there is no
300      * <code>ActionForm</code> associated with this mapping, return
301      * <code>null</code>.</p>
302      *
303      * @param request  The servlet request we are processing
304      * @param response The servlet response we are creating
305      * @param mapping  The mapping we are using
306      * @return The <code>ActionForm</code> associated with this mapping.
307      */
308     protected ActionForm processActionForm(HttpServletRequest request,
309         HttpServletResponse response, ActionMapping mapping) {
310         // Create (if necessary) a form bean to use
311         ActionForm instance =
312             RequestUtils.createActionForm(request, mapping, moduleConfig,
313                 servlet);
314 
315         if (instance == null) {
316             return (null);
317         }
318 
319         // Store the new instance in the appropriate scope
320         log.debug(" Storing ActionForm bean instance in scope '{}' "
321             + "under attribute key '{}'",
322             mapping.getScope(), mapping.getAttribute());
323 
324         if ("request".equals(mapping.getScope())) {
325             request.setAttribute(mapping.getAttribute(), instance);
326         } else {
327             HttpSession session = request.getSession();
328 
329             session.setAttribute(mapping.getAttribute(), instance);
330         }
331 
332         return (instance);
333     }
334 
335     /**
336      * <p>Forward or redirect to the specified destination, by the specified
337      * mechanism.  This method uses a <code>ForwardConfig</code> object
338      * instead an <code>ActionForward</code>.</p>
339      *
340      * @param request  The servlet request we are processing
341      * @param response The servlet response we are creating
342      * @param forward  The ForwardConfig controlling where we go next
343      * @throws IOException      if an input/output error occurs
344      * @throws ServletException if a servlet exception occurs
345      */
346     protected void processForwardConfig(HttpServletRequest request,
347         HttpServletResponse response, ForwardConfig forward)
348         throws IOException, ServletException {
349         if (forward == null) {
350             return;
351         }
352 
353         log.debug("processForwardConfig({})", forward);
354 
355         String forwardPath = forward.getPath();
356         String uri;
357 
358         // If the forward can be unaliased into an action, then use the path of the action
359         String actionIdPath = RequestUtils.actionIdURL(forward, request, servlet);
360         if (actionIdPath != null) {
361             forwardPath = actionIdPath;
362             ForwardConfig actionIdForward = new ForwardConfig(forward);
363             actionIdForward.setPath(actionIdPath);
364             forward = actionIdForward;
365         }
366 
367         // paths not starting with / should be passed through without any
368         // processing (ie. they're absolute)
369         if (forwardPath.startsWith("/")) {
370             // get module relative uri
371             uri = RequestUtils.forwardURL(request, forward, null);
372         } else {
373             uri = forwardPath;
374         }
375 
376         if (forward.getRedirect()) {
377             // only prepend context path for relative uri
378             if (uri.startsWith("/")) {
379                 uri = request.getContextPath() + uri;
380             }
381 
382             response.sendRedirect(response.encodeRedirectURL(uri));
383         } else {
384             doForward(uri, request, response);
385         }
386     }
387 
388     // :FIXME: if Action.execute throws Exception, and Action.process has been
389     // removed, should the process* methods still throw IOException,
390     // ServletException?
391 
392     /**
393      * <P>Ask the specified <code>Action</code> instance to handle this
394      * request. Return the <code>ActionForward</code> instance (if any)
395      * returned by the called <code>Action</code> for further processing.
396      * </P>
397      *
398      * @param request  The servlet request we are processing
399      * @param response The servlet response we are creating
400      * @param action   The Action instance to be used
401      * @param form     The ActionForm instance to pass to this Action
402      * @param mapping  The ActionMapping instance to pass to this Action
403      * @return The <code>ActionForward</code> instance (if any) returned by
404      *         the called <code>Action</code>.
405      * @throws IOException      if an input/output error occurs
406      * @throws ServletException if a servlet exception occurs
407      */
408     protected ActionForward processActionPerform(HttpServletRequest request,
409         HttpServletResponse response, Action action, ActionForm form,
410         ActionMapping mapping)
411         throws IOException, ServletException {
412         try {
413             return (action.execute(mapping, form, request, response));
414         } catch (Exception e) {
415             return (processException(request, response, e, form, mapping));
416         }
417     }
418 
419     /**
420      * <p>Removes any <code>ActionMessages</code> object stored in the session
421      * under <code>Globals.MESSAGE_KEY</code> and <code>Globals.ERROR_KEY</code>
422      * if the messages' <code>isAccessed</code> method returns true.  This
423      * allows messages to be stored in the session, display one time, and be
424      * released here.</p>
425      *
426      * @param request  The servlet request we are processing.
427      * @param response The servlet response we are creating.
428      * @since Struts 1.2
429      */
430     protected void processCachedMessages(HttpServletRequest request,
431         HttpServletResponse response) {
432         HttpSession session = request.getSession(false);
433 
434         if (session == null) {
435             return;
436         }
437 
438         // Remove messages as needed
439         ActionMessages messages =
440             (ActionMessages) session.getAttribute(Globals.MESSAGE_KEY);
441 
442         if (messages != null) {
443             if (messages.isAccessed()) {
444                 session.removeAttribute(Globals.MESSAGE_KEY);
445             }
446         }
447 
448         // Remove error messages as needed
449         messages = (ActionMessages) session.getAttribute(Globals.ERROR_KEY);
450 
451         if (messages != null) {
452             if (messages.isAccessed()) {
453                 session.removeAttribute(Globals.ERROR_KEY);
454             }
455         }
456     }
457 
458     /**
459      * <p>Set the default content type (with optional character encoding) for
460      * all responses if requested.  <strong>NOTE</strong> - This header will
461      * be overridden automatically if a <code>RequestDispatcher.forward</code>
462      * call is ultimately invoked.</p>
463      *
464      * @param request  The servlet request we are processing
465      * @param response The servlet response we are creating
466      */
467     protected void processContent(HttpServletRequest request,
468         HttpServletResponse response) {
469         String contentType =
470             moduleConfig.getControllerConfig().getContentType();
471 
472         if (contentType != null) {
473             response.setContentType(contentType);
474         }
475     }
476 
477     /**
478      * <p>Ask our exception handler to handle the exception. Return the
479      * <code>ActionForward</code> instance (if any) returned by the called
480      * <code>ExceptionHandler</code>.</p>
481      *
482      * @param request   The servlet request we are processing
483      * @param response  The servlet response we are processing
484      * @param exception The exception being handled
485      * @param form      The ActionForm we are processing
486      * @param mapping   The ActionMapping we are using
487      * @return The <code>ActionForward</code> instance (if any) returned by
488      *         the called <code>ExceptionHandler</code>.
489      * @throws IOException      if an input/output error occurs
490      * @throws ServletException if a servlet exception occurs
491      */
492     protected ActionForward processException(HttpServletRequest request,
493         HttpServletResponse response, Exception exception, ActionForm form,
494         ActionMapping mapping)
495         throws IOException, ServletException {
496         // Is there a defined handler for this exception?
497         ExceptionConfig config = mapping.findException(exception.getClass());
498 
499         if (config == null) {
500             log.atWarn().log(() -> getInternal().getMessage("unhandledException",
501                     exception.getClass()));
502 
503             if (exception instanceof IOException) {
504                 throw (IOException) exception;
505             } else if (exception instanceof ServletException) {
506                 throw (ServletException) exception;
507             } else {
508                 throw new ServletException(exception);
509             }
510         }
511 
512         // Use the configured exception handling
513         try {
514             ExceptionHandler handler =
515                 (ExceptionHandler) RequestUtils.applicationInstance(config
516                     .getHandler());
517 
518             return (handler.execute(exception, config, mapping, form, request,
519                 response));
520         } catch (Exception e) {
521             throw new ServletException(e);
522         }
523     }
524 
525     /**
526      * <p>Process a forward requested by this mapping (if any). Return
527      * <code>true</code> if standard processing should continue, or
528      * <code>false</code> if we have already handled this request.</p>
529      *
530      * @param request  The servlet request we are processing
531      * @param response The servlet response we are creating
532      * @param mapping  The ActionMapping we are using
533      * @return <code>true</code> to continue normal processing;
534      *         <code>false</code> if a response has been created.
535      * @throws IOException      if an input/output error occurs
536      * @throws ServletException if a servlet exception occurs
537      */
538     protected boolean processForward(HttpServletRequest request,
539         HttpServletResponse response, ActionMapping mapping)
540         throws IOException, ServletException {
541         // Are we going to processing this request?
542         String forward = mapping.getForward();
543 
544         if (forward == null) {
545             return (true);
546         }
547 
548         // If the forward can be unaliased into an action, then use the path of the action
549         String actionIdPath = RequestUtils.actionIdURL(forward, this.moduleConfig, this.servlet);
550         if (actionIdPath != null) {
551             forward = actionIdPath;
552         }
553 
554         internalModuleRelativeForward(forward, request, response);
555 
556         return (false);
557     }
558 
559     /**
560      * <p>Process an include requested by this mapping (if any). Return
561      * <code>true</code> if standard processing should continue, or
562      * <code>false</code> if we have already handled this request.</p>
563      *
564      * @param request  The servlet request we are processing
565      * @param response The servlet response we are creating
566      * @param mapping  The ActionMapping we are using
567      * @return <code>true</code> to continue normal processing;
568      *         <code>false</code> if a response has been created.
569      * @throws IOException      if an input/output error occurs
570      * @throws ServletException if thrown by invoked methods
571      */
572     protected boolean processInclude(HttpServletRequest request,
573         HttpServletResponse response, ActionMapping mapping)
574         throws IOException, ServletException {
575         // Are we going to processing this request?
576         String include = mapping.getInclude();
577 
578         if (include == null) {
579             return (true);
580         }
581 
582         // If the forward can be unaliased into an action, then use the path of the action
583         String actionIdPath = RequestUtils.actionIdURL(include, this.moduleConfig, this.servlet);
584         if (actionIdPath != null) {
585             include = actionIdPath;
586         }
587 
588         internalModuleRelativeInclude(include, request, response);
589 
590         return (false);
591     }
592 
593     /**
594      * <p>Automatically select a <code>Locale</code> for the current user, if
595      * requested. <strong>NOTE</strong> - configuring Locale selection will
596      * trigger the creation of a new <code>HttpSession</code> if
597      * necessary.</p>
598      *
599      * @param request  The servlet request we are processing
600      * @param response The servlet response we are creating
601      */
602     protected void processLocale(HttpServletRequest request,
603         HttpServletResponse response) {
604         // Are we configured to select the Locale automatically?
605         if (!moduleConfig.getControllerConfig().getLocale()) {
606             return;
607         }
608 
609         // Has a Locale already been selected?
610         HttpSession session = request.getSession();
611 
612         if (session.getAttribute(Globals.LOCALE_KEY) != null) {
613             return;
614         }
615 
616         // Use the Locale returned by the servlet container (if any)
617         Locale locale = request.getLocale();
618 
619         if (locale != null) {
620             log.debug(" Setting user locale '{}'", locale);
621 
622             session.setAttribute(Globals.LOCALE_KEY, locale);
623         }
624     }
625 
626     /**
627      * <p>Select the mapping used to process the selection path for this
628      * request. If no mapping can be identified, create an error response and
629      * return <code>null</code>.</p>
630      *
631      * @param request  The servlet request we are processing
632      * @param response The servlet response we are creating
633      * @param path     The portion of the request URI for selecting a mapping
634      * @return The mapping used to process the selection path for this
635      *         request.
636      * @throws IOException if an input/output error occurs
637      */
638     protected ActionMapping processMapping(HttpServletRequest request,
639         HttpServletResponse response, String path)
640         throws IOException {
641         // Is there a mapping for this path?
642         ActionMapping mapping =
643             (ActionMapping) moduleConfig.findActionConfig(path);
644 
645         // If a mapping is found, put it in the request and return it
646         if (mapping != null) {
647             request.setAttribute(Globals.MAPPING_KEY, mapping);
648 
649             return (mapping);
650         }
651 
652         // Locate the mapping for unknown paths (if any)
653         ActionConfig[] configs = moduleConfig.findActionConfigs();
654 
655         for (int i = 0; i < configs.length; i++) {
656             if (configs[i].getUnknown()) {
657                 mapping = (ActionMapping) configs[i];
658                 request.setAttribute(Globals.MAPPING_KEY, mapping);
659 
660                 return (mapping);
661             }
662         }
663 
664         // No mapping can be found to process this request
665         String msg = getInternal().getMessage("processInvalid");
666 
667         log.error("{} {}", msg, path);
668         response.sendError(HttpServletResponse.SC_NOT_FOUND, msg);
669 
670         return null;
671     }
672 
673     /**
674      * <p>If this is a multipart request, wrap it with a special wrapper.
675      * Otherwise, return the request unchanged.</p>
676      *
677      * @param request The HttpServletRequest we are processing
678      * @return A wrapped request, if the request is multipart; otherwise the
679      *         original request.
680      */
681     protected HttpServletRequest processMultipart(HttpServletRequest request) {
682         if (!"POST".equalsIgnoreCase(request.getMethod())) {
683             return (request);
684         }
685 
686         String contentType = request.getContentType();
687 
688         if ((contentType != null)
689             && contentType.startsWith("multipart/form-data")) {
690             return (new MultipartRequestWrapper(request));
691         } else {
692             return (request);
693         }
694     }
695 
696     /**
697      * <p>Set the no-cache headers for all responses, if requested.
698      * <strong>NOTE</strong> - This header will be overridden automatically if
699      * a <code>RequestDispatcher.forward</code> call is ultimately
700      * invoked.</p>
701      *
702      * @param request  The servlet request we are processing
703      * @param response The servlet response we are creating
704      */
705     protected void processNoCache(HttpServletRequest request,
706         HttpServletResponse response) {
707         if (moduleConfig.getControllerConfig().getNocache()) {
708             response.setHeader("Pragma", "No-cache");
709             response.setHeader("Cache-Control", "no-cache,no-store,max-age=0");
710             response.setDateHeader("Expires", 1);
711         }
712     }
713 
714     /**
715      * <p>Identify and return the path component (from the request URI) that
716      * we will use to select an <code>ActionMapping</code> with which to
717      * dispatch. If no such path can be identified, create an error response
718      * and return <code>null</code>.</p>
719      *
720      * @param request  The servlet request we are processing
721      * @param response The servlet response we are creating
722      * @return The path that will be used to select an action mapping.
723      * @throws IOException if an input/output error occurs
724      */
725     protected String processPath(HttpServletRequest request,
726         HttpServletResponse response)
727         throws IOException {
728         String path;
729 
730         // Set per request the original path for postback forms
731         if (request.getAttribute(Globals.ORIGINAL_URI_KEY) == null) {
732             request.setAttribute(Globals.ORIGINAL_URI_KEY, request.getServletPath());
733         }
734 
735         // For prefix matching, match on the path info (if any)
736         path = (String) request.getAttribute(INCLUDE_PATH_INFO);
737 
738         if (path == null) {
739             path = request.getPathInfo();
740         }
741 
742         if ((path != null) && (path.length() > 0)) {
743             return (path);
744         }
745 
746         // For extension matching, strip the module prefix and extension
747         path = (String) request.getAttribute(INCLUDE_SERVLET_PATH);
748 
749         if (path == null) {
750             path = request.getServletPath();
751         }
752 
753         String prefix = moduleConfig.getPrefix();
754 
755         if (!path.startsWith(prefix)) {
756             String msg = getInternal().getMessage("processPath");
757 
758             log.error("{} {}", msg, request.getRequestURI());
759             response.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
760 
761             return null;
762         }
763 
764         path = path.substring(prefix.length());
765 
766         int slash = path.lastIndexOf("/");
767         int period = path.lastIndexOf(".");
768 
769         if ((period >= 0) && (period > slash)) {
770             path = path.substring(0, period);
771         }
772 
773         return (path);
774     }
775 
776     /**
777      * <p>Populate the properties of the specified <code>ActionForm</code>
778      * instance from the request parameters included with this request.  In
779      * addition, request attribute <code>Globals.CANCEL_KEY</code> will be set
780      * if the request was submitted with a button created by
781      * <code>CancelTag</code>.</p>
782      *
783      * @param request  The servlet request we are processing
784      * @param response The servlet response we are creating
785      * @param form     The ActionForm instance we are populating
786      * @param mapping  The ActionMapping we are using
787      * @throws ServletException if thrown by RequestUtils.populate()
788      */
789     protected void processPopulate(HttpServletRequest request,
790         HttpServletResponse response, ActionForm form, ActionMapping mapping)
791         throws ServletException {
792         if (form == null) {
793             return;
794         }
795 
796         // Populate the bean properties of this ActionForm instance
797         log.debug(" Populating bean properties from this request");
798 
799         form.setServlet(this.servlet);
800         form.reset(mapping, request);
801 
802         if (mapping.getMultipartClass() != null) {
803             request.setAttribute(Globals.MULTIPART_KEY,
804                 mapping.getMultipartClass());
805         }
806 
807         RequestUtils.populate(form, mapping.getPrefix(), mapping.getSuffix(),
808             request);
809 
810         // Set the cancellation request attribute if appropriate
811         if ((request.getParameter(Globals.CANCEL_PROPERTY) != null)
812             || (request.getParameter(Globals.CANCEL_PROPERTY_X) != null)) {
813             request.setAttribute(Globals.CANCEL_KEY, Boolean.TRUE);
814         }
815     }
816 
817     /**
818      * <p>General-purpose preprocessing hook that can be overridden as
819      * required by subclasses. Return <code>true</code> if you want standard
820      * processing to continue, or <code>false</code> if the response has
821      * already been completed. The default implementation does nothing.</p>
822      *
823      * @param request  The servlet request we are processing
824      * @param response The servlet response we are creating
825      * @return <code>true</code> to continue normal processing;
826      *         <code>false</code> if a response has been created.
827      */
828     protected boolean processPreprocess(HttpServletRequest request,
829         HttpServletResponse response) {
830         return (true);
831     }
832 
833     /**
834      * <p>If this action is protected by security roles, make sure that the
835      * current user possesses at least one of them.  Return <code>true</code>
836      * to continue normal processing, or <code>false</code> if an appropriate
837      * response has been created and processing should terminate.</p>
838      *
839      * @param request  The servlet request we are processing
840      * @param response The servlet response we are creating
841      * @param mapping  The mapping we are using
842      * @return <code>true</code> to continue normal processing;
843      *         <code>false</code> if a response has been created.
844      * @throws IOException      if an input/output error occurs
845      * @throws ServletException if a servlet exception occurs
846      */
847     protected boolean processRoles(HttpServletRequest request,
848         HttpServletResponse response, ActionMapping mapping)
849         throws IOException, ServletException {
850         // Is this action protected by role requirements?
851         String[] roles = mapping.getRoleNames();
852 
853         if ((roles == null) || (roles.length < 1)) {
854             return (true);
855         }
856 
857         // Check the current user against the list of required roles
858         for (int i = 0; i < roles.length; i++) {
859             if (request.isUserInRole(roles[i])) {
860                 log.debug(" User '{}' has role '{}', granting access",
861                     request.getRemoteUser(), roles[i]);
862 
863                 return (true);
864             }
865         }
866 
867         // The current user is not authorized for this action
868         log.debug(" User '{}' does not have any required role, denying access",
869             request.getRemoteUser());
870 
871         response.sendError(HttpServletResponse.SC_FORBIDDEN,
872             getInternal().getMessage("notAuthorized", mapping.getPath()));
873 
874         return (false);
875     }
876 
877     /**
878      * <p>If this request was not cancelled, and the request's {@link
879      * ActionMapping} has not disabled validation, call the
880      * <code>validate</code> method of the specified {@link ActionForm}, and
881      * forward to the input path if there were any errors. Return
882      * <code>true</code> if we should continue processing, or
883      * <code>false</code> if we have already forwarded control back to the
884      * input form.</p>
885      *
886      * @param request  The servlet request we are processing
887      * @param response The servlet response we are creating
888      * @param form     The ActionForm instance we are populating
889      * @param mapping  The ActionMapping we are using
890      * @return <code>true</code> to continue normal processing;
891      *         <code>false</code> if a response has been created.
892      * @throws IOException      if an input/output error occurs
893      * @throws ServletException if a servlet exception occurs
894      * @throws InvalidCancelException if a cancellation is attempted
895      *         without the proper action configuration.
896      */
897     protected boolean processValidate(HttpServletRequest request,
898         HttpServletResponse response, ActionForm form, ActionMapping mapping)
899         throws IOException, ServletException, InvalidCancelException {
900         if (form == null) {
901             return (true);
902         }
903 
904         // Has validation been turned off for this mapping?
905         if (!mapping.getValidate()) {
906             return (true);
907         }
908 
909         // Was this request cancelled? If it has been, the mapping also
910         // needs to state whether the cancellation is permissable; otherwise
911         // the cancellation is considered to be a symptom of a programmer
912         // error or a spoof.
913         if (request.getAttribute(Globals.CANCEL_KEY) != null) {
914             if (mapping.getCancellable()) {
915                 log.debug(" Cancelled transaction, skipping validation");
916                 return (true);
917             } else {
918                 request.removeAttribute(Globals.CANCEL_KEY);
919                 throw new InvalidCancelException();
920             }
921         }
922 
923         // Call the form bean's validation method
924         log.debug(" Validating input form properties");
925 
926         ActionMessages errors = form.validate(mapping, request);
927 
928         if ((errors == null) || errors.isEmpty()) {
929             log.trace("  No errors detected, accepting input");
930 
931             return (true);
932         }
933 
934         // Special handling for multipart request
935         if (form.getMultipartRequestHandler() != null) {
936             log.trace("  Rolling back multipart request");
937 
938             form.getMultipartRequestHandler().rollback();
939         }
940 
941         // Was an input path (or forward) specified for this mapping?
942         String input = mapping.getInput();
943 
944         if (input == null) {
945             log.trace("  Validation failed but no input form available");
946 
947             response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
948                 getInternal().getMessage("noInput", mapping.getPath()));
949 
950             return (false);
951         }
952 
953         // Save our error messages and return to the input form if possible
954         log.debug(" Validation failed, returning to '{}'", input);
955 
956         request.setAttribute(Globals.ERROR_KEY, errors);
957 
958         if (moduleConfig.getControllerConfig().getInputForward()) {
959             ForwardConfig forward = mapping.findForward(input);
960 
961             processForwardConfig(request, response, forward);
962         } else {
963             internalModuleRelativeForward(input, request, response);
964         }
965 
966         return (false);
967     }
968 
969     /**
970      * <p>Do a module relative forward to specified URI using request
971      * dispatcher. URI is relative to the current module. The real URI is
972      * compute by prefixing the module name.</p> <p>This method is used
973      * internally and is not part of the public API. It is advised to not use
974      * it in subclasses. </p>
975      *
976      * @param uri      Module-relative URI to forward to
977      * @param request  Current page request
978      * @param response Current page response
979      * @throws IOException      if an input/output error occurs
980      * @throws ServletException if a servlet exception occurs
981      * @since Struts 1.1
982      */
983     protected void internalModuleRelativeForward(String uri,
984         HttpServletRequest request, HttpServletResponse response)
985         throws IOException, ServletException {
986         // Construct a request dispatcher for the specified path
987         uri = moduleConfig.getPrefix() + uri;
988 
989         // Delegate the processing of this request
990         // :FIXME: - exception handling?
991         log.debug(" Delegating via forward to '{}'", uri);
992 
993         doForward(uri, request, response);
994     }
995 
996     /**
997      * <p>Do a module relative include to specified URI using request
998      * dispatcher. URI is relative to the current module. The real URI is
999      * compute by prefixing the module name.</p> <p>This method is used
1000      * internally and is not part of the public API. It is advised to not use
1001      * it in subclasses.</p>
1002      *
1003      * @param uri      Module-relative URI to include
1004      * @param request  Current page request
1005      * @param response Current page response
1006      * @throws IOException      if an input/output error occurs
1007      * @throws ServletException if a servlet exception occurs
1008      * @since Struts 1.1
1009      */
1010     protected void internalModuleRelativeInclude(String uri,
1011         HttpServletRequest request, HttpServletResponse response)
1012         throws IOException, ServletException {
1013         // Construct a request dispatcher for the specified path
1014         uri = moduleConfig.getPrefix() + uri;
1015 
1016         // Delegate the processing of this request
1017         // FIXME - exception handling?
1018         log.debug(" Delegating via include to '{}'", uri);
1019 
1020         doInclude(uri, request, response);
1021     }
1022 
1023     /**
1024      * <p>Do a forward to specified URI using a <code>RequestDispatcher</code>.
1025      * This method is used by all internal method needing to do a
1026      * forward.</p>
1027      *
1028      * @param uri      Context-relative URI to forward to
1029      * @param request  Current page request
1030      * @param response Current page response
1031      * @throws IOException      if an input/output error occurs
1032      * @throws ServletException if a servlet exception occurs
1033      * @since Struts 1.1
1034      */
1035     protected void doForward(String uri, HttpServletRequest request,
1036         HttpServletResponse response)
1037         throws IOException, ServletException {
1038         RequestDispatcher rd = getServletContext().getRequestDispatcher(uri);
1039 
1040         if (rd == null) {
1041             response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
1042                 getInternal().getMessage("requestDispatcher", uri));
1043 
1044             return;
1045         }
1046 
1047         rd.forward(request, response);
1048     }
1049 
1050     /**
1051      * <p>Do an include of specified URI using a <code>RequestDispatcher</code>.
1052      * This method is used by all internal method needing to do an
1053      * include.</p>
1054      *
1055      * @param uri      Context-relative URI to include
1056      * @param request  Current page request
1057      * @param response Current page response
1058      * @throws IOException      if an input/output error occurs
1059      * @throws ServletException if a servlet exception occurs
1060      * @since Struts 1.1
1061      */
1062     protected void doInclude(String uri, HttpServletRequest request,
1063         HttpServletResponse response)
1064         throws IOException, ServletException {
1065         RequestDispatcher rd = getServletContext().getRequestDispatcher(uri);
1066 
1067         if (rd == null) {
1068             response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
1069                 getInternal().getMessage("requestDispatcher", uri));
1070 
1071             return;
1072         }
1073 
1074         rd.include(request, response);
1075     }
1076 
1077     // -------------------------------------------------------- Support Methods
1078 
1079     /**
1080      * <p>Return the <code>MessageResources</code> instance containing our
1081      * internal message strings.</p>
1082      *
1083      * @return The <code>MessageResources</code> instance containing our
1084      *         internal message strings.
1085      */
1086     protected MessageResources getInternal() {
1087         return (servlet.getInternal());
1088     }
1089 
1090     /**
1091      * <p>Return the <code>ServletContext</code> for the web application in
1092      * which we are running.</p>
1093      *
1094      * @return The <code>ServletContext</code> for the web application.
1095      */
1096     protected ServletContext getServletContext() {
1097         return (servlet.getServletContext());
1098     }
1099 }