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.extras.actions;
22  
23  import java.lang.reflect.InvocationTargetException;
24  import java.lang.reflect.Method;
25  import java.util.HashMap;
26  
27  import jakarta.servlet.ServletException;
28  import jakarta.servlet.http.HttpServletRequest;
29  import jakarta.servlet.http.HttpServletResponse;
30  
31  import org.apache.struts.Globals;
32  import org.apache.struts.action.Action;
33  import org.apache.struts.action.ActionForm;
34  import org.apache.struts.action.ActionForward;
35  import org.apache.struts.action.ActionMapping;
36  import org.apache.struts.chain.contexts.ActionContext;
37  import org.apache.struts.chain.contexts.ServletActionContext;
38  import org.apache.struts.dispatcher.Dispatcher;
39  import org.apache.struts.util.MessageResources;
40  import org.slf4j.Logger;
41  import org.slf4j.LoggerFactory;
42  
43  /**
44   * <p>Action <i>helper</i> class that dispatches to a public method in an
45   * Action.</p> <p/> <p>This class is provided as an alternative mechanism to
46   * using DispatchAction and its various flavours and means <i>Dispatch</i>
47   * behaviour can be easily implemented into any <code>Action</code> without
48   * having to inherit from a particular super <code>Action</code>.</p> <p/>
49   * <p>To implement <i>dispatch</i> behaviour in an <code>Action</code> class,
50   * create your custom Action as follows, along with the methods you require
51   * (and optionally "cancelled" and "unspecified" methods):</p> <p/>
52   * <pre>
53   *   public class MyCustomAction extends Action {
54   *
55   *       protected ActionDispatcher dispatcher
56   *                = new ActionDispatcher(this, ActionDispatcher.MAPPING_FLAVOR);
57   *
58   *       public ActionForward execute(ActionMapping mapping,
59   *                                    ActionForm form,
60   *                                    HttpServletRequest request,
61   *                                    HttpServletResponse response)
62   *                           throws Exception {
63   *           return dispatcher.execute(mapping, form, request, response);
64   *       }
65   *   }
66   * </pre>
67   * <p/>
68   *
69   * <p>It provides three flavours of determing the name of the method:</p>
70   *
71   * <ul>
72   *
73   * <li><strong>{@link #DEFAULT_FLAVOR}</strong> - uses the parameter
74   * specified in the struts-config.xml to get the method name from the Request
75   * (equivalent to <code>DispatchAction</code> <b>except</b> uses "method" as a
76   * default if the <code>parameter</code> is not specified in the
77   * struts-config.xml).</li>
78   *
79   * <li><strong>{@link #DISPATCH_FLAVOR}</strong>
80   * - uses the parameter specified in the struts-config.xml to get the method
81   * name from the Request (equivalent to <code>DispatchAction</code>).</li>
82   *
83   * <li><strong>{@link #MAPPING_FLAVOR}</strong> - uses the parameter
84   * specified in the struts-config.xml as the method name (equivalent to
85   * <code>MappingDispatchAction</code>).</li>
86  
87   * </ul>
88   *
89   * @version $Rev$ $Date$
90   * @since Struts 1.2.7
91   */
92  public class ActionDispatcher implements Dispatcher {
93      private static final long serialVersionUID = 3784151345188637566L;
94  
95      // ----------------------------------------------------- Instance Variables
96  
97      /**
98       * Indicates "default" dispatch flavor.
99       */
100     public static final int DEFAULT_FLAVOR = 0;
101 
102     /**
103      * Indicates "mapping" dispatch flavor.
104      */
105     public static final int MAPPING_FLAVOR = 1;
106 
107     /**
108      * Indicates flavor compatible with DispatchAction.
109      */
110     public static final int DISPATCH_FLAVOR = 2;
111 
112     /**
113      * The {@code Log} instance for this class.
114      */
115     private transient final Logger log =
116         LoggerFactory.getLogger(ActionDispatcher.class);
117 
118     /**
119      * The message resources for this package.
120      */
121     protected static MessageResources messages =
122         MessageResources.getMessageResources(
123             "org.apache.struts.extras.actions.LocalStrings");
124 
125     /**
126      * The associated Action to dispatch to.
127      */
128     protected Action actionInstance;
129 
130     /**
131      * Indicates dispatch <i>flavor</i>.
132      */
133     protected int flavor;
134 
135     /**
136      * The Class instance of this <code>DispatchAction</code> class.
137      */
138     protected Class<? extends Action> clazz;
139 
140     /**
141      * The set of Method objects we have introspected for this class, keyed by
142      * method name.  This collection is populated as different methods are
143      * called, so that introspection needs to occur only once per method
144      * name.
145      */
146     protected HashMap<String, Method> methods = new HashMap<>();
147 
148     /**
149      * The set of argument type classes for the reflected method call.  These
150      * are the same for all calls, so calculate them only once.
151      */
152     protected Class<?>[] types =
153         {
154             ActionMapping.class, ActionForm.class, HttpServletRequest.class,
155             HttpServletResponse.class
156         };
157 
158     // ----------------------------------------------------- Constructors
159 
160     /**
161      * Construct an instance of this class from the supplied parameters.
162      *
163      * @param actionInstance The action instance to be invoked.
164      */
165     public ActionDispatcher(Action actionInstance) {
166         this(actionInstance, DEFAULT_FLAVOR);
167     }
168 
169     /**
170      * Construct an instance of this class from the supplied parameters.
171      *
172      * @param actionInstance The action instance to be invoked.
173      * @param flavor         The flavor of dispatch to use.
174      */
175     public ActionDispatcher(Action actionInstance, int flavor) {
176         this.actionInstance = actionInstance;
177         this.flavor = flavor;
178 
179         clazz = actionInstance.getClass();
180     }
181 
182     // --------------------------------------------------------- Public Methods
183 
184     /**
185      * Process the specified HTTP request, and create the corresponding HTTP
186      * response (or forward to another web component that will create it).
187      * Return an <code>ActionForward</code> instance describing where and how
188      * control should be forwarded, or <code>null</code> if the response has
189      * already been completed.
190      *
191      * @param mapping  The ActionMapping used to select this instance
192      * @param form     The optional ActionForm bean for this request (if any)
193      * @param request  The HTTP request we are processing
194      * @param response The HTTP response we are creating
195      * @return The forward to which control should be transferred, or
196      *         <code>null</code> if the response has been completed.
197      * @throws Exception if an exception occurs
198      */
199     public ActionForward execute(ActionMapping mapping, ActionForm form,
200         HttpServletRequest request, HttpServletResponse response)
201         throws Exception {
202         // Process "cancelled"
203         if (isCancelled(request)) {
204             ActionForward af = cancelled(mapping, form, request, response);
205 
206             if (af != null) {
207                 return af;
208             }
209         }
210 
211         // Identify the request parameter containing the method name
212         String parameter = getParameter(mapping, form, request, response);
213 
214         // Get the method's name. This could be overridden in subclasses.
215         String name =
216             getMethodName(mapping, form, request, response, parameter);
217 
218         // Prevent recursive calls
219         if ("execute".equals(name) || "perform".equals(name)) {
220             String message =
221                 messages.getMessage("dispatch.recursive", mapping.getPath());
222 
223             log.error(message);
224             throw new ServletException(message);
225         }
226 
227         // Invoke the named method, and return the result
228         return dispatchMethod(mapping, form, request, response, name);
229     }
230 
231     /**
232      * <p>Dispatches to the target class' <code>unspecified</code> method, if
233      * present, otherwise throws a ServletException. Classes utilizing
234      * <code>ActionDispatcher</code> should provide an <code>unspecified</code>
235      * method if they wish to provide behavior different than throwing a
236      * ServletException.</p>
237      *
238      * @param mapping  The ActionMapping used to select this instance
239      * @param form     The optional ActionForm bean for this request (if any)
240      * @param request  The non-HTTP request we are processing
241      * @param response The non-HTTP response we are creating
242      * @return The forward to which control should be transferred, or
243      *         <code>null</code> if the response has been completed.
244      * @throws Exception if the application business logic throws an
245      *                   exception.
246      */
247     protected ActionForward unspecified(ActionMapping mapping, ActionForm form,
248         HttpServletRequest request, HttpServletResponse response)
249         throws Exception {
250         // Identify if there is an "unspecified" method to be dispatched to
251         String name = "unspecified";
252         Method method = null;
253 
254         try {
255             method = getMethod(name);
256         } catch (NoSuchMethodException e) {
257             String message =
258                 messages.getMessage("dispatch.parameter", mapping.getPath(),
259                     mapping.getParameter());
260 
261             log.error(message);
262 
263             throw new ServletException(message, e);
264         }
265 
266         return dispatchMethod(mapping, form, request, response, name, method);
267     }
268 
269     /**
270      * <p>Dispatches to the target class' cancelled method, if present,
271      * otherwise returns null. Classes utilizing <code>ActionDispatcher</code>
272      * should provide a <code>cancelled</code> method if they wish to provide
273      * behavior different than returning null.</p>
274      *
275      * @param mapping  The ActionMapping used to select this instance
276      * @param form     The optional ActionForm bean for this request (if any)
277      * @param request  The non-HTTP request we are processing
278      * @param response The non-HTTP response we are creating
279      * @return The forward to which control should be transferred, or
280      *         <code>null</code> if the response has been completed.
281      * @throws Exception if the application business logic throws an
282      *                   exception.
283      */
284     protected ActionForward cancelled(ActionMapping mapping, ActionForm form,
285         HttpServletRequest request, HttpServletResponse response)
286         throws Exception {
287         // Identify if there is an "cancelled" method to be dispatched to
288         String name = "cancelled";
289         Method method = null;
290 
291         try {
292             method = getMethod(name);
293         } catch (NoSuchMethodException e) {
294             return null;
295         }
296 
297         return dispatchMethod(mapping, form, request, response, name, method);
298     }
299 
300     // ----------------------------------------------------- Protected Methods
301 
302     /**
303      * Dispatch to the specified method.
304      *
305      * @param mapping  The ActionMapping used to select this instance
306      * @param form     The optional ActionForm bean for this request (if any)
307      * @param request  The non-HTTP request we are processing
308      * @param response The non-HTTP response we are creating
309      * @param name     The name of the method to invoke
310      * @return The forward to which control should be transferred, or
311      *         <code>null</code> if the response has been completed.
312      * @throws Exception if the application business logic throws an
313      *                   exception.
314      */
315     protected ActionForward dispatchMethod(ActionMapping mapping,
316         ActionForm form, HttpServletRequest request,
317         HttpServletResponse response, String name)
318         throws Exception {
319         // Make sure we have a valid method name to call.
320         // This may be null if the user hacks the query string.
321         if (name == null) {
322             return this.unspecified(mapping, form, request, response);
323         }
324 
325         // Identify the method object to be dispatched to
326         Method method = null;
327 
328         try {
329             method = getMethod(name);
330         } catch (NoSuchMethodException e) {
331             log.atError()
332                 .setMessage(() -> messages.getMessage("dispatch.method", mapping.getPath(), name))
333                 .setCause(e).log();
334 
335             String userMsg =
336                 messages.getMessage("dispatch.method.user", mapping.getPath());
337             NoSuchMethodException e2 = new NoSuchMethodException(userMsg);
338             e2.initCause(e);
339             throw e2;
340         }
341 
342         return dispatchMethod(mapping, form, request, response, name, method);
343     }
344 
345     /**
346      * Dispatch to the specified method.
347      *
348      * @param mapping  The ActionMapping used to select this instance
349      * @param form     The optional ActionForm bean for this request (if any)
350      * @param request  The non-HTTP request we are processing
351      * @param response The non-HTTP response we are creating
352      * @param name     The name of the method to invoke
353      * @param method   The method to invoke
354      * @return The forward to which control should be transferred, or
355      *         <code>null</code> if the response has been completed.
356      * @throws Exception if the application business logic throws an
357      *                   exception.
358      */
359     protected ActionForward dispatchMethod(ActionMapping mapping,
360         ActionForm form, HttpServletRequest request,
361         HttpServletResponse response, String name, Method method)
362         throws Exception {
363         ActionForward forward = null;
364 
365         try {
366             Object[] args = { mapping, form, request, response };
367 
368             forward = (ActionForward) method.invoke(actionInstance, args);
369         } catch (ClassCastException e) {
370             log.atError()
371                 .setMessage(() -> messages.getMessage("dispatch.return", mapping.getPath(), name))
372                 .setCause(e).log();
373             throw e;
374         } catch (IllegalAccessException e) {
375             log.atError()
376                 .setMessage(() -> messages.getMessage("dispatch.error", mapping.getPath(), name))
377                 .setCause(e).log();
378             throw e;
379         } catch (InvocationTargetException e) {
380             // Rethrow the target exception if possible so that the
381             // exception handling machinery can deal with it
382             Throwable t = e.getTargetException();
383 
384             if (t instanceof Exception) {
385                 throw ((Exception) t);
386             } else {
387                 log.atError()
388                     .setMessage(() -> messages.getMessage("dispatch.error", mapping.getPath(),
389                         name))
390                     .setCause(e).log();
391                 throw new ServletException(t);
392             }
393         }
394 
395         // Return the returned ActionForward instance
396         return (forward);
397     }
398 
399     /**
400      * Introspect the current class to identify a method of the specified name
401      * that accepts the same parameter types as the <code>execute</code>
402      * method does.
403      *
404      * @param name Name of the method to be introspected
405      * @return The method with the specified name.
406      * @throws NoSuchMethodException if no such method can be found
407      */
408     protected Method getMethod(String name)
409         throws NoSuchMethodException {
410         synchronized (methods) {
411             Method method = methods.get(name);
412 
413             if (method == null) {
414                 method = clazz.getMethod(name, types);
415                 methods.put(name, method);
416             }
417 
418             return (method);
419         }
420     }
421 
422     /**
423      * <p>Returns the parameter value as influenced by the selected {@link
424      * #flavor} specified for this <code>ActionDispatcher</code>.</p>
425      *
426      * @param mapping  The ActionMapping used to select this instance
427      * @param form     The optional ActionForm bean for this request (if any)
428      * @param request  The HTTP request we are processing
429      * @param response The HTTP response we are creating
430      * @return The <code>ActionMapping</code> parameter's value
431      * @throws Exception if an error occurs.
432      */
433     protected String getParameter(ActionMapping mapping, ActionForm form,
434         HttpServletRequest request, HttpServletResponse response)
435         throws Exception {
436         String parameter = mapping.getParameter();
437 
438         if ("".equals(parameter)) {
439             parameter = null;
440         }
441 
442         if ((parameter == null) && (flavor == DEFAULT_FLAVOR)) {
443             // use "method" for DEFAULT_FLAVOR if no parameter was provided
444             return "method";
445         }
446 
447         if ((parameter == null)
448             && ((flavor == MAPPING_FLAVOR) || (flavor == DISPATCH_FLAVOR))) {
449             String message =
450                 messages.getMessage("dispatch.handler", mapping.getPath());
451 
452             log.error(message);
453 
454             throw new ServletException(message);
455         }
456 
457         return parameter;
458     }
459 
460     /**
461      * Returns the method name, given a parameter's value.
462      *
463      * @param mapping   The ActionMapping used to select this instance
464      * @param form      The optional ActionForm bean for this request (if
465      *                  any)
466      * @param request   The HTTP request we are processing
467      * @param response  The HTTP response we are creating
468      * @param parameter The <code>ActionMapping</code> parameter's name
469      * @return The method's name.
470      * @throws Exception if an error occurs.
471      */
472     protected String getMethodName(ActionMapping mapping, ActionForm form,
473         HttpServletRequest request, HttpServletResponse response,
474         String parameter) throws Exception {
475         // "Mapping" flavor, defaults to "method"
476         if (flavor == MAPPING_FLAVOR) {
477             return parameter;
478         }
479 
480         // default behaviour
481         return request.getParameter(parameter);
482     }
483 
484     /**
485      * <p>Returns <code>true</code> if the current form's cancel button was
486      * pressed.  This method will check if the <code>Globals.CANCEL_KEY</code>
487      * request attribute has been set, which normally occurs if the cancel
488      * button generated by <strong>CancelTag</strong> was pressed by the user
489      * in the current request.  If <code>true</code>, validation performed by
490      * an <strong>ActionForm</strong>'s <code>validate()</code> method will
491      * have been skipped by the controller servlet.</p>
492      *
493      * @param request The servlet request we are processing
494      * @return <code>true</code> if the current form's cancel button was
495      *         pressed; <code>false</code> otherwise.
496      * @see org.apache.struts.taglib.html.CancelTag
497      */
498     protected boolean isCancelled(HttpServletRequest request) {
499         return (request.getAttribute(Globals.CANCEL_KEY) != null);
500     }
501 
502     /**
503      * @since Struts 1.4
504      */
505     public Object dispatch(ActionContext context) throws Exception {
506         ServletActionContext servletContext = (ServletActionContext) context;
507         return execute((ActionMapping) context.getActionConfig(), context.getActionForm(),
508             servletContext.getRequest(), servletContext.getResponse());
509     }
510 }