/* * $Id$ * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.struts2.dispatcher.mapper; import com.opensymphony.xwork2.ActionContext; import com.opensymphony.xwork2.config.Configuration; import com.opensymphony.xwork2.config.ConfigurationManager; import com.opensymphony.xwork2.config.entities.PackageConfig; import com.opensymphony.xwork2.inject.Container; import com.opensymphony.xwork2.inject.Inject; import org.apache.struts2.RequestUtils; import org.apache.struts2.ServletActionContext; import org.apache.struts2.StrutsConstants; import org.apache.struts2.dispatcher.ServletRedirectResult; import org.apache.struts2.util.PrefixTrie; import javax.servlet.http.HttpServletRequest; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; /** * <!-- START SNIPPET: javadoc --> * <p/> * Default action mapper implementation, using the standard *.[ext] (where ext * usually "action") pattern. The extension is looked up from the Struts * configuration key <b>struts.action.extension</b>. * <p/> * <p/> To help with dealing with buttons and other related requirements, this * mapper (and other {@link ActionMapper}s, we hope) has the ability to name a * button with some predefined prefix and have that button name alter the * execution behaviour. The four prefixes are: * <p/> * <ul> * <p/> * <li>Method prefix - <i>method:default</i></li> * <p/> * <li>Action prefix - <i>action:dashboard</i></li> * <p/> * <li>Redirect prefix - <i>redirect:cancel.jsp</i></li> * <p/> * <li>Redirect-action prefix - <i>redirectAction:cancel</i></li> * <p/> * </ul> * <p/> * <p/> In addition to these four prefixes, this mapper also understands the * action naming pattern of <i>foo!bar</i> in either the extension form (eg: * foo!bar.action) or in the prefix form (eg: action:foo!bar). This syntax tells * this mapper to map to the action named <i>foo</i> and the method <i>bar</i>. * <p/> * <!-- END SNIPPET: javadoc --> * <p/> * <p/> <b>Method Prefix</b> <p/> * <p/> * <!-- START SNIPPET: method --> * <p/> * With method-prefix, instead of calling baz action's execute() method (by * default if it isn't overriden in struts.xml to be something else), the baz * action's anotherMethod() will be called. A very elegant way determine which * button is clicked. Alternatively, one would have submit button set a * particular value on the action when clicked, and the execute() method decides * on what to do with the setted value depending on which button is clicked. * <p/> * <!-- END SNIPPET: method --> * <p/> * <pre> * <!-- START SNIPPET: method-example --> * <s:form action="baz"> * <s:textfield label="Enter your name" name="person.name"/> * <s:submit value="Create person"/> * <s:submit name="method:anotherMethod" value="Cancel"/> * </s:form> * <!-- END SNIPPET: method-example --> * </pre> * <p/> * <p/> <b>Action prefix</b> <p/> * <p/> * <!-- START SNIPPET: action --> * <p/> * With action-prefix, instead of executing baz action's execute() method (by * default if it isn't overriden in struts.xml to be something else), the * anotherAction action's execute() method (assuming again if it isn't overriden * with something else in struts.xml) will be executed. * <p/> * <!-- END SNIPPET: action --> * <p/> * <pre> * <!-- START SNIPPET: action-example --> * <s:form action="baz"> * <s:textfield label="Enter your name" name="person.name"/> * <s:submit value="Create person"/> * <s:submit name="action:anotherAction" value="Cancel"/> * </s:form> * <!-- END SNIPPET: action-example --> * </pre> * <p/> * <p/> <b>Redirect prefix</b> <p/> * <p/> * <!-- START SNIPPET: redirect --> * <p/> * With redirect-prefix, instead of executing baz action's execute() method (by * default it isn't overriden in struts.xml to be something else), it will get * redirected to, in this case to www.google.com. Internally it uses * ServletRedirectResult to do the task. * <p/> * <!-- END SNIPPET: redirect --> * <p/> * <pre> * <!-- START SNIPPET: redirect-example --> * <s:form action="baz"> * <s:textfield label="Enter your name" name="person.name"/> * <s:submit value="Create person"/> * <s:submit name="redirect:www.google.com" value="Cancel"/> * </s:form> * <!-- END SNIPPET: redirect-example --> * </pre> * <p/> * <p/> <b>Redirect-action prefix</b> <p/> * <p/> * <!-- START SNIPPET: redirect-action --> * <p/> * With redirect-action-prefix, instead of executing baz action's execute() * method (by default it isn't overriden in struts.xml to be something else), it * will get redirected to, in this case 'dashboard.action'. Internally it uses * ServletRedirectResult to do the task and read off the extension from the * struts.properties. * <p/> * <!-- END SNIPPET: redirect-action --> * <p/> * <pre> * <!-- START SNIPPET: redirect-action-example --> * <s:form action="baz"> * <s:textfield label="Enter your name" name="person.name"/> * <s:submit value="Create person"/> * <s:submit name="redirectAction:dashboard" value="Cancel"/> * </s:form> * <!-- END SNIPPET: redirect-action-example --> * </pre> */ public class DefaultActionMapper implements ActionMapper { protected static final String METHOD_PREFIX = "method:"; protected static final String ACTION_PREFIX = "action:"; protected static final String REDIRECT_PREFIX = "redirect:"; protected static final String REDIRECT_ACTION_PREFIX = "redirectAction:"; protected boolean allowDynamicMethodCalls = true; protected boolean allowSlashesInActionNames = false; protected boolean alwaysSelectFullNamespace = false; protected PrefixTrie prefixTrie = null; protected List<String> extensions = new ArrayList<String>() {{ add("action"); add(""); }}; protected Container container; public DefaultActionMapper() { prefixTrie = new PrefixTrie() { { put(METHOD_PREFIX, new ParameterAction() { public void execute(String key, ActionMapping mapping) { if (allowDynamicMethodCalls) { mapping.setMethod(key.substring( METHOD_PREFIX.length())); } } }); put(ACTION_PREFIX, new ParameterAction() { public void execute(String key, ActionMapping mapping) { String name = key.substring(ACTION_PREFIX.length()); if (allowDynamicMethodCalls) { int bang = name.indexOf('!'); if (bang != -1) { String method = name.substring(bang + 1); mapping.setMethod(method); name = name.substring(0, bang); } } mapping.setName(name); } }); put(REDIRECT_PREFIX, new ParameterAction() { public void execute(String key, ActionMapping mapping) { ServletRedirectResult redirect = new ServletRedirectResult(); container.inject(redirect); redirect.setLocation(key.substring(REDIRECT_PREFIX .length())); mapping.setResult(redirect); } }); put(REDIRECT_ACTION_PREFIX, new ParameterAction() { public void execute(String key, ActionMapping mapping) { String location = key.substring(REDIRECT_ACTION_PREFIX .length()); ServletRedirectResult redirect = new ServletRedirectResult(); container.inject(redirect); String extension = getDefaultExtension(); if (extension != null && extension.length() > 0) { location += "." + extension; } redirect.setLocation(location); mapping.setResult(redirect); } }); } }; } /** * Adds a parameter action. Should only be called during initialization * * @param prefix The string prefix to trigger the action * @param parameterAction The parameter action to execute * @since 2.1.0 */ protected void addParameterAction(String prefix, ParameterAction parameterAction) { prefixTrie.put(prefix, parameterAction); } @Inject(StrutsConstants.STRUTS_ENABLE_DYNAMIC_METHOD_INVOCATION) public void setAllowDynamicMethodCalls(String allow) { allowDynamicMethodCalls = "true".equals(allow); } @Inject(StrutsConstants.STRUTS_ENABLE_SLASHES_IN_ACTION_NAMES) public void setSlashesInActionNames(String allow) { allowSlashesInActionNames = "true".equals(allow); } @Inject(StrutsConstants.STRUTS_ALWAYS_SELECT_FULL_NAMESPACE) public void setAlwaysSelectFullNamespace(String val) { this.alwaysSelectFullNamespace = "true".equals(val); } @Inject public void setContainer(Container container) { this.container = container; } @Inject(StrutsConstants.STRUTS_ACTION_EXTENSION) public void setExtensions(String extensions) { if (extensions != null && !"".equals(extensions)) { List<String> list = new ArrayList<String>(); String[] tokens = extensions.split(","); for (String token : tokens) { list.add(token); } if (extensions.endsWith(",")) { list.add(""); } this.extensions = Collections.unmodifiableList(list); } else { this.extensions = null; } } public ActionMapping getMappingFromActionName(String actionName) { ActionMapping mapping = new ActionMapping(); mapping.setName(actionName); return parseActionName(mapping); } /* * (non-Javadoc) * * @see org.apache.struts2.dispatcher.mapper.ActionMapper#getMapping(javax.servlet.http.HttpServletRequest) */ public ActionMapping getMapping(HttpServletRequest request, ConfigurationManager configManager) { ActionMapping mapping = new ActionMapping(); String uri = getUri(request); int indexOfSemicolon = uri.indexOf(";"); uri = (indexOfSemicolon > -1) ? uri.substring(0, indexOfSemicolon) : uri; uri = dropExtension(uri, mapping); if (uri == null) { return null; } parseNameAndNamespace(uri, mapping, configManager); handleSpecialParameters(request, mapping); if (mapping.getName() == null) { return null; } parseActionName(mapping); return mapping; } protected ActionMapping parseActionName(ActionMapping mapping) { if (mapping.getName() == null) { return mapping; } if (allowDynamicMethodCalls) { // handle "name!method" convention. String name = mapping.getName(); int exclamation = name.lastIndexOf("!"); if (exclamation != -1) { mapping.setName(name.substring(0, exclamation)); mapping.setMethod(name.substring(exclamation + 1)); } } return mapping; } /** * Special parameters, as described in the class-level comment, are searched * for and handled. * * @param request The request * @param mapping The action mapping */ public void handleSpecialParameters(HttpServletRequest request, ActionMapping mapping) { // handle special parameter prefixes. Set<String> uniqueParameters = new HashSet<String>(); Map parameterMap = request.getParameterMap(); for (Iterator iterator = parameterMap.keySet().iterator(); iterator .hasNext();) { String key = (String) iterator.next(); // Strip off the image button location info, if found if (key.endsWith(".x") || key.endsWith(".y")) { key = key.substring(0, key.length() - 2); } // Ensure a parameter doesn't get processed twice if (!uniqueParameters.contains(key)) { ParameterAction parameterAction = (ParameterAction) prefixTrie .get(key); if (parameterAction != null) { parameterAction.execute(key, mapping); uniqueParameters.add(key); break; } } } } /** * Parses the name and namespace from the uri * * @param uri The uri * @param mapping The action mapping to populate */ protected void parseNameAndNamespace(String uri, ActionMapping mapping, ConfigurationManager configManager) { String namespace, name; int lastSlash = uri.lastIndexOf("/"); if (lastSlash == -1) { namespace = ""; name = uri; } else if (lastSlash == 0) { // ww-1046, assume it is the root namespace, it will fallback to // default // namespace anyway if not found in root namespace. namespace = "/"; name = uri.substring(lastSlash + 1); } else if (alwaysSelectFullNamespace) { // Simply select the namespace as everything before the last slash namespace = uri.substring(0, lastSlash); name = uri.substring(lastSlash + 1); } else { // Try to find the namespace in those defined, defaulting to "" Configuration config = configManager.getConfiguration(); String prefix = uri.substring(0, lastSlash); namespace = ""; boolean rootAvailable = false; // Find the longest matching namespace, defaulting to the default for (Object cfg : config.getPackageConfigs().values()) { String ns = ((PackageConfig) cfg).getNamespace(); if (ns != null && prefix.startsWith(ns) && (prefix.length() == ns.length() || prefix.charAt(ns.length()) == '/')) { if (ns.length() > namespace.length()) { namespace = ns; } } if ("/".equals(ns)) { rootAvailable = true; } } name = uri.substring(namespace.length() + 1); // Still none found, use root namespace if found if (rootAvailable && "".equals(namespace)) { namespace = "/"; } } if (!allowSlashesInActionNames && name != null) { int pos = name.lastIndexOf('/'); if (pos > -1 && pos < name.length() - 1) { name = name.substring(pos + 1); } } mapping.setNamespace(namespace); mapping.setName(name); } /** * Drops the extension from the action name * * @param name The action name * @return The action name without its extension * @deprecated Since 2.1, use {@link #dropExtension(java.lang.String,org.apache.struts2.dispatcher.mapper.ActionMapping)} instead */ protected String dropExtension(String name) { return dropExtension(name, new ActionMapping()); } /** * Drops the extension from the action name, storing it in the mapping for later use * * @param name The action name * @param mapping The action mapping to store the extension in * @return The action name without its extension */ protected String dropExtension(String name, ActionMapping mapping) { if (extensions == null) { return name; } for (String ext : extensions) { if ("".equals(ext)) { // This should also handle cases such as /foo/bar-1.0/description. It is tricky to // distinquish /foo/bar-1.0 but perhaps adding a numeric check in the future could // work int index = name.lastIndexOf('.'); if (index == -1 || name.indexOf('/', index) >= 0) { return name; } } else { String extension = "." + ext; if (name.endsWith(extension)) { name = name.substring(0, name.length() - extension.length()); mapping.setExtension(ext); return name; } } } return null; } /** * Returns null if no extension is specified. */ protected String getDefaultExtension() { if (extensions == null) { return null; } else { return (String) extensions.get(0); } } /** * Gets the uri from the request * * @param request The request * @return The uri */ protected String getUri(HttpServletRequest request) { // handle http dispatcher includes. String uri = (String) request .getAttribute("javax.servlet.include.servlet_path"); if (uri != null) { return uri; } uri = RequestUtils.getServletPath(request); if (uri != null && !"".equals(uri)) { return uri; } uri = request.getRequestURI(); return uri.substring(request.getContextPath().length()); } /* * (non-Javadoc) * * @see org.apache.struts2.dispatcher.mapper.ActionMapper#getUriFromActionMapping(org.apache.struts2.dispatcher.mapper.ActionMapping) */ public String getUriFromActionMapping(ActionMapping mapping) { StringBuilder uri = new StringBuilder(); if (mapping.getNamespace() != null) { uri.append(mapping.getNamespace()); if (!"/".equals(mapping.getNamespace())) { uri.append("/"); } } String name = mapping.getName(); String params = ""; if (name.indexOf('?') != -1) { params = name.substring(name.indexOf('?')); name = name.substring(0, name.indexOf('?')); } uri.append(name); if (null != mapping.getMethod() && !"".equals(mapping.getMethod())) { uri.append("!").append(mapping.getMethod()); } String extension = mapping.getExtension(); if (extension == null) { // Look for the current extension, if available ActionContext context = ActionContext.getContext(); if (context != null) { ActionMapping orig = (ActionMapping) context.get(ServletActionContext.ACTION_MAPPING); if (orig != null) { extension = orig.getExtension(); } } if (extension == null) { extension = getDefaultExtension(); } } if (extension != null) { if (extension.length() == 0 || (extension.length() > 0 && uri.indexOf('.' + extension) == -1)) { if (extension.length() > 0) { uri.append(".").append(extension); } } } if (params.length() > 0) { uri.append(params); } return uri.toString(); } public boolean isSlashesInActionNames() { return allowSlashesInActionNames; } }