1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 * <!-- START SNIPPET: method-example -->
87 * <s:form action="baz">
88 * <s:textfield label="Enter your name" name="person.name"/>
89 * <s:submit value="Create person"/>
90 * <s:submit name="method:anotherMethod" value="Cancel"/>
91 * </s:form>
92 * <!-- END SNIPPET: method-example -->
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 * <!-- START SNIPPET: action-example -->
108 * <s:form action="baz">
109 * <s:textfield label="Enter your name" name="person.name"/>
110 * <s:submit value="Create person"/>
111 * <s:submit name="action:anotherAction" value="Cancel"/>
112 * </s:form>
113 * <!-- END SNIPPET: action-example -->
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 * <!-- START SNIPPET: redirect-example -->
129 * <s:form action="baz">
130 * <s:textfield label="Enter your name" name="person.name"/>
131 * <s:submit value="Create person"/>
132 * <s:submit name="redirect:www.google.com" value="Cancel"/>
133 * </s:form>
134 * <!-- END SNIPPET: redirect-example -->
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 * <!-- START SNIPPET: redirect-action-example -->
151 * <s:form action="baz">
152 * <s:textfield label="Enter your name" name="person.name"/>
153 * <s:submit value="Create person"/>
154 * <s:submit name="redirect-action:dashboard" value="Cancel"/>
155 * </s:form>
156 * <!-- END SNIPPET: redirect-action-example -->
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
292
293
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
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
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
356 if (key.endsWith(".x") || key.endsWith(".y")) {
357 key = key.substring(0, key.length() - 2);
358 }
359
360
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
390
391
392 namespace = "/";
393 name = uri.substring(lastSlash + 1);
394 } else if (alwaysSelectFullNamespace) {
395
396 namespace = uri.substring(0, lastSlash);
397 name = uri.substring(lastSlash + 1);
398 } else {
399
400 Configuration config = configManager.getConfiguration();
401 String prefix = uri.substring(0, lastSlash);
402 namespace = "";
403 boolean rootAvailable = false;
404
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
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
463
464
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
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
518
519
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
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 }