001: /*
002: * $Id: DefaultActionMapper.java 540141 2007-05-21 13:46:48Z mrdon $
003: *
004: * Licensed to the Apache Software Foundation (ASF) under one
005: * or more contributor license agreements. See the NOTICE file
006: * distributed with this work for additional information
007: * regarding copyright ownership. The ASF licenses this file
008: * to you under the Apache License, Version 2.0 (the
009: * "License"); you may not use this file except in compliance
010: * with the License. You may obtain a copy of the License at
011: *
012: * http://www.apache.org/licenses/LICENSE-2.0
013: *
014: * Unless required by applicable law or agreed to in writing,
015: * software distributed under the License is distributed on an
016: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017: * KIND, either express or implied. See the License for the
018: * specific language governing permissions and limitations
019: * under the License.
020: */
021: package org.apache.struts2.dispatcher.mapper;
022:
023: import java.util.ArrayList;
024: import java.util.Arrays;
025: import java.util.HashSet;
026: import java.util.Iterator;
027: import java.util.List;
028: import java.util.Map;
029: import java.util.Set;
030:
031: import javax.servlet.http.HttpServletRequest;
032:
033: import org.apache.struts2.RequestUtils;
034: import org.apache.struts2.StrutsConstants;
035: import org.apache.struts2.dispatcher.ServletRedirectResult;
036: import org.apache.struts2.util.PrefixTrie;
037:
038: import com.opensymphony.xwork2.config.Configuration;
039: import com.opensymphony.xwork2.config.ConfigurationManager;
040: import com.opensymphony.xwork2.config.entities.PackageConfig;
041: import com.opensymphony.xwork2.inject.Inject;
042: import com.opensymphony.xwork2.inject.Container;
043:
044: /**
045: * <!-- START SNIPPET: javadoc -->
046: *
047: * Default action mapper implementation, using the standard *.[ext] (where ext
048: * usually "action") pattern. The extension is looked up from the Struts
049: * configuration key <b>struts.action.exection</b>.
050: *
051: * <p/> To help with dealing with buttons and other related requirements, this
052: * mapper (and other {@link ActionMapper}s, we hope) has the ability to name a
053: * button with some predefined prefix and have that button name alter the
054: * execution behaviour. The four prefixes are:
055: *
056: * <ul>
057: *
058: * <li>Method prefix - <i>method:default</i></li>
059: *
060: * <li>Action prefix - <i>action:dashboard</i></li>
061: *
062: * <li>Redirect prefix - <i>redirect:cancel.jsp</i></li>
063: *
064: * <li>Redirect-action prefix - <i>redirect-action:cancel</i></li>
065: *
066: * </ul>
067: *
068: * <p/> In addition to these four prefixes, this mapper also understands the
069: * action naming pattern of <i>foo!bar</i> in either the extension form (eg:
070: * foo!bar.action) or in the prefix form (eg: action:foo!bar). This syntax tells
071: * this mapper to map to the action named <i>foo</i> and the method <i>bar</i>.
072: *
073: * <!-- END SNIPPET: javadoc -->
074: *
075: * <p/> <b>Method Prefix</b> <p/>
076: *
077: * <!-- START SNIPPET: method -->
078: *
079: * With method-prefix, instead of calling baz action's execute() method (by
080: * default if it isn't overriden in struts.xml to be something else), the baz
081: * action's anotherMethod() will be called. A very elegant way determine which
082: * button is clicked. Alternatively, one would have submit button set a
083: * particular value on the action when clicked, and the execute() method decides
084: * on what to do with the setted value depending on which button is clicked.
085: *
086: * <!-- END SNIPPET: method -->
087: *
088: * <pre>
089: * <!-- START SNIPPET: method-example -->
090: * <a:form action="baz">
091: * <a:textfield label="Enter your name" name="person.name"/>
092: * <a:submit value="Create person"/>
093: * <a:submit name="method:anotherMethod" value="Cancel"/>
094: * </a:form>
095: * <!-- END SNIPPET: method-example -->
096: * </pre>
097: *
098: * <p/> <b>Action prefix</b> <p/>
099: *
100: * <!-- START SNIPPET: action -->
101: *
102: * With action-prefix, instead of executing baz action's execute() method (by
103: * default if it isn't overriden in struts.xml to be something else), the
104: * anotherAction action's execute() method (assuming again if it isn't overriden
105: * with something else in struts.xml) will be executed.
106: *
107: * <!-- END SNIPPET: action -->
108: *
109: * <pre>
110: * <!-- START SNIPPET: action-example -->
111: * <a:form action="baz">
112: * <a:textfield label="Enter your name" name="person.name"/>
113: * <a:submit value="Create person"/>
114: * <a:submit name="action:anotherAction" value="Cancel"/>
115: * </a:form>
116: * <!-- END SNIPPET: action-example -->
117: * </pre>
118: *
119: * <p/> <b>Redirect prefix</b> <p/>
120: *
121: * <!-- START SNIPPET: redirect -->
122: *
123: * With redirect-prefix, instead of executing baz action's execute() method (by
124: * default it isn't overriden in struts.xml to be something else), it will get
125: * redirected to, in this case to www.google.com. Internally it uses
126: * ServletRedirectResult to do the task.
127: *
128: * <!-- END SNIPPET: redirect -->
129: *
130: * <pre>
131: * <!-- START SNIPPET: redirect-example -->
132: * <a:form action="baz">
133: * <a:textfield label="Enter your name" name="person.name"/>
134: * <a:submit value="Create person"/>
135: * <a:submit name="redirect:www.google.com" value="Cancel"/>
136: * </a:form>
137: * <!-- END SNIPPET: redirect-example -->
138: * </pre>
139: *
140: * <p/> <b>Redirect-action prefix</b> <p/>
141: *
142: * <!-- START SNIPPET: redirect-action -->
143: *
144: * With redirect-action-prefix, instead of executing baz action's execute()
145: * method (by default it isn't overriden in struts.xml to be something else), it
146: * will get redirected to, in this case 'dashboard.action'. Internally it uses
147: * ServletRedirectResult to do the task and read off the extension from the
148: * struts.properties.
149: *
150: * <!-- END SNIPPET: redirect-action -->
151: *
152: * <pre>
153: * <!-- START SNIPPET: redirect-action-example -->
154: * <a:form action="baz">
155: * <a:textfield label="Enter your name" name="person.name"/>
156: * <a:submit value="Create person"/>
157: * <a:submit name="redirect-action:dashboard" value="Cancel"/>
158: * </a:form>
159: * <!-- END SNIPPET: redirect-action-example -->
160: * </pre>
161: *
162: */
163: public class DefaultActionMapper implements ActionMapper {
164:
165: static final String METHOD_PREFIX = "method:";
166:
167: static final String ACTION_PREFIX = "action:";
168:
169: static final String REDIRECT_PREFIX = "redirect:";
170:
171: static final String REDIRECT_ACTION_PREFIX = "redirect-action:";
172:
173: private boolean allowDynamicMethodCalls = true;
174:
175: private boolean allowSlashesInActionNames = false;
176:
177: private boolean alwaysSelectFullNamespace = false;
178:
179: private PrefixTrie prefixTrie = null;
180:
181: List extensions = new ArrayList() {
182: {
183: add("action");
184: }
185: };
186:
187: private Container container;
188:
189: public DefaultActionMapper() {
190: prefixTrie = new PrefixTrie() {
191: {
192: put(METHOD_PREFIX, new ParameterAction() {
193: public void execute(String key,
194: ActionMapping mapping) {
195: mapping.setMethod(key.substring(METHOD_PREFIX
196: .length()));
197: }
198: });
199:
200: put(ACTION_PREFIX, new ParameterAction() {
201: public void execute(String key,
202: ActionMapping mapping) {
203: String name = key.substring(ACTION_PREFIX
204: .length());
205: if (allowDynamicMethodCalls) {
206: int bang = name.indexOf('!');
207: if (bang != -1) {
208: String method = name
209: .substring(bang + 1);
210: mapping.setMethod(method);
211: name = name.substring(0, bang);
212: }
213: }
214: mapping.setName(name);
215: }
216: });
217:
218: put(REDIRECT_PREFIX, new ParameterAction() {
219: public void execute(String key,
220: ActionMapping mapping) {
221: ServletRedirectResult redirect = new ServletRedirectResult();
222: container.inject(redirect);
223: redirect.setLocation(key
224: .substring(REDIRECT_PREFIX.length()));
225: mapping.setResult(redirect);
226: }
227: });
228:
229: put(REDIRECT_ACTION_PREFIX, new ParameterAction() {
230: public void execute(String key,
231: ActionMapping mapping) {
232: String location = key
233: .substring(REDIRECT_ACTION_PREFIX
234: .length());
235: ServletRedirectResult redirect = new ServletRedirectResult();
236: container.inject(redirect);
237: String extension = getDefaultExtension();
238: if (extension != null) {
239: location += "." + extension;
240: }
241: redirect.setLocation(location);
242: mapping.setResult(redirect);
243: }
244: });
245: }
246: };
247: }
248:
249: @Inject(StrutsConstants.STRUTS_ENABLE_DYNAMIC_METHOD_INVOCATION)
250: public void setAllowDynamicMethodCalls(String allow) {
251: allowDynamicMethodCalls = "true".equals(allow);
252: }
253:
254: @Inject(StrutsConstants.STRUTS_ENABLE_SLASHES_IN_ACTION_NAMES)
255: public void setSlashesInActionNames(String allow) {
256: allowSlashesInActionNames = "true".equals(allow);
257: }
258:
259: @Inject(StrutsConstants.STRUTS_ALWAYS_SELECT_FULL_NAMESPACE)
260: public void setAlwaysSelectFullNamespace(String val) {
261: this .alwaysSelectFullNamespace = "true".equals(val);
262: }
263:
264: @Inject
265: public void setContainer(Container container) {
266: this .container = container;
267: }
268:
269: /*
270: * (non-Javadoc)
271: *
272: * @see org.apache.struts2.dispatcher.mapper.ActionMapper#getMapping(javax.servlet.http.HttpServletRequest)
273: */
274: public ActionMapping getMapping(HttpServletRequest request,
275: ConfigurationManager configManager) {
276: ActionMapping mapping = new ActionMapping();
277: String uri = getUri(request);
278:
279: uri = dropExtension(uri);
280: if (uri == null) {
281: return null;
282: }
283:
284: parseNameAndNamespace(uri, mapping, configManager);
285:
286: handleSpecialParameters(request, mapping);
287:
288: if (mapping.getName() == null) {
289: return null;
290: }
291:
292: if (allowDynamicMethodCalls) {
293: // handle "name!method" convention.
294: String name = mapping.getName();
295: int exclamation = name.lastIndexOf("!");
296: if (exclamation != -1) {
297: mapping.setName(name.substring(0, exclamation));
298: mapping.setMethod(name.substring(exclamation + 1));
299: }
300: }
301:
302: return mapping;
303: }
304:
305: /**
306: * Special parameters, as described in the class-level comment, are searched
307: * for and handled.
308: *
309: * @param request
310: * The request
311: * @param mapping
312: * The action mapping
313: */
314: public void handleSpecialParameters(HttpServletRequest request,
315: ActionMapping mapping) {
316: // handle special parameter prefixes.
317: Set<String> uniqueParameters = new HashSet<String>();
318: Map parameterMap = request.getParameterMap();
319: for (Iterator iterator = parameterMap.keySet().iterator(); iterator
320: .hasNext();) {
321: String key = (String) iterator.next();
322:
323: // Strip off the image button location info, if found
324: if (key.endsWith(".x") || key.endsWith(".y")) {
325: key = key.substring(0, key.length() - 2);
326: }
327:
328: // Ensure a parameter doesn't get processed twice
329: if (!uniqueParameters.contains(key)) {
330: ParameterAction parameterAction = (ParameterAction) prefixTrie
331: .get(key);
332: if (parameterAction != null) {
333: parameterAction.execute(key, mapping);
334: uniqueParameters.add(key);
335: break;
336: }
337: }
338: }
339: }
340:
341: /**
342: * Parses the name and namespace from the uri
343: *
344: * @param uri
345: * The uri
346: * @param mapping
347: * The action mapping to populate
348: */
349: void parseNameAndNamespace(String uri, ActionMapping mapping,
350: ConfigurationManager configManager) {
351: String namespace, name;
352: int lastSlash = uri.lastIndexOf("/");
353: if (lastSlash == -1) {
354: namespace = "";
355: name = uri;
356: } else if (lastSlash == 0) {
357: // ww-1046, assume it is the root namespace, it will fallback to
358: // default
359: // namespace anyway if not found in root namespace.
360: namespace = "/";
361: name = uri.substring(lastSlash + 1);
362: } else if (alwaysSelectFullNamespace) {
363: // Simply select the namespace as everything before the last slash
364: namespace = uri.substring(0, lastSlash);
365: name = uri.substring(lastSlash + 1);
366: } else {
367: // Try to find the namespace in those defined, defaulting to ""
368: Configuration config = configManager.getConfiguration();
369: String prefix = uri.substring(0, lastSlash);
370: namespace = "";
371: // Find the longest matching namespace, defaulting to the default
372: for (Iterator i = config.getPackageConfigs().values()
373: .iterator(); i.hasNext();) {
374: String ns = ((PackageConfig) i.next()).getNamespace();
375: if (ns != null
376: && prefix.startsWith(ns)
377: && (prefix.length() == ns.length() || prefix
378: .charAt(ns.length()) == '/')) {
379: if (ns.length() > namespace.length()) {
380: namespace = ns;
381: }
382: }
383: }
384:
385: name = uri.substring(namespace.length() + 1);
386: }
387:
388: if (!allowSlashesInActionNames && name != null) {
389: int pos = name.lastIndexOf('/');
390: if (pos > -1 && pos < name.length() - 1) {
391: name = name.substring(pos + 1);
392: }
393: }
394:
395: mapping.setNamespace(namespace);
396: mapping.setName(name);
397: }
398:
399: /**
400: * Drops the extension from the action name
401: *
402: * @param name
403: * The action name
404: * @return The action name without its extension
405: */
406: String dropExtension(String name) {
407: if (extensions == null) {
408: return name;
409: }
410: Iterator it = extensions.iterator();
411: while (it.hasNext()) {
412: String extension = "." + (String) it.next();
413: if (name.endsWith(extension)) {
414: name = name.substring(0, name.length()
415: - extension.length());
416: return name;
417: }
418: }
419: return null;
420: }
421:
422: /**
423: * Returns null if no extension is specified.
424: */
425: String getDefaultExtension() {
426: if (extensions == null) {
427: return null;
428: } else {
429: return (String) extensions.get(0);
430: }
431: }
432:
433: @Inject(StrutsConstants.STRUTS_ACTION_EXTENSION)
434: public void setExtensions(String extensions) {
435: if (!"".equals(extensions)) {
436: this .extensions = Arrays.asList(extensions.split(","));
437: } else {
438: this .extensions = null;
439: }
440: }
441:
442: /**
443: * Gets the uri from the request
444: *
445: * @param request
446: * The request
447: * @return The uri
448: */
449: String getUri(HttpServletRequest request) {
450: // handle http dispatcher includes.
451: String uri = (String) request
452: .getAttribute("javax.servlet.include.servlet_path");
453: if (uri != null) {
454: return uri;
455: }
456:
457: uri = RequestUtils.getServletPath(request);
458: if (uri != null && !"".equals(uri)) {
459: return uri;
460: }
461:
462: uri = request.getRequestURI();
463: return uri.substring(request.getContextPath().length());
464: }
465:
466: /*
467: * (non-Javadoc)
468: *
469: * @see org.apache.struts2.dispatcher.mapper.ActionMapper#getUriFromActionMapping(org.apache.struts2.dispatcher.mapper.ActionMapping)
470: */
471: public String getUriFromActionMapping(ActionMapping mapping) {
472: StringBuffer uri = new StringBuffer();
473:
474: uri.append(mapping.getNamespace());
475: if (!"/".equals(mapping.getNamespace())) {
476: uri.append("/");
477: }
478: String name = mapping.getName();
479: String params = "";
480: if (name.indexOf('?') != -1) {
481: params = name.substring(name.indexOf('?'));
482: name = name.substring(0, name.indexOf('?'));
483: }
484: uri.append(name);
485:
486: if (null != mapping.getMethod()
487: && !"".equals(mapping.getMethod())) {
488: uri.append("!").append(mapping.getMethod());
489: }
490:
491: String extension = getDefaultExtension();
492: if (extension != null) {
493: if (uri.indexOf('.' + extension) == -1) {
494: uri.append(".").append(extension);
495: if (params.length() > 0) {
496: uri.append(params);
497: }
498: }
499: }
500:
501: return uri.toString();
502: }
503:
504: public boolean isSlashesInActionNames() {
505: return allowSlashesInActionNames;
506: }
507:
508: /**
509: * Defines a parameter action prefix
510: */
511: interface ParameterAction {
512: void execute(String key, ActionMapping mapping);
513: }
514:
515: }
|