You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@struts.apache.org by mr...@apache.org on 2006/10/17 06:19:24 UTC

svn commit: r464796 - in /struts/struts2/trunk/core/src: main/java/org/apache/struts2/dispatcher/mapper/Restful2ActionMapper.java test/java/org/apache/struts2/dispatcher/mapper/Restful2ActionMapperTest.java

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>
+ *   &lt;action name="movie/*" className="app.MovieAction"&gt;
+ *     &lt;param name="id"&gt;{0}&lt;/param&gt;
+ *     ...
+ *   &lt;/action&gt;
+ * </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());
+    }
+}