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.dispatcher;
22
23 import java.io.Serializable;
24 import java.lang.reflect.InvocationTargetException;
25 import java.lang.reflect.Method;
26 import java.util.HashMap;
27
28 import org.apache.struts.action.Action;
29 import org.apache.struts.chain.contexts.ActionContext;
30 import org.apache.struts.config.ActionConfig;
31 import org.apache.struts.util.MessageResources;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
34
35 /**
36 * This abstract class is the stock template for {@link Dispatcher}
37 * implementations.
38 *
39 * @version $Rev$
40 * @since Struts 1.4
41 */
42 public abstract class AbstractDispatcher implements Dispatcher, Serializable {
43 private static final long serialVersionUID = 8527912438873600103L;
44
45 // Package message bundle keys
46 static final String LOCAL_STRINGS = "org.apache.struts.dispatcher.LocalStrings";
47 static final String MSG_KEY_DISPATCH_ERROR = "dispatcher.error";
48 static final String MSG_KEY_MISSING_METHOD = "dispatcher.missingMethod";
49 static final String MSG_KEY_MISSING_METHOD_LOG = "dispatcher.missingMethod.log";
50 static final String MSG_KEY_MISSING_MAPPING_PARAMETER = "dispatcher.missingMappingParameter";
51 static final String MSG_KEY_UNSPECIFIED = "dispatcher.unspecified";
52
53 /**
54 * The name of the <code>cancelled</code> method.
55 *
56 * @see ActionContext#getCancelled()
57 */
58 public static final String CANCELLED_METHOD_NAME = "cancelled";
59
60 /**
61 * The name of the <code>execute</code> method.
62 */
63 public static final String EXECUTE_METHOD_NAME = "execute";
64
65 /**
66 * The message resources for this package.
67 */
68 static MessageResources messages = MessageResources.getMessageResources(LOCAL_STRINGS);
69
70 /**
71 * The {@code Log} instance for this class.
72 */
73 private transient final Logger log =
74 LoggerFactory.getLogger(AbstractDispatcher.class);
75
76 /**
77 * The dictionary of {@link Method} objects we have introspected for this
78 * class, keyed by method name. This collection is populated as different
79 * methods are called, so that introspection needs to occur only once per
80 * method name.
81 */
82 private transient final HashMap<String, Method> methods;
83
84 private final MethodResolver methodResolver;
85
86 /**
87 * Constructs a new dispatcher with the specified method resolver.
88 *
89 * @param methodResolver the method resolver
90 */
91 public AbstractDispatcher(MethodResolver methodResolver) {
92 this.methodResolver = methodResolver;
93 methods = new HashMap<>();
94 }
95
96 /**
97 * Constructs the arguments that will be passed to the dispatched method.
98 * The construction is delegated to the method resolver instance.
99 *
100 * @param context the current action context
101 * @param method the target method of this dispatch
102 * @return the arguments array
103 * @throws IllegalStateException if the method does not have a supported
104 * signature
105 * @see MethodResolver#buildArguments(ActionContext, Method)
106 */
107 Object[] buildMethodArguments(ActionContext context, Method method) {
108 Object[] args = methodResolver.buildArguments(context, method);
109 if (args == null) {
110 throw new IllegalStateException("Unsupported method signature: " + method.toString());
111 }
112 return args;
113 }
114
115 public Object dispatch(ActionContext context) throws Exception {
116 // Resolve the method name; fallback to default if necessary
117 String methodName = resolveMethodName(context);
118 if ((methodName == null) || "".equals(methodName)) {
119 methodName = getDefaultMethodName();
120 }
121
122 // Ensure there is a specified method name to invoke.
123 // This may be null if the user hacks the query string.
124 if (methodName == null) {
125 return unspecified(context);
126 }
127
128 // Identify the method object to dispatch
129 Method method;
130 try {
131 method = getMethod(context, methodName);
132 } catch (NoSuchMethodException e) {
133 // The log message reveals the offending method name...
134 String path = context.getActionConfig().getPath();
135 if (log.isErrorEnabled()) {
136 String message = messages.getMessage(MSG_KEY_MISSING_METHOD_LOG, path, methodName);
137 log.error(message, e);
138 }
139
140 // ...but the exception thrown does not
141 // See r383718 (XSS vulnerability)
142 String userMsg = messages.getMessage(MSG_KEY_MISSING_METHOD, path);
143 NoSuchMethodException e2 = new NoSuchMethodException(userMsg);
144 e2.initCause(e);
145 throw e2;
146 }
147
148 // Invoke the named method and return its result
149 return dispatchMethod(context, method, methodName);
150 }
151
152 /**
153 * Dispatches to the specified method.
154 *
155 * @param context the current action context
156 * @param method The method to invoke
157 * @param name The name of the method to invoke
158 * @return the return value of the method
159 * @throws Exception if the dispatch fails with an exception
160 * @see #buildMethodArguments(ActionContext, Method)
161 */
162 protected final Object dispatchMethod(ActionContext context, Method method, String name) throws Exception {
163 Action target = context.getAction();
164 String path = context.getActionConfig().getPath();
165 Object[] args = buildMethodArguments(context, method);
166 return invoke(target, method, args, path);
167 }
168
169 /**
170 * Empties the method cache.
171 *
172 * @see #getMethod(ActionContext, String)
173 */
174 final void flushMethodCache() {
175 synchronized (methods) {
176 methods.clear();
177 }
178 }
179
180 /**
181 * Retrieves the name of the method to fallback upon if no method name can
182 * be resolved. The default implementation returns
183 * {@link #EXECUTE_METHOD_NAME}.
184 *
185 * @return the fallback method name; can be <code>null</code>
186 * @see #resolveMethodName(ActionContext)
187 * @see #EXECUTE_METHOD_NAME
188 */
189 protected String getDefaultMethodName() {
190 return EXECUTE_METHOD_NAME;
191 }
192
193 /**
194 * Introspects the action to identify a method of the specified name that
195 * will be the target of the dispatch. This implementation caches the method
196 * instance for subsequent invocations.
197 *
198 * @param context the current action context
199 * @param methodName the name of the method to be introspected
200 * @return the method of the specified name
201 * @throws NoSuchMethodException if no such method can be found
202 * @see #resolveMethod(ActionContext, String)
203 * @see #flushMethodCache()
204 */
205 protected final Method getMethod(ActionContext context, String methodName) throws NoSuchMethodException {
206 synchronized (methods) {
207 // Key the method based on the class-method combination
208 StringBuilder keyBuf = new StringBuilder(100);
209 keyBuf.append(context.getAction().getClass().getName());
210 keyBuf.append(":");
211 keyBuf.append(methodName);
212 String key = keyBuf.toString();
213
214 Method method = methods.get(key);
215
216 if (method == null) {
217 method = resolveMethod(context, methodName);
218 methods.put(key, method);
219 }
220
221 return method;
222 }
223 }
224
225 /**
226 * Convenience method to help dispatch the specified method. The method is
227 * invoked via reflection.
228 *
229 * @param target the target object
230 * @param method the method of the target object
231 * @param args the arguments for the method
232 * @param path the mapping path
233 * @return the return value of the method
234 * @throws Exception if the dispatch fails with an exception
235 */
236 protected final Object invoke(Object target, Method method, Object[] args, String path) throws Exception {
237 try {
238 Object retval = method.invoke(target, args);
239 if (method.getReturnType() == void.class) {
240 retval = void.class;
241 }
242 return retval;
243 } catch (IllegalAccessException e) {
244 log.atError()
245 .setMessage("{}:{}")
246 .addArgument(() -> messages.getMessage(MSG_KEY_DISPATCH_ERROR, path))
247 .addArgument(method.getName())
248 .setCause(e).log();
249 throw e;
250 } catch (InvocationTargetException e) {
251 // Rethrow the target exception if possible so that the
252 // exception handling machinery can deal with it
253 Throwable t = e.getTargetException();
254 if (t instanceof Exception) {
255 throw (Exception) t;
256 } else {
257 log.atError()
258 .setMessage("{}:{}")
259 .addArgument(() -> messages.getMessage(MSG_KEY_DISPATCH_ERROR, path))
260 .addArgument(method.getName())
261 .setCause(e).log();
262 throw new Exception(t);
263 }
264 }
265 }
266
267 /**
268 * Determines whether the current form's cancel button was pressed. The
269 * default behavior method will check if the
270 * {@link ActionContext#getCancelled()} context property is set , which
271 * normally occurs if the cancel button generated by <strong>CancelTag</strong>
272 * was pressed by the user in the current request.
273 *
274 * @param context the current action context
275 * @return <code>true</code> if the request is cancelled; otherwise
276 * <code>false</code>
277 */
278 protected boolean isCancelled(ActionContext context) {
279 Boolean cancelled = context.getCancelled();
280 return (cancelled != null) && cancelled.booleanValue();
281 }
282
283 /**
284 * Decides the appropriate method instance for the specified method name.
285 * Implementations may introspect for any desired method signature. This
286 * resolution is only invoked if {@link #getMethod(ActionContext, String)}
287 * does not find a match in its method cache.
288 *
289 * @param context the current action context
290 * @param methodName the method name to use for introspection
291 * @return the method to invoke
292 * @throws NoSuchMethodException if an appropriate method cannot be found
293 * @see #getMethod(ActionContext, String)
294 * @see #invoke(Object, Method, Object[], String)
295 */
296 Method resolveMethod(ActionContext context, String methodName) throws NoSuchMethodException {
297 return methodResolver.resolveMethod(context, methodName);
298 }
299
300 /**
301 * Decides the method name that can handle the request.
302 *
303 * @param context the current action context
304 * @return the method name or <code>null</code> if no name can be resolved
305 * @see #getDefaultMethodName()
306 * @see #resolveMethod(ActionContext, String)
307 */
308 abstract String resolveMethodName(ActionContext context);
309
310 /**
311 * Services the case when the dispatch fails because the method name cannot
312 * be resolved. The default behavior throws an {@link IllegalStateException}.
313 * Subclasses should override this to provide custom handling such as
314 * sending an HTTP 404 error or dispatching elsewhere.
315 *
316 * @param context the current action context
317 * @return the return value of the dispatch
318 * @throws Exception if an error occurs
319 * @see #resolveMethodName(ActionContext)
320 */
321 protected Object unspecified(ActionContext context) throws Exception {
322 ActionConfig config = context.getActionConfig();
323 String msg = messages.getMessage(MSG_KEY_UNSPECIFIED, config.getPath());
324 log.error(msg);
325 throw new IllegalStateException(msg);
326 }
327 }