Author: mrdon Date: Mon Oct 16 21:19:23 2006 New Revision: 464796 URL: http://svn.apache.org/viewvc?view=rev&rev=464796 Log: Added first cut at a restful action mapper that supports actions mapped via XML WW-1475
Added: struts/struts2/trunk/core/src/main/java/org/apache/struts2/dispatcher/mapper/Restful2ActionMapper.java struts/struts2/trunk/core/src/test/java/org/apache/struts2/dispatcher/mapper/Restful2ActionMapperTest.java Added: struts/struts2/trunk/core/src/main/java/org/apache/struts2/dispatcher/mapper/Restful2ActionMapper.java URL: http://svn.apache.org/viewvc/struts/struts2/trunk/core/src/main/java/org/apache/struts2/dispatcher/mapper/Restful2ActionMapper.java?view=auto&rev=464796 ============================================================================== --- struts/struts2/trunk/core/src/main/java/org/apache/struts2/dispatcher/mapper/Restful2ActionMapper.java (added) +++ struts/struts2/trunk/core/src/main/java/org/apache/struts2/dispatcher/mapper/Restful2ActionMapper.java Mon Oct 16 21:19:23 2006 @@ -0,0 +1,198 @@ +/* + * $Id: RestfulActionMapper.java 449367 2006-09-24 06:49:04Z mrdon $ + * + * Copyright 2006 The Apache Software Foundation. + * + * Licensed 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.config.ConfigurationManager; + +import javax.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.StringTokenizer; +import java.net.URLDecoder; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Improved restful action mapper that adds several ReST-style improvements to + * action mapping, but supports fully-customized URL's via XML. The two primary + * ReST enhancements are: + * <ul> + * <li>If the method is not specified (via '!' or 'method:' prefix), the method is + * "guessed" at using ReST-style conventions that examine the URL and the HTTP + * method.</li> + * <li>Parameters are extracted from the action name, if parameter name/value pairs + * are specified using PARAM_NAME/PARAM_VALUE syntax. + * </ul> + * <p> + * These two improvements allow a GET request for 'category/action/movie/Swingers' to + * be mapped to the action name 'movie' with an id of 'Swingers' with an extra parameter + * named 'category' with a value of 'action'. A single action mapping can then handle + * all CRUD operations using wildcards, e.g. + * </p> + * <pre> + * <action name="movie/*" className="app.MovieAction"> + * <param name="id">{0}</param> + * ... + * </action> + * </pre> + * <p> + * The following URL's will invoke its methods: + * </p> + * <ul> + * <li><code>GET: /movie => method="index"</code></li> + * <li><code>GET: /movie/Swingers => method="view", id="Swingers"</code></li> + * <li><code>GET: /movie/Swingers!edit => method="edit", id="Swingers"</code></li> + * <li><code>GET: /movie/new => method="editNew"</code></li> + * <li><code>POST: /movie/Swingers => method="update"</code></li> + * <li><code>PUT: /movie/ => method="create"</code></li> + * <li><code>DELETE: /movie/Swingers => method="remove"</code></li> + * </ul> + * <p> + * To simulate the HTTP methods PUT and DELETE, since they aren't supported by HTML, + * the HTTP parameter "__http_method" will be used. + * </p> + * <p> + * The syntax and design for this feature was inspired by the ReST support in Ruby on Rails. + * See <a href="http://ryandaigle.com/articles/2006/08/01/whats-new-in-edge-rails-simply-restful-support-and-how-to-use-it"> + * http://ryandaigle.com/articles/2006/08/01/whats-new-in-edge-rails-simply-restful-support-and-how-to-use-it + * </a> + * </p> + */ +public class Restful2ActionMapper extends DefaultActionMapper { + + protected static final Log LOG = LogFactory.getLog(Restful2ActionMapper.class); + private static final String HTTP_METHOD_PARAM = "__http_method"; + + /* + * (non-Javadoc) + * + * @see org.apache.struts2.dispatcher.mapper.ActionMapper#getMapping(javax.servlet.http.HttpServletRequest) + */ + public ActionMapping getMapping(HttpServletRequest request, ConfigurationManager configManager) { + + ActionMapping mapping = super.getMapping(request, configManager); + + String actionName = mapping.getName(); + + // Only try something if the action name is specified + if (actionName != null && actionName.length() > 0) { + int lastSlashPos = actionName.lastIndexOf('/'); + + // If a method hasn't been explicitly named, try to guess using ReST-style patterns + if (mapping.getMethod() == null) { + + if (lastSlashPos == actionName.length() -1) { + + // Index e.g. foo/ + if (isGet(request)) { + mapping.setMethod("index"); + + // Creating a new entry on POST e.g. foo/ + } else if (isPost(request)) { + mapping.setMethod("create"); + } + + } else if (lastSlashPos > -1) { + String id = actionName.substring(lastSlashPos+1); + + // Viewing the form to create a new item e.g. foo/new + if (isGet(request) && "new".equals(id)) { + mapping.setMethod("editNew"); + + // Viewing an item e.g. foo/1 + } else if (isGet(request)) { + mapping.setMethod("view"); + + // Updating an item e.g. foo/1 + } else if (isPut(request)) { + mapping.setMethod("update"); + + // Removing an item e.g. foo/1 + } else if (isDelete(request)) { + mapping.setMethod("remove"); + } + } + } + + // Try to determine parameters from the url before the action name + int actionSlashPos = actionName.lastIndexOf('/', lastSlashPos - 1); + if (actionSlashPos > 0 && actionSlashPos < lastSlashPos) { + String params = actionName.substring(0, actionSlashPos); + HashMap<String,String> parameters = new HashMap<String,String>(); + try { + StringTokenizer st = new StringTokenizer(params, "/"); + boolean isNameTok = true; + String paramName = null; + String paramValue; + + while (st.hasMoreTokens()) { + if (isNameTok) { + paramName = URLDecoder.decode(st.nextToken(), "UTF-8"); + isNameTok = false; + } else { + paramValue = URLDecoder.decode(st.nextToken(), "UTF-8"); + + if ((paramName != null) && (paramName.length() > 0)) { + parameters.put(paramName, paramValue); + } + + isNameTok = true; + } + } + if (parameters.size() > 0) { + if (mapping.getParams() == null) { + mapping.setParams(new HashMap()); + } + mapping.getParams().putAll(parameters); + } + } catch (Exception e) { + LOG.warn(e); + } + mapping.setName(actionName.substring(actionSlashPos+1)); + } + } + + + return mapping; + } + + protected boolean isGet(HttpServletRequest request) { + return "get".equalsIgnoreCase(request.getMethod()); + } + + protected boolean isPost(HttpServletRequest request) { + return "post".equalsIgnoreCase(request.getMethod()); + } + + protected boolean isPut(HttpServletRequest request) { + if ("put".equalsIgnoreCase(request.getMethod())) { + return true; + } else { + return isPost(request) && "put".equalsIgnoreCase(request.getParameter(HTTP_METHOD_PARAM)); + } + } + + protected boolean isDelete(HttpServletRequest request) { + if ("delete".equalsIgnoreCase(request.getMethod())) { + return true; + } else { + return isPost(request) && "delete".equalsIgnoreCase(request.getParameter(HTTP_METHOD_PARAM)); + } + } + +} Added: struts/struts2/trunk/core/src/test/java/org/apache/struts2/dispatcher/mapper/Restful2ActionMapperTest.java URL: http://svn.apache.org/viewvc/struts/struts2/trunk/core/src/test/java/org/apache/struts2/dispatcher/mapper/Restful2ActionMapperTest.java?view=auto&rev=464796 ============================================================================== --- struts/struts2/trunk/core/src/test/java/org/apache/struts2/dispatcher/mapper/Restful2ActionMapperTest.java (added) +++ struts/struts2/trunk/core/src/test/java/org/apache/struts2/dispatcher/mapper/Restful2ActionMapperTest.java Mon Oct 16 21:19:23 2006 @@ -0,0 +1,103 @@ +/* + * $Id: RestfulActionMapper.java 449367 2006-09-24 06:49:04Z mrdon $ + * + * Copyright 2006 The Apache Software Foundation. + * + * Licensed 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 org.apache.struts2.StrutsTestCase; +import org.apache.struts2.StrutsConstants; +import org.apache.struts2.config.Settings; +import com.mockobjects.servlet.MockHttpServletRequest; +import com.opensymphony.xwork2.config.ConfigurationManager; +import com.opensymphony.xwork2.config.Configuration; +import com.opensymphony.xwork2.config.entities.PackageConfig; +import com.opensymphony.xwork2.config.impl.DefaultConfiguration; + +import java.util.HashMap; + +public class Restful2ActionMapperTest extends StrutsTestCase { + + private MockHttpServletRequest req; + private ConfigurationManager configManager; + private Configuration config; + + @Override + protected void setUp() throws Exception { + super.setUp(); + Settings.set(StrutsConstants.STRUTS_ACTION_EXTENSION, ""); + req = new MockHttpServletRequest(); + req.setupGetParameterMap(new HashMap()); + req.setupGetContextPath("/my/namespace"); + + config = new DefaultConfiguration(); + PackageConfig pkg = new PackageConfig("myns", "/my/namespace", false, null); + PackageConfig pkg2 = new PackageConfig("my", "/my", false, null); + config.addPackageConfig("mvns", pkg); + config.addPackageConfig("my", pkg2); + configManager = new ConfigurationManager() { + public Configuration getConfiguration() { + return config; + } + }; + } + + public void testGetIndex() throws Exception { + req.setupGetRequestURI("/my/namespace/foo/"); + req.setupGetServletPath("/my/namespace/foo/"); + req.setupGetAttribute(null); + req.addExpectedGetAttributeName("javax.servlet.include.servlet_path"); + req.setupGetMethod("GET"); + + Restful2ActionMapper mapper = new Restful2ActionMapper(); + ActionMapping mapping = mapper.getMapping(req, configManager); + + assertEquals("/my/namespace", mapping.getNamespace()); + assertEquals("foo/", mapping.getName()); + assertEquals("index", mapping.getMethod()); + } + + public void testGetIndexWithParams() throws Exception { + req.setupGetRequestURI("/my/namespace/bar/1/foo/"); + req.setupGetServletPath("/my/namespace/bar/1/foo/"); + req.setupGetAttribute(null); + req.addExpectedGetAttributeName("javax.servlet.include.servlet_path"); + req.setupGetMethod("GET"); + + Restful2ActionMapper mapper = new Restful2ActionMapper(); + ActionMapping mapping = mapper.getMapping(req, configManager); + + assertEquals("/my/namespace", mapping.getNamespace()); + assertEquals("foo/", mapping.getName()); + assertEquals("index", mapping.getMethod()); + assertEquals(1, mapping.getParams().size()); + assertEquals("1", mapping.getParams().get("bar")); + } + + public void testPostCreate() throws Exception { + req.setupGetRequestURI("/my/namespace/foo/"); + req.setupGetServletPath("/my/namespace/foo/"); + req.setupGetAttribute(null); + req.addExpectedGetAttributeName("javax.servlet.include.servlet_path"); + req.setupGetMethod("POST"); + + Restful2ActionMapper mapper = new Restful2ActionMapper(); + ActionMapping mapping = mapper.getMapping(req, configManager); + + assertEquals("/my/namespace", mapping.getNamespace()); + assertEquals("foo/", mapping.getName()); + assertEquals("create", mapping.getMethod()); + } +}