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 }