View Javadoc

1   /*
2    * $Id: DefaultActionMapper.java 498085 2007-01-20 08:13:11Z mrdon $
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.struts2.dispatcher.mapper;
22  
23  import java.util.ArrayList;
24  import java.util.Arrays;
25  import java.util.HashSet;
26  import java.util.Iterator;
27  import java.util.List;
28  import java.util.Map;
29  import java.util.Set;
30  
31  import javax.servlet.http.HttpServletRequest;
32  
33  import org.apache.struts2.RequestUtils;
34  import org.apache.struts2.StrutsConstants;
35  import org.apache.struts2.dispatcher.ServletRedirectResult;
36  import org.apache.struts2.util.PrefixTrie;
37  
38  import com.opensymphony.xwork2.config.Configuration;
39  import com.opensymphony.xwork2.config.ConfigurationManager;
40  import com.opensymphony.xwork2.config.entities.PackageConfig;
41  import com.opensymphony.xwork2.inject.Inject;
42  import com.opensymphony.xwork2.inject.Container;
43  
44  /***
45   * <!-- START SNIPPET: javadoc -->
46   *
47   * Default action mapper implementation, using the standard *.[ext] (where ext
48   * usually "action") pattern. The extension is looked up from the Struts
49   * configuration key <b>struts.action.exection</b>.
50   *
51   * <p/> To help with dealing with buttons and other related requirements, this
52   * mapper (and other {@link ActionMapper}s, we hope) has the ability to name a
53   * button with some predefined prefix and have that button name alter the
54   * execution behaviour. The four prefixes are:
55   *
56   * <ul>
57   *
58   * <li>Method prefix - <i>method:default</i></li>
59   *
60   * <li>Action prefix - <i>action:dashboard</i></li>
61   *
62   * <li>Redirect prefix - <i>redirect:cancel.jsp</i></li>
63   *
64   * <li>Redirect-action prefix - <i>redirect-action:cancel</i></li>
65   *
66   * </ul>
67   *
68   * <p/> In addition to these four prefixes, this mapper also understands the
69   * action naming pattern of <i>foo!bar</i> in either the extension form (eg:
70   * foo!bar.action) or in the prefix form (eg: action:foo!bar). This syntax tells
71   * this mapper to map to the action named <i>foo</i> and the method <i>bar</i>.
72   *
73   * <!-- END SNIPPET: javadoc -->
74   *
75   * <p/> <b>Method Prefix</b> <p/>
76   *
77   * <!-- START SNIPPET: method -->
78   *
79   * With method-prefix, instead of calling baz action's execute() method (by
80   * default if it isn't overriden in struts.xml to be something else), the baz
81   * action's anotherMethod() will be called. A very elegant way determine which
82   * button is clicked. Alternatively, one would have submit button set a
83   * particular value on the action when clicked, and the execute() method decides
84   * on what to do with the setted value depending on which button is clicked.
85   *
86   * <!-- END SNIPPET: method -->
87   *
88   * <pre>
89   *  &lt;!-- START SNIPPET: method-example --&gt;
90   *  &lt;a:form action=&quot;baz&quot;&gt;
91   *      &lt;a:textfield label=&quot;Enter your name&quot; name=&quot;person.name&quot;/&gt;
92   *      &lt;a:submit value=&quot;Create person&quot;/&gt;
93   *      &lt;a:submit name=&quot;method:anotherMethod&quot; value=&quot;Cancel&quot;/&gt;
94   *  &lt;/a:form&gt;
95   *  &lt;!-- END SNIPPET: method-example --&gt;
96   * </pre>
97   *
98   * <p/> <b>Action prefix</b> <p/>
99   *
100  * <!-- START SNIPPET: action -->
101  *
102  * With action-prefix, instead of executing baz action's execute() method (by
103  * default if it isn't overriden in struts.xml to be something else), the
104  * anotherAction action's execute() method (assuming again if it isn't overriden
105  * with something else in struts.xml) will be executed.
106  *
107  * <!-- END SNIPPET: action -->
108  *
109  * <pre>
110  *  &lt;!-- START SNIPPET: action-example --&gt;
111  *  &lt;a:form action=&quot;baz&quot;&gt;
112  *      &lt;a:textfield label=&quot;Enter your name&quot; name=&quot;person.name&quot;/&gt;
113  *      &lt;a:submit value=&quot;Create person&quot;/&gt;
114  *      &lt;a:submit name=&quot;action:anotherAction&quot; value=&quot;Cancel&quot;/&gt;
115  *  &lt;/a:form&gt;
116  *  &lt;!-- END SNIPPET: action-example --&gt;
117  * </pre>
118  *
119  * <p/> <b>Redirect prefix</b> <p/>
120  *
121  * <!-- START SNIPPET: redirect -->
122  *
123  * With redirect-prefix, instead of executing baz action's execute() method (by
124  * default it isn't overriden in struts.xml to be something else), it will get
125  * redirected to, in this case to www.google.com. Internally it uses
126  * ServletRedirectResult to do the task.
127  *
128  * <!-- END SNIPPET: redirect -->
129  *
130  * <pre>
131  *  &lt;!-- START SNIPPET: redirect-example --&gt;
132  *  &lt;a:form action=&quot;baz&quot;&gt;
133  *      &lt;a:textfield label=&quot;Enter your name&quot; name=&quot;person.name&quot;/&gt;
134  *      &lt;a:submit value=&quot;Create person&quot;/&gt;
135  *      &lt;a:submit name=&quot;redirect:www.google.com&quot; value=&quot;Cancel&quot;/&gt;
136  *  &lt;/a:form&gt;
137  *  &lt;!-- END SNIPPET: redirect-example --&gt;
138  * </pre>
139  *
140  * <p/> <b>Redirect-action prefix</b> <p/>
141  *
142  * <!-- START SNIPPET: redirect-action -->
143  *
144  * With redirect-action-prefix, instead of executing baz action's execute()
145  * method (by default it isn't overriden in struts.xml to be something else), it
146  * will get redirected to, in this case 'dashboard.action'. Internally it uses
147  * ServletRedirectResult to do the task and read off the extension from the
148  * struts.properties.
149  *
150  * <!-- END SNIPPET: redirect-action -->
151  *
152  * <pre>
153  *  &lt;!-- START SNIPPET: redirect-action-example --&gt;
154  *  &lt;a:form action=&quot;baz&quot;&gt;
155  *      &lt;a:textfield label=&quot;Enter your name&quot; name=&quot;person.name&quot;/&gt;
156  *      &lt;a:submit value=&quot;Create person&quot;/&gt;
157  *      &lt;a:submit name=&quot;redirect-action:dashboard&quot; value=&quot;Cancel&quot;/&gt;
158  *  &lt;/a:form&gt;
159  *  &lt;!-- END SNIPPET: redirect-action-example --&gt;
160  * </pre>
161  *
162  */
163 public class DefaultActionMapper implements ActionMapper {
164 
165     static final String METHOD_PREFIX = "method:";
166 
167     static final String ACTION_PREFIX = "action:";
168 
169     static final String REDIRECT_PREFIX = "redirect:";
170 
171     static final String REDIRECT_ACTION_PREFIX = "redirect-action:";
172 
173     private boolean allowDynamicMethodCalls = true;
174 
175     private boolean allowSlashesInActionNames = false;
176     
177     private boolean alwaysSelectFullNamespace = false;
178 
179     private PrefixTrie prefixTrie = null;
180     
181     List extensions = new ArrayList() {{ add("action");}};
182 
183     private Container container;
184 
185     public DefaultActionMapper() {
186         prefixTrie = new PrefixTrie() {
187             {
188                 put(METHOD_PREFIX, new ParameterAction() {
189                     public void execute(String key, ActionMapping mapping) {
190                         mapping
191                                 .setMethod(key
192                                         .substring(METHOD_PREFIX.length()));
193                     }
194                 });
195 
196                 put(ACTION_PREFIX, new ParameterAction() {
197                     public void execute(String key, ActionMapping mapping) {
198                         String name = key.substring(ACTION_PREFIX.length());
199                         if (allowDynamicMethodCalls) {
200                             int bang = name.indexOf('!');
201                             if (bang != -1) {
202                                 String method = name.substring(bang + 1);
203                                 mapping.setMethod(method);
204                                 name = name.substring(0, bang);
205                             }
206                         }
207                         mapping.setName(name);
208                     }
209                 });
210 
211                 put(REDIRECT_PREFIX, new ParameterAction() {
212                     public void execute(String key, ActionMapping mapping) {
213                         ServletRedirectResult redirect = new ServletRedirectResult();
214                         container.inject(redirect);
215                         redirect.setLocation(key.substring(REDIRECT_PREFIX
216                                 .length()));
217                         mapping.setResult(redirect);
218                     }
219                 });
220 
221                 put(REDIRECT_ACTION_PREFIX, new ParameterAction() {
222                     public void execute(String key, ActionMapping mapping) {
223                         String location = key.substring(REDIRECT_ACTION_PREFIX
224                                 .length());
225                         ServletRedirectResult redirect = new ServletRedirectResult();
226                         container.inject(redirect);
227                         String extension = getDefaultExtension();
228                         if (extension != null) {
229                             location += "." + extension;
230                         }
231                         redirect.setLocation(location);
232                         mapping.setResult(redirect);
233                     }
234                 });
235             }
236         };
237     }
238     
239     @Inject(StrutsConstants.STRUTS_ENABLE_DYNAMIC_METHOD_INVOCATION)
240     public void setAllowDynamicMethodCalls(String allow) {
241         allowDynamicMethodCalls = "true".equals(allow);
242     }
243     
244     @Inject(StrutsConstants.STRUTS_ENABLE_SLASHES_IN_ACTION_NAMES)
245     public void setSlashesInActionNames(String allow) {
246         allowSlashesInActionNames = "true".equals(allow);
247     }
248     
249     @Inject(StrutsConstants.STRUTS_ALWAYS_SELECT_FULL_NAMESPACE)
250     public void setAlwaysSelectFullNamespace(String val) {
251         this.alwaysSelectFullNamespace = "true".equals(val);
252     }
253 
254     @Inject
255     public void setContainer(Container container) {
256         this.container = container;
257     }
258 
259     /*
260      * (non-Javadoc)
261      *
262      * @see org.apache.struts2.dispatcher.mapper.ActionMapper#getMapping(javax.servlet.http.HttpServletRequest)
263      */
264     public ActionMapping getMapping(HttpServletRequest request,
265             ConfigurationManager configManager) {
266         ActionMapping mapping = new ActionMapping();
267         String uri = getUri(request);
268 
269         uri = dropExtension(uri);
270         if (uri == null) {
271             return null;
272         }
273 
274         parseNameAndNamespace(uri, mapping, configManager);
275 
276         handleSpecialParameters(request, mapping);
277 
278         if (mapping.getName() == null) {
279             return null;
280         }
281 
282         if (allowDynamicMethodCalls) {
283             // handle "name!method" convention.
284             String name = mapping.getName();
285             int exclamation = name.lastIndexOf("!");
286             if (exclamation != -1) {
287                 mapping.setName(name.substring(0, exclamation));
288                 mapping.setMethod(name.substring(exclamation + 1));
289             }
290         }
291 
292         return mapping;
293     }
294 
295     /***
296      * Special parameters, as described in the class-level comment, are searched
297      * for and handled.
298      *
299      * @param request
300      *            The request
301      * @param mapping
302      *            The action mapping
303      */
304     public void handleSpecialParameters(HttpServletRequest request,
305             ActionMapping mapping) {
306         // handle special parameter prefixes.
307         Set<String> uniqueParameters = new HashSet<String>();
308         Map parameterMap = request.getParameterMap();
309         for (Iterator iterator = parameterMap.keySet().iterator(); iterator
310                 .hasNext();) {
311             String key = (String) iterator.next();
312             
313             // Strip off the image button location info, if found
314             if (key.endsWith(".x") || key.endsWith(".y")) {
315                 key = key.substring(0, key.length() - 2);
316             }
317             
318             // Ensure a parameter doesn't get processed twice
319             if (!uniqueParameters.contains(key)) {
320                 ParameterAction parameterAction = (ParameterAction) prefixTrie
321                         .get(key);
322                 if (parameterAction != null) {
323                     parameterAction.execute(key, mapping);
324                     uniqueParameters.add(key);
325                     break;
326                 }
327             }
328         }
329     }
330 
331     /***
332      * Parses the name and namespace from the uri
333      *
334      * @param uri
335      *            The uri
336      * @param mapping
337      *            The action mapping to populate
338      */
339     void parseNameAndNamespace(String uri, ActionMapping mapping,
340             ConfigurationManager configManager) {
341         String namespace, name;
342         int lastSlash = uri.lastIndexOf("/");
343         if (lastSlash == -1) {
344             namespace = "";
345             name = uri;
346         } else if (lastSlash == 0) {
347             // ww-1046, assume it is the root namespace, it will fallback to
348             // default
349             // namespace anyway if not found in root namespace.
350             namespace = "/";
351             name = uri.substring(lastSlash + 1);
352         } else if (alwaysSelectFullNamespace) {
353             // Simply select the namespace as everything before the last slash
354             namespace = uri.substring(0, lastSlash);
355             name = uri.substring(lastSlash + 1);
356         } else {
357             // Try to find the namespace in those defined, defaulting to ""
358             Configuration config = configManager.getConfiguration();
359             String prefix = uri.substring(0, lastSlash);
360             namespace = "";
361             // Find the longest matching namespace, defaulting to the default
362             for (Iterator i = config.getPackageConfigs().values().iterator(); i
363                     .hasNext();) {
364                 String ns = ((PackageConfig) i.next()).getNamespace();
365                 if (ns != null && prefix.startsWith(ns) && (prefix.length() == ns.length() || prefix.charAt(ns.length()) == '/')) {
366                     if (ns.length() > namespace.length()) {
367                         namespace = ns;
368                     }
369                 }
370             }
371 
372             name = uri.substring(namespace.length() + 1);
373         }
374 
375         if (!allowSlashesInActionNames && name != null) {
376             int pos = name.lastIndexOf('/');
377             if (pos > -1 && pos < name.length() - 1) {
378                 name = name.substring(pos + 1);
379             }
380         }
381 
382         mapping.setNamespace(namespace);
383         mapping.setName(name);
384     }
385 
386     /***
387      * Drops the extension from the action name
388      *
389      * @param name
390      *            The action name
391      * @return The action name without its extension
392      */
393     String dropExtension(String name) {
394         if (extensions == null) {
395             return name;
396         }
397         Iterator it = extensions.iterator();
398         while (it.hasNext()) {
399             String extension = "." + (String) it.next();
400             if (name.endsWith(extension)) {
401                 name = name.substring(0, name.length() - extension.length());
402                 return name;
403             }
404         }
405         return null;
406     }
407 
408     /***
409      * Returns null if no extension is specified.
410      */
411     String getDefaultExtension() {
412         if (extensions == null) {
413             return null;
414         } else {
415             return (String) extensions.get(0);
416         }
417     }
418 
419     @Inject(StrutsConstants.STRUTS_ACTION_EXTENSION)
420     public void setExtensions(String extensions) {
421         if (!"".equals(extensions)) {
422             this.extensions = Arrays.asList(extensions.split(","));
423         } else {
424             this.extensions = null;
425         }
426     }
427     
428     /***
429      * Gets the uri from the request
430      *
431      * @param request
432      *            The request
433      * @return The uri
434      */
435     String getUri(HttpServletRequest request) {
436         // handle http dispatcher includes.
437         String uri = (String) request
438                 .getAttribute("javax.servlet.include.servlet_path");
439         if (uri != null) {
440             return uri;
441         }
442 
443         uri = RequestUtils.getServletPath(request);
444         if (uri != null && !"".equals(uri)) {
445             return uri;
446         }
447 
448         uri = request.getRequestURI();
449         return uri.substring(request.getContextPath().length());
450     }
451 
452     /*
453      * (non-Javadoc)
454      *
455      * @see org.apache.struts2.dispatcher.mapper.ActionMapper#getUriFromActionMapping(org.apache.struts2.dispatcher.mapper.ActionMapping)
456      */
457     public String getUriFromActionMapping(ActionMapping mapping) {
458         StringBuffer uri = new StringBuffer();
459 
460         uri.append(mapping.getNamespace());
461         if (!"/".equals(mapping.getNamespace())) {
462             uri.append("/");
463         }
464         String name = mapping.getName();
465         String params = "";
466         if (name.indexOf('?') != -1) {
467             params = name.substring(name.indexOf('?'));
468             name = name.substring(0, name.indexOf('?'));
469         }
470         uri.append(name);
471 
472         if (null != mapping.getMethod() && !"".equals(mapping.getMethod())) {
473             uri.append("!").append(mapping.getMethod());
474         }
475 
476         String extension = getDefaultExtension();
477         if (extension != null) {
478             if (uri.indexOf('.' + extension) == -1) {
479                 uri.append(".").append(extension);
480                 if (params.length() > 0) {
481                     uri.append(params);
482                 }
483             }
484         }
485 
486         return uri.toString();
487     }
488 
489     /***
490      * Defines a parameter action prefix
491      */
492     interface ParameterAction {
493         void execute(String key, ActionMapping mapping);
494     }
495 }