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 <action path="/saveSubscription"
49 * type="org.apache.struts.extras.actions.DispatchAction"
50 * name="subscriptionForm" scope="request" input="/subscription.jsp"
51 * parameter="method"/>}
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 }