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 }