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
404 for (Iterator i = config.getPackageConfigs().values().iterator(); i
405 .hasNext();) {
406 String ns = ((PackageConfig) i.next()).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 }
413
414 name = uri.substring(namespace.length() + 1);
415 }
416
417 if (!allowSlashesInActionNames && name != null) {
418 int pos = name.lastIndexOf('/');
419 if (pos > -1 && pos < name.length() - 1) {
420 name = name.substring(pos + 1);
421 }
422 }
423
424 mapping.setNamespace(namespace);
425 mapping.setName(name);
426 }
427
428 /***
429 * Drops the extension from the action name
430 *
431 * @param name
432 * The action name
433 * @return The action name without its extension
434 * @deprecated Since 2.1, use {@link #dropExtension(java.lang.String,org.apache.struts2.dispatcher.mapper.ActionMapping)} instead
435 */
436 protected String dropExtension(String name) {
437 return dropExtension(name, new ActionMapping());
438 }
439
440 /***
441 * Drops the extension from the action name, storing it in the mapping for later use
442 *
443 * @param name
444 * The action name
445 * @param mapping The action mapping to store the extension in
446 * @return The action name without its extension
447 */
448 protected String dropExtension(String name, ActionMapping mapping) {
449 if (extensions == null) {
450 return name;
451 }
452 for (String ext : extensions) {
453 if ("".equals(ext)) {
454
455
456
457 int index = name.lastIndexOf('.');
458 if (index == -1 || name.indexOf('/', index) >= 0) {
459 return name;
460 }
461 } else {
462 String extension = "." + ext;
463 if (name.endsWith(extension)) {
464 name = name.substring(0, name.length() - extension.length());
465 mapping.setExtension(ext);
466 return name;
467 }
468 }
469 }
470 return null;
471 }
472
473 /***
474 * Returns null if no extension is specified.
475 */
476 protected String getDefaultExtension() {
477 if (extensions == null) {
478 return null;
479 } else {
480 return (String) extensions.get(0);
481 }
482 }
483
484 /***
485 * Gets the uri from the request
486 *
487 * @param request
488 * The request
489 * @return The uri
490 */
491 protected String getUri(HttpServletRequest request) {
492
493 String uri = (String) request
494 .getAttribute("javax.servlet.include.servlet_path");
495 if (uri != null) {
496 return uri;
497 }
498
499 uri = RequestUtils.getServletPath(request);
500 if (uri != null && !"".equals(uri)) {
501 return uri;
502 }
503
504 uri = request.getRequestURI();
505 return uri.substring(request.getContextPath().length());
506 }
507
508
509
510
511
512
513 public String getUriFromActionMapping(ActionMapping mapping) {
514 StringBuffer uri = new StringBuffer();
515
516 if (mapping.getNamespace() != null) {
517 uri.append(mapping.getNamespace());
518 if (!"/".equals(mapping.getNamespace())) {
519 uri.append("/");
520 }
521 }
522 String name = mapping.getName();
523 String params = "";
524 if (name.indexOf('?') != -1) {
525 params = name.substring(name.indexOf('?'));
526 name = name.substring(0, name.indexOf('?'));
527 }
528 uri.append(name);
529
530 if (null != mapping.getMethod() && !"".equals(mapping.getMethod())) {
531 uri.append("!").append(mapping.getMethod());
532 }
533
534 String extension = mapping.getExtension();
535 if (extension == null) {
536 extension = getDefaultExtension();
537
538 ActionContext context = ActionContext.getContext();
539 if (context != null) {
540 ActionMapping orig = (ActionMapping) context.get(ServletActionContext.ACTION_MAPPING);
541 if (orig != null) {
542 extension = orig.getExtension();
543 }
544 }
545 }
546
547 if (extension != null) {
548
549 if (extension.length() == 0 || (extension.length() > 0 && uri.indexOf('.' + extension) == -1)) {
550 if (extension.length() > 0) {
551 uri.append(".").append(extension);
552 }
553 if (params.length() > 0) {
554 uri.append(params);
555 }
556 }
557 }
558
559 return uri.toString();
560 }
561
562
563 public boolean isSlashesInActionNames() {
564 return allowSlashesInActionNames;
565 }
566
567 }