View Javadoc
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 }