View Javadoc

1   /*
2    * $Id: DefaultActionMapper.java 720222 2008-11-24 16:41:07Z musachy $
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>redirect-action: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;redirect-action: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 = "redirect-action:";
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                         mapping
188                                 .setMethod(key
189                                         .substring(METHOD_PREFIX.length()));
190                     }
191                 });
192 
193                 put(ACTION_PREFIX, new ParameterAction() {
194                     public void execute(String key, ActionMapping mapping) {
195                         String name = key.substring(ACTION_PREFIX.length());
196                         if (allowDynamicMethodCalls) {
197                             int bang = name.indexOf('!');
198                             if (bang != -1) {
199                                 String method = name.substring(bang + 1);
200                                 mapping.setMethod(method);
201                                 name = name.substring(0, bang);
202                             }
203                         }
204                         mapping.setName(name);
205                     }
206                 });
207 
208                 put(REDIRECT_PREFIX, new ParameterAction() {
209                     public void execute(String key, ActionMapping mapping) {
210                         ServletRedirectResult redirect = new ServletRedirectResult();
211                         container.inject(redirect);
212                         redirect.setLocation(key.substring(REDIRECT_PREFIX
213                                 .length()));
214                         mapping.setResult(redirect);
215                     }
216                 });
217 
218                 put(REDIRECT_ACTION_PREFIX, new ParameterAction() {
219                     public void execute(String key, ActionMapping mapping) {
220                         String location = key.substring(REDIRECT_ACTION_PREFIX
221                                 .length());
222                         ServletRedirectResult redirect = new ServletRedirectResult();
223                         container.inject(redirect);
224                         String extension = getDefaultExtension();
225                         if (extension != null && extension.length() > 0) {
226                             location += "." + extension;
227                         }
228                         redirect.setLocation(location);
229                         mapping.setResult(redirect);
230                     }
231                 });
232             }
233         };
234     }
235 
236     /***
237      * Adds a parameter action.  Should only be called during initialization
238      *
239      * @param prefix The string prefix to trigger the action
240      * @param parameterAction The parameter action to execute
241      * @since 2.1.0
242     */
243     protected void addParameterAction(String prefix, ParameterAction parameterAction) {
244         prefixTrie.put(prefix, parameterAction);
245     }
246 
247     @Inject(StrutsConstants.STRUTS_ENABLE_DYNAMIC_METHOD_INVOCATION)
248     public void setAllowDynamicMethodCalls(String allow) {
249         allowDynamicMethodCalls = "true".equals(allow);
250     }
251 
252     @Inject(StrutsConstants.STRUTS_ENABLE_SLASHES_IN_ACTION_NAMES)
253     public void setSlashesInActionNames(String allow) {
254         allowSlashesInActionNames = "true".equals(allow);
255     }
256 
257     @Inject(StrutsConstants.STRUTS_ALWAYS_SELECT_FULL_NAMESPACE)
258     public void setAlwaysSelectFullNamespace(String val) {
259         this.alwaysSelectFullNamespace = "true".equals(val);
260     }
261 
262     @Inject
263     public void setContainer(Container container) {
264         this.container = container;
265     }
266 
267     @Inject(StrutsConstants.STRUTS_ACTION_EXTENSION)
268     public void setExtensions(String extensions) {
269         if (extensions != null && !"".equals(extensions)) {
270             List<String> list = new ArrayList<String>();
271             String[] tokens = extensions.split(",");
272             for (String token : tokens) {
273                 list.add(token);
274             }
275             if (extensions.endsWith(",")) {
276                 list.add("");
277             }
278             this.extensions = Collections.unmodifiableList(list);
279         } else {
280             this.extensions = null;
281         }
282     }
283 
284     public ActionMapping getMappingFromActionName(String actionName) {
285         ActionMapping mapping = new ActionMapping();
286         mapping.setName(actionName);
287         return parseActionName(mapping);
288     }
289 
290     /*
291      * (non-Javadoc)
292      *
293      * @see org.apache.struts2.dispatcher.mapper.ActionMapper#getMapping(javax.servlet.http.HttpServletRequest)
294      */
295     public ActionMapping getMapping(HttpServletRequest request,
296             ConfigurationManager configManager) {
297         ActionMapping mapping = new ActionMapping();
298         String uri = getUri(request);
299 
300         int indexOfSemicolon = uri.indexOf(";");
301         uri = (indexOfSemicolon > -1) ? uri.substring(0, indexOfSemicolon) : uri;
302 
303         uri = dropExtension(uri, mapping);
304         if (uri == null) {
305             return null;
306         }
307 
308         parseNameAndNamespace(uri, mapping, configManager);
309 
310         handleSpecialParameters(request, mapping);
311 
312         if (mapping.getName() == null) {
313             return null;
314         }
315 
316         parseActionName(mapping);
317 
318         return mapping;
319     }
320 
321     protected ActionMapping parseActionName(ActionMapping mapping) {
322         if (mapping.getName() == null) {
323             return mapping;
324         }
325         if (allowDynamicMethodCalls) {
326             // handle "name!method" convention.
327             String name = mapping.getName();
328             int exclamation = name.lastIndexOf("!");
329             if (exclamation != -1) {
330                 mapping.setName(name.substring(0, exclamation));
331                 mapping.setMethod(name.substring(exclamation + 1));
332             }
333         }
334         return mapping;
335     }
336 
337     /***
338      * Special parameters, as described in the class-level comment, are searched
339      * for and handled.
340      *
341      * @param request
342      *            The request
343      * @param mapping
344      *            The action mapping
345      */
346     public void handleSpecialParameters(HttpServletRequest request,
347             ActionMapping mapping) {
348         // handle special parameter prefixes.
349         Set<String> uniqueParameters = new HashSet<String>();
350         Map parameterMap = request.getParameterMap();
351         for (Iterator iterator = parameterMap.keySet().iterator(); iterator
352                 .hasNext();) {
353             String key = (String) iterator.next();
354 
355             // Strip off the image button location info, if found
356             if (key.endsWith(".x") || key.endsWith(".y")) {
357                 key = key.substring(0, key.length() - 2);
358             }
359 
360             // Ensure a parameter doesn't get processed twice
361             if (!uniqueParameters.contains(key)) {
362                 ParameterAction parameterAction = (ParameterAction) prefixTrie
363                         .get(key);
364                 if (parameterAction != null) {
365                     parameterAction.execute(key, mapping);
366                     uniqueParameters.add(key);
367                     break;
368                 }
369             }
370         }
371     }
372 
373     /***
374      * Parses the name and namespace from the uri
375      *
376      * @param uri
377      *            The uri
378      * @param mapping
379      *            The action mapping to populate
380      */
381     protected void parseNameAndNamespace(String uri, ActionMapping mapping,
382             ConfigurationManager configManager) {
383         String namespace, name;
384         int lastSlash = uri.lastIndexOf("/");
385         if (lastSlash == -1) {
386             namespace = "";
387             name = uri;
388         } else if (lastSlash == 0) {
389             // ww-1046, assume it is the root namespace, it will fallback to
390             // default
391             // namespace anyway if not found in root namespace.
392             namespace = "/";
393             name = uri.substring(lastSlash + 1);
394         } else if (alwaysSelectFullNamespace) {
395             // Simply select the namespace as everything before the last slash
396             namespace = uri.substring(0, lastSlash);
397             name = uri.substring(lastSlash + 1);
398         } else {
399             // Try to find the namespace in those defined, defaulting to ""
400             Configuration config = configManager.getConfiguration();
401             String prefix = uri.substring(0, lastSlash);
402             namespace = "";
403             boolean rootAvailable = false;
404             // Find the longest matching namespace, defaulting to the default
405             for (Object cfg : config.getPackageConfigs().values()) {
406                 String ns = ((PackageConfig) cfg).getNamespace();
407                 if (ns != null && prefix.startsWith(ns) && (prefix.length() == ns.length() || prefix.charAt(ns.length()) == '/')) {
408                     if (ns.length() > namespace.length()) {
409                         namespace = ns;
410                     }
411                 }
412                 if ("/".equals(ns)) {
413                     rootAvailable = true;
414                 }
415             }
416 
417             name = uri.substring(namespace.length() + 1);
418 
419             // Still none found, use root namespace if found
420             if (rootAvailable && "".equals(namespace)) {
421                 namespace = "/";
422             }
423         }
424 
425         if (!allowSlashesInActionNames && name != null) {
426             int pos = name.lastIndexOf('/');
427             if (pos > -1 && pos < name.length() - 1) {
428                 name = name.substring(pos + 1);
429             }
430         }
431 
432         mapping.setNamespace(namespace);
433         mapping.setName(name);
434     }
435 
436     /***
437      * Drops the extension from the action name
438      *
439      * @param name
440      *            The action name
441      * @return The action name without its extension
442      * @deprecated Since 2.1, use {@link #dropExtension(java.lang.String,org.apache.struts2.dispatcher.mapper.ActionMapping)} instead
443      */
444     protected String dropExtension(String name) {
445         return dropExtension(name, new ActionMapping());
446     }
447 
448     /***
449      * Drops the extension from the action name, storing it in the mapping for later use
450      *
451      * @param name
452      *            The action name
453      * @param mapping The action mapping to store the extension in
454      * @return The action name without its extension
455      */
456     protected String dropExtension(String name, ActionMapping mapping) {
457         if (extensions == null) {
458             return name;
459         }
460         for (String ext : extensions) {
461             if ("".equals(ext)) {
462                 // This should also handle cases such as /foo/bar-1.0/description. It is tricky to
463                 // distinquish /foo/bar-1.0 but perhaps adding a numeric check in the future could
464                 // work
465                 int index = name.lastIndexOf('.');
466                 if (index == -1 || name.indexOf('/', index) >= 0) {
467                     return name;
468                 }
469             } else {
470                 String extension = "." + ext;
471                 if (name.endsWith(extension)) {
472                     name = name.substring(0, name.length() - extension.length());
473                     mapping.setExtension(ext);
474                     return name;
475                 }
476             }
477         }
478         return null;
479     }
480 
481     /***
482      * Returns null if no extension is specified.
483      */
484     protected String getDefaultExtension() {
485         if (extensions == null) {
486             return null;
487         } else {
488             return (String) extensions.get(0);
489         }
490     }
491 
492     /***
493      * Gets the uri from the request
494      *
495      * @param request
496      *            The request
497      * @return The uri
498      */
499     protected String getUri(HttpServletRequest request) {
500         // handle http dispatcher includes.
501         String uri = (String) request
502                 .getAttribute("javax.servlet.include.servlet_path");
503         if (uri != null) {
504             return uri;
505         }
506 
507         uri = RequestUtils.getServletPath(request);
508         if (uri != null && !"".equals(uri)) {
509             return uri;
510         }
511 
512         uri = request.getRequestURI();
513         return uri.substring(request.getContextPath().length());
514     }
515 
516     /*
517      * (non-Javadoc)
518      *
519      * @see org.apache.struts2.dispatcher.mapper.ActionMapper#getUriFromActionMapping(org.apache.struts2.dispatcher.mapper.ActionMapping)
520      */
521     public String getUriFromActionMapping(ActionMapping mapping) {
522         StringBuilder uri = new StringBuilder();
523 
524         if (mapping.getNamespace() != null) {
525             uri.append(mapping.getNamespace());
526             if (!"/".equals(mapping.getNamespace())) {
527                 uri.append("/");
528             }
529         }
530         String name = mapping.getName();
531         String params = "";
532         if (name.indexOf('?') != -1) {
533             params = name.substring(name.indexOf('?'));
534             name = name.substring(0, name.indexOf('?'));
535         }
536         uri.append(name);
537 
538         if (null != mapping.getMethod() && !"".equals(mapping.getMethod())) {
539             uri.append("!").append(mapping.getMethod());
540         }
541 
542         String extension = mapping.getExtension();
543         if (extension == null) {
544             extension = getDefaultExtension();
545             // Look for the current extension, if available
546             ActionContext context = ActionContext.getContext();
547             if (context != null) {
548                 ActionMapping orig = (ActionMapping) context.get(ServletActionContext.ACTION_MAPPING);
549                 if (orig != null) {
550                     extension = orig.getExtension();
551                 }
552             }
553         }
554 
555         if (extension != null) {
556 
557             if (extension.length() == 0 || (extension.length() > 0 && uri.indexOf('.' + extension) == -1)) {
558                 if (extension.length() > 0) {
559                     uri.append(".").append(extension);
560                 }
561                 if (params.length() > 0) {
562                     uri.append(params);
563                 }
564             }
565         }
566 
567         return uri.toString();
568     }
569 
570 
571 	public boolean isSlashesInActionNames() {
572 		return allowSlashesInActionNames;
573 	}
574 
575 }