View Javadoc

1   /*
2    * $Id: Restful2ActionMapper.java 496932 2007-01-17 04:12:46Z mrdon $
3    *
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *  http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  package org.apache.struts2.dispatcher.mapper;
22  
23  import com.opensymphony.xwork2.config.ConfigurationManager;
24  
25  import javax.servlet.http.HttpServletRequest;
26  import java.util.HashMap;
27  import java.util.StringTokenizer;
28  import java.net.URLDecoder;
29  
30  import org.apache.commons.logging.Log;
31  import org.apache.commons.logging.LogFactory;
32  
33  /***
34   * <!-- START SNIPPET: description -->
35   *
36   * Improved restful action mapper that adds several ReST-style improvements to
37   * action mapping, but supports fully-customized URL's via XML.  The two primary
38   * ReST enhancements are:
39   * <ul>
40   *  <li>If the method is not specified (via '!' or 'method:' prefix), the method is
41   *      "guessed" at using ReST-style conventions that examine the URL and the HTTP
42   *      method.</li>
43   *  <li>Parameters are extracted from the action name, if parameter name/value pairs
44   *      are specified using PARAM_NAME/PARAM_VALUE syntax.
45   * </ul>
46   * <p>
47   * These two improvements allow a GET request for 'category/action/movie/Thrillers' to
48   * be mapped to the action name 'movie' with an id of 'Thrillers' with an extra parameter
49   * named 'category' with a value of 'action'.  A single action mapping can then handle
50   * all CRUD operations using wildcards, e.g.
51   * </p>
52   * <pre>
53   *   &lt;action name="movie/*" className="app.MovieAction"&gt;
54   *     &lt;param name="id"&gt;{0}&lt;/param&gt;
55   *     ...
56   *   &lt;/action&gt;
57   * </pre>
58   * <p>
59   * The following URL's will invoke its methods:
60   * </p>
61   * <ul>
62   *  <li><code>GET:    /movie               => method="index"</code></li>
63   *  <li><code>GET:    /movie/Thrillers      => method="view", id="Thrillers"</code></li>
64   *  <li><code>GET:    /movie/Thrillers!edit => method="edit", id="Thrillers"</code></li>
65   *  <li><code>GET:    /movie/new           => method="editNew"</code></li>
66   *  <li><code>POST:   /movie/Thrillers      => method="create"</code></li>
67   *  <li><code>PUT:    /movie/              => method="update"</code></li>
68   *  <li><code>DELETE: /movie/Thrillers      => method="remove"</code></li>
69   * </ul>
70   * <p>
71   * To simulate the HTTP methods PUT and DELETE, since they aren't supported by HTML,
72   * the HTTP parameter "__http_method" will be used.
73   * </p>
74   * <p>
75   * The syntax and design for this feature was inspired by the ReST support in Ruby on Rails.
76   * See <a href="http://ryandaigle.com/articles/2006/08/01/whats-new-in-edge-rails-simply-restful-support-and-how-to-use-it">
77   * http://ryandaigle.com/articles/2006/08/01/whats-new-in-edge-rails-simply-restful-support-and-how-to-use-it
78   * </a>
79   * </p>
80   *
81   * <!-- END SNIPPET: description -->
82   */
83  public class Restful2ActionMapper extends DefaultActionMapper {
84  
85      protected static final Log LOG = LogFactory.getLog(Restful2ActionMapper.class);
86      private static final String HTTP_METHOD_PARAM = "__http_method";
87  
88      /*
89      * (non-Javadoc)
90      *
91      * @see org.apache.struts2.dispatcher.mapper.ActionMapper#getMapping(javax.servlet.http.HttpServletRequest)
92      */
93      public ActionMapping getMapping(HttpServletRequest request, ConfigurationManager configManager) {
94  
95          ActionMapping mapping = super.getMapping(request, configManager);
96          
97          if (mapping == null) {
98              return null;
99          }
100 
101         String actionName = mapping.getName();
102 
103         // Only try something if the action name is specified
104         if (actionName != null && actionName.length() > 0) {
105             int lastSlashPos = actionName.lastIndexOf('/');
106 
107             // If a method hasn't been explicitly named, try to guess using ReST-style patterns
108             if (mapping.getMethod() == null) {
109 
110                 if (lastSlashPos == actionName.length() -1) {
111 
112                     // Index e.g. foo/
113                     if (isGet(request)) {
114                         mapping.setMethod("index");
115                         
116                     // Creating a new entry on POST e.g. foo/
117                     } else if (isPut(request)) {
118                         mapping.setMethod("create");
119                     }
120 
121                 } else if (lastSlashPos > -1) {
122                     String id = actionName.substring(lastSlashPos+1);
123 
124                     // Viewing the form to create a new item e.g. foo/new
125                     if (isGet(request) && "new".equals(id)) {
126                         mapping.setMethod("editNew");
127 
128                     // Viewing an item e.g. foo/1
129                     } else if (isGet(request)) {
130                         mapping.setMethod("view");
131 
132                     // Updating an item e.g. foo/1
133                     } else if (isPost(request)) {
134                         mapping.setMethod("update");
135 
136                     // Removing an item e.g. foo/1
137                     } else if (isDelete(request)) {
138                         mapping.setMethod("remove");
139                     }
140                 }
141             }
142 
143             // Try to determine parameters from the url before the action name
144             int actionSlashPos = actionName.lastIndexOf('/', lastSlashPos - 1);
145             if (actionSlashPos > 0 && actionSlashPos < lastSlashPos) {
146                 String params = actionName.substring(0, actionSlashPos);
147                 HashMap<String,String> parameters = new HashMap<String,String>();
148                 try {
149                     StringTokenizer st = new StringTokenizer(params, "/");
150                     boolean isNameTok = true;
151                     String paramName = null;
152                     String paramValue;
153 
154                     while (st.hasMoreTokens()) {
155                         if (isNameTok) {
156                             paramName = URLDecoder.decode(st.nextToken(), "UTF-8");
157                             isNameTok = false;
158                         } else {
159                             paramValue = URLDecoder.decode(st.nextToken(), "UTF-8");
160 
161                             if ((paramName != null) && (paramName.length() > 0)) {
162                                 parameters.put(paramName, paramValue);
163                             }
164 
165                             isNameTok = true;
166                         }
167                     }
168                     if (parameters.size() > 0) {
169                         if (mapping.getParams() == null) {
170                             mapping.setParams(new HashMap());
171                         }
172                         mapping.getParams().putAll(parameters);
173                     }
174                 } catch (Exception e) {
175                     LOG.warn(e);
176                 }
177                 mapping.setName(actionName.substring(actionSlashPos+1));
178             }
179         }
180 
181         return mapping;
182     }
183 
184     protected boolean isGet(HttpServletRequest request) {
185         return "get".equalsIgnoreCase(request.getMethod());
186     }
187 
188     protected boolean isPost(HttpServletRequest request) {
189         return "post".equalsIgnoreCase(request.getMethod());
190     }
191 
192     protected boolean isPut(HttpServletRequest request) {
193         if ("put".equalsIgnoreCase(request.getMethod())) {
194             return true;
195         } else {
196             return isPost(request) && "put".equalsIgnoreCase(request.getParameter(HTTP_METHOD_PARAM));
197         }
198     }
199 
200     protected boolean isDelete(HttpServletRequest request) {
201         if ("delete".equalsIgnoreCase(request.getMethod())) {
202             return true;
203         } else {
204             return isPost(request) && "delete".equalsIgnoreCase(request.getParameter(HTTP_METHOD_PARAM));
205         }
206     }
207 
208 }