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 }