View Javadoc

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