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.action.ActionForm;
32  import org.apache.struts.action.ActionForward;
33  import org.apache.struts.action.ActionMapping;
34  import org.slf4j.Logger;
35  import org.slf4j.LoggerFactory;
36  
37  /**
38   * <p>An abstract <strong>Action</strong> that dispatches to a public method
39   * that is named by the request parameter whose name is specified by the
40   * <code>parameter</code> property of the corresponding ActionMapping.  This
41   * Action is useful for developers who prefer to combine many similar actions
42   * into a single Action class, in order to simplify their application
43   * design.</p>
44   *
45   * <p>To configure the use of this action in your <code>struts-config.xml</code>
46   * file, create an entry like this:</p>
47   *
48   * {@code &lt;action path="/saveSubscription"
49   * type="org.apache.struts.extras.actions.DispatchAction"
50   * name="subscriptionForm" scope="request" input="/subscription.jsp"
51   * parameter="method"/&gt;}
52   *
53   * <p>which will use the value of the request parameter named "method" to pick
54   * the appropriate "execute" method, which must have the same signature (other
55   * than method name) of the standard Action.execute method.  For example, you
56   * might have the following three methods in the same action:</p>
57   *
58   * <ul>
59   *
60   * <li>public ActionForward delete(ActionMapping mapping, ActionForm form,
61   * HttpServletRequest request, HttpServletResponse response) throws
62   * Exception</li>
63   *
64   * <li>public ActionForward insert(ActionMapping mapping, ActionForm form,
65   * HttpServletRequest request, HttpServletResponse response) throws
66   * Exception</li>
67   *
68   * <li>public ActionForward update(ActionMapping mapping, ActionForm form,
69   * HttpServletRequest request, HttpServletResponse response) throws
70   * Exception</li>
71   *
72   * </ul>
73   *
74   * <p>and call one of the methods with a URL like this:</p>
75   *
76   * <p> <code> http://localhost:8080/myapp/saveSubscription.do?method=update
77   * </code></p>
78   *
79   * <p><strong>NOTE</strong> - All of the other mapping characteristics of this
80   * action must be shared by the various handlers.  This places some
81   * constraints over what types of handlers may reasonably be packaged into the
82   * same <code>DispatchAction</code> subclass.</p>
83   *
84   * <p><strong>NOTE</strong> - If the value of the request parameter is empty,
85   * a method named <code>unspecified</code> is called. The default action is to
86   * throw an exception. If the request was cancelled (a
87   * <code>html:cancel</code> button was pressed), the custom handler
88   * <code>cancelled</code> will be used instead. You can also override the
89   * <code>getMethodName</code> method to override the action's default handler
90   * selection.</p>
91   *
92   * @version $Rev$ $Date$
93   */
94  public abstract class DispatchAction extends BaseAction {
95      private static final long serialVersionUID = 4211198620691041849L;
96  
97      /**
98       * The {@code Log} instance for this class.
99       */
100     private transient final Logger log =
101         LoggerFactory.getLogger(DispatchAction.class);
102 
103     // ----------------------------------------------------- Instance Variables
104 
105     /**
106      * The Class instance of this <code>DispatchAction</code> class.
107      */
108     protected Class<? extends DispatchAction> clazz = this.getClass();
109 
110     /**
111      * The set of Method objects we have introspected for this class, keyed by
112      * method name.  This collection is populated as different methods are
113      * called, so that introspection needs to occur only once per method
114      * name.
115      */
116     protected HashMap<String, Method> methods = new HashMap<>();
117 
118     /**
119      * The set of argument type classes for the reflected method call.  These
120      * are the same for all calls, so calculate them only once.
121      */
122     protected Class<?>[] types =
123         {
124             ActionMapping.class, ActionForm.class, HttpServletRequest.class,
125             HttpServletResponse.class
126         };
127 
128     // --------------------------------------------------------- Public Methods
129 
130     /**
131      * Process the specified HTTP request, and create the corresponding HTTP
132      * response (or forward to another web component that will create it).
133      * Return an <code>ActionForward</code> instance describing where and how
134      * control should be forwarded, or <code>null</code> if the response has
135      * already been completed.
136      *
137      * @param mapping  The ActionMapping used to select this instance
138      * @param form     The optional ActionForm bean for this request (if any)
139      * @param request  The HTTP request we are processing
140      * @param response The HTTP response we are creating
141      * @return The forward to which control should be transferred, or
142      *         <code>null</code> if the response has been completed.
143      * @throws Exception if an exception occurs
144      */
145     public ActionForward execute(ActionMapping mapping, ActionForm form,
146         HttpServletRequest request, HttpServletResponse response)
147         throws Exception {
148         if (isCancelled(request)) {
149             ActionForward af = cancelled(mapping, form, request, response);
150 
151             if (af != null) {
152                 return af;
153             }
154         }
155 
156         // Get the parameter. This could be overridden in subclasses.
157         String parameter = getParameter(mapping, form, request, response);
158 
159         // Get the method's name. This could be overridden in subclasses.
160         String name =
161             getMethodName(mapping, form, request, response, parameter);
162 
163         // Prevent recursive calls
164         if ("execute".equals(name) || "perform".equals(name)) {
165             String message =
166                 messages.getMessage("dispatch.recursive", mapping.getPath());
167 
168             log.error(message);
169             throw new ServletException(message);
170         }
171 
172         // Invoke the named method, and return the result
173         return dispatchMethod(mapping, form, request, response, name);
174     }
175 
176     /**
177      * Method which is dispatched to when there is no value for specified
178      * request parameter included in the request.  Subclasses of
179      * <code>DispatchAction</code> should override this method if they wish to
180      * provide default behavior different than throwing a ServletException.
181      *
182      * @param mapping  The ActionMapping used to select this instance
183      * @param form     The optional ActionForm bean for this request (if any)
184      * @param request  The non-HTTP request we are processing
185      * @param response The non-HTTP response we are creating
186      * @return The forward to which control should be transferred, or
187      *         <code>null</code> if the response has been completed.
188      * @throws Exception if the application business logic throws an
189      *                   exception.
190      */
191     protected ActionForward unspecified(ActionMapping mapping, ActionForm form,
192         HttpServletRequest request, HttpServletResponse response)
193         throws Exception {
194         String message =
195             messages.getMessage("dispatch.parameter", mapping.getPath(),
196                 mapping.getParameter());
197 
198         log.error(message);
199 
200         throw new ServletException(message);
201     }
202 
203     /**
204      * Method which is dispatched to when the request is a cancel button
205      * submit. Subclasses of <code>DispatchAction</code> should override this
206      * method if they wish to provide default behavior different than
207      * returning null.
208      *
209      * @param mapping  The ActionMapping used to select this instance
210      * @param form     The optional ActionForm bean for this request (if any)
211      * @param request  The non-HTTP request we are processing
212      * @param response The non-HTTP response we are creating
213      * @return The forward to which control should be transferred, or
214      *         <code>null</code> if the response has been completed.
215      * @throws Exception if the application business logic throws an
216      *                   exception.
217      * @since Struts 1.2.0
218      */
219     protected ActionForward cancelled(ActionMapping mapping, ActionForm form,
220         HttpServletRequest request, HttpServletResponse response)
221         throws Exception {
222         return null;
223     }
224 
225     // ----------------------------------------------------- Protected Methods
226 
227     /**
228      * Dispatch to the specified method.
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      * @param name     The name of the method to invoke
235      * @return The forward to which control should be transferred, or
236      *         <code>null</code> if the response has been completed.
237      * @throws Exception if the application business logic throws an
238      *                   exception.
239      * @since Struts 1.1
240      */
241     protected ActionForward dispatchMethod(ActionMapping mapping,
242         ActionForm form, HttpServletRequest request,
243         HttpServletResponse response, String name)
244         throws Exception {
245         // Make sure we have a valid method name to call.
246         // This may be null if the user hacks the query string.
247         if (name == null) {
248             return this.unspecified(mapping, form, request, response);
249         }
250 
251         // Identify the method object to be dispatched to
252         Method method = null;
253 
254         try {
255             method = getMethod(name);
256         } catch (NoSuchMethodException e) {
257             log.atError()
258                 .setMessage(() -> messages.getMessage("dispatch.method", mapping.getPath(), name))
259                 .setCause(e).log();
260 
261             String userMsg =
262                 messages.getMessage("dispatch.method.user", mapping.getPath());
263             NoSuchMethodException e2 = new NoSuchMethodException(userMsg);
264             e2.initCause(e);
265             throw e2;
266         }
267 
268         ActionForward forward = null;
269 
270         try {
271             Object[] args = { mapping, form, request, response };
272 
273             forward = (ActionForward) method.invoke(this, args);
274         } catch (ClassCastException e) {
275             log.atError()
276                 .setMessage(() -> messages.getMessage("dispatch.return", mapping.getPath(), name))
277                 .setCause(e).log();
278             throw e;
279         } catch (IllegalAccessException e) {
280             log.atError()
281                 .setMessage(() -> messages.getMessage("dispatch.error", mapping.getPath(), name))
282                 .setCause(e).log();
283             throw e;
284         } catch (InvocationTargetException e) {
285             // Rethrow the target exception if possible so that the
286             // exception handling machinery can deal with it
287             Throwable t = e.getTargetException();
288 
289             if (t instanceof Exception) {
290                 throw ((Exception) t);
291             } else {
292                 log.atError()
293                     .setMessage(() -> messages.getMessage("dispatch.error", mapping.getPath(),
294                         name))
295                     .setCause(e).log();
296                 throw new ServletException(t);
297             }
298         }
299 
300         // Return the returned ActionForward instance
301         return (forward);
302     }
303 
304     /**
305      * <p>Returns the parameter value.</p>
306      *
307      * @param mapping  The ActionMapping used to select this instance
308      * @param form     The optional ActionForm bean for this request (if any)
309      * @param request  The HTTP request we are processing
310      * @param response The HTTP response we are creating
311      * @return The <code>ActionMapping</code> parameter's value
312      * @throws Exception if the parameter is missing.
313      */
314     protected String getParameter(ActionMapping mapping, ActionForm form,
315         HttpServletRequest request, HttpServletResponse response)
316         throws Exception {
317 
318         // Identify the request parameter containing the method name
319         String parameter = mapping.getParameter();
320 
321         if (parameter == null) {
322             String message =
323                 messages.getMessage("dispatch.handler", mapping.getPath());
324 
325             log.error(message);
326 
327             throw new ServletException(message);
328         }
329 
330 
331         return parameter;
332     }
333 
334     /**
335      * Introspect the current class to identify a method of the specified name
336      * that accepts the same parameter types as the <code>execute</code>
337      * method does.
338      *
339      * @param name Name of the method to be introspected
340      * @return The method with the specified name.
341      * @throws NoSuchMethodException if no such method can be found
342      */
343     protected Method getMethod(String name)
344         throws NoSuchMethodException {
345         synchronized (methods) {
346             Method method = methods.get(name);
347 
348             if (method == null) {
349                 method = clazz.getMethod(name, types);
350                 methods.put(name, method);
351             }
352 
353             return (method);
354         }
355     }
356 
357     /**
358      * Returns the method name, given a parameter's value.
359      *
360      * @param mapping   The ActionMapping used to select this instance
361      * @param form      The optional ActionForm bean for this request (if
362      *                  any)
363      * @param request   The HTTP request we are processing
364      * @param response  The HTTP response we are creating
365      * @param parameter The <code>ActionMapping</code> parameter's name
366      * @return The method's name.
367      * @throws Exception if an error occurs.
368      * @since Struts 1.2.0
369      */
370     protected String getMethodName(ActionMapping mapping, ActionForm form,
371         HttpServletRequest request, HttpServletResponse response,
372         String parameter) throws Exception {
373         // Identify the method name to be dispatched to.
374         // dispatchMethod() will call unspecified() if name is null
375         return request.getParameter(parameter);
376     }
377 }