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>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 * <!-- 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="redirectAction: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 = "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
293
294
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
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
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
357 if (key.endsWith(".x") || key.endsWith(".y")) {
358 key = key.substring(0, key.length() - 2);
359 }
360
361
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
391
392
393 namespace = "/";
394 name = uri.substring(lastSlash + 1);
395 } else if (alwaysSelectFullNamespace) {
396
397 namespace = uri.substring(0, lastSlash);
398 name = uri.substring(lastSlash + 1);
399 } else {
400
401 Configuration config = configManager.getConfiguration();
402 String prefix = uri.substring(0, lastSlash);
403 namespace = "";
404 boolean rootAvailable = false;
405
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
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
464
465
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
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
519
520
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
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 }