View Javadoc

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