View Javadoc

1   /*
2    * $Id: Restful2ActionMapper.java 540814 2007-05-23 02:46:05Z 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  import com.opensymphony.xwork2.inject.Inject;
25  
26  import javax.servlet.http.HttpServletRequest;
27  import java.util.HashMap;
28  import java.util.StringTokenizer;
29  import java.net.URLDecoder;
30  
31  import org.apache.commons.logging.Log;
32  import org.apache.commons.logging.LogFactory;
33  import org.apache.struts2.StrutsConstants;
34  
35  /***
36   * <!-- START SNIPPET: description -->
37   *
38   * Improved restful action mapper that adds several ReST-style improvements to
39   * action mapping, but supports fully-customized URL's via XML.  The two primary
40   * ReST enhancements are:
41   * <ul>
42   *  <li>If the method is not specified (via '!' or 'method:' prefix), the method is
43   *      "guessed" at using ReST-style conventions that examine the URL and the HTTP
44   *      method.</li>
45   *  <li>Parameters are extracted from the action name, if parameter name/value pairs
46   *      are specified using PARAM_NAME/PARAM_VALUE syntax.
47   * </ul>
48   * <p>
49   * These two improvements allow a GET request for 'category/action/movie/Thrillers' to
50   * be mapped to the action name 'movie' with an id of 'Thrillers' with an extra parameter
51   * named 'category' with a value of 'action'.  A single action mapping can then handle
52   * all CRUD operations using wildcards, e.g.
53   * </p>
54   * <pre>
55   *   &lt;action name="movie/*" className="app.MovieAction"&gt;
56   *     &lt;param name="id"&gt;{0}&lt;/param&gt;
57   *     ...
58   *   &lt;/action&gt;
59   * </pre>
60   * <p>
61   *   This mapper supports the following parameters:
62   * </p>
63   * <ul>
64   *   <li><code>struts.mapper.idParameterName</code> - If set, this value will be the name
65   *       of the parameter under which the id is stored.  The id will then be removed
66   *       from the action name.  This allows restful actions to not require wildcards.
67   *   </li>
68   * </ul>
69   * <p>
70   * The following URL's will invoke its methods:
71   * </p>
72   * <ul>
73   *  <li><code>GET:    /movie/               => method="index"</code></li>
74   *  <li><code>GET:    /movie/Thrillers      => method="view", id="Thrillers"</code></li>
75   *  <li><code>GET:    /movie/Thrillers!edit => method="edit", id="Thrillers"</code></li>
76   *  <li><code>GET:    /movie/new            => method="editNew"</code></li>
77   *  <li><code>POST:   /movie/               => method="create"</code></li>
78   *  <li><code>PUT:    /movie/Thrillers      => method="update", id="Thrillers"</code></li>
79   *  <li><code>DELETE: /movie/Thrillers      => method="remove", id="Thrillers"</code></li>
80   * </ul>
81   * <p>
82   * To simulate the HTTP methods PUT and DELETE, since they aren't supported by HTML,
83   * the HTTP parameter "__http_method" will be used.
84   * </p>
85   * <p>
86   * The syntax and design for this feature was inspired by the ReST support in Ruby on Rails.
87   * See <a href="http://ryandaigle.com/articles/2006/08/01/whats-new-in-edge-rails-simply-restful-support-and-how-to-use-it">
88   * http://ryandaigle.com/articles/2006/08/01/whats-new-in-edge-rails-simply-restful-support-and-how-to-use-it
89   * </a>
90   * </p>
91   *
92   * <!-- END SNIPPET: description -->
93   */
94  public class Restful2ActionMapper extends DefaultActionMapper {
95  
96      protected static final Log LOG = LogFactory.getLog(Restful2ActionMapper.class);
97      public static final String HTTP_METHOD_PARAM = "__http_method";
98      private String idParameterName = null;
99      
100     public Restful2ActionMapper() {
101     	setSlashesInActionNames("true");
102     }
103     
104 
105     /*
106     * (non-Javadoc)
107     *
108     * @see org.apache.struts2.dispatcher.mapper.ActionMapper#getMapping(javax.servlet.http.HttpServletRequest)
109     */
110     public ActionMapping getMapping(HttpServletRequest request, ConfigurationManager configManager) {
111 
112     	if (!isSlashesInActionNames()) {
113     		throw new IllegalStateException("This action mapper requires the setting 'slashesInActionNames' to be set to 'true'");
114     	}
115         ActionMapping mapping = super.getMapping(request, configManager);
116         
117         if (mapping == null) {
118             return null;
119         }
120 
121         String actionName = mapping.getName();
122 
123         // Only try something if the action name is specified
124         if (actionName != null && actionName.length() > 0) {
125             int lastSlashPos = actionName.lastIndexOf('/');
126 
127             // If a method hasn't been explicitly named, try to guess using ReST-style patterns
128             if (mapping.getMethod() == null) {
129 
130                 if (lastSlashPos == actionName.length() -1) {
131 
132                     // Index e.g. foo/
133                     if (isGet(request)) {
134                         mapping.setMethod("index");
135                         
136                     // Creating a new entry on POST e.g. foo/
137                     } else if (isPost(request)) {
138                         mapping.setMethod("create");
139                     }
140 
141                 } else if (lastSlashPos > -1) {
142                     String id = actionName.substring(lastSlashPos+1);
143 
144                     // Viewing the form to create a new item e.g. foo/new
145                     if (isGet(request) && "new".equals(id)) {
146                         mapping.setMethod("editNew");
147 
148                     // Viewing an item e.g. foo/1
149                     } else if (isGet(request)) {
150                         mapping.setMethod("view");
151 
152                     // Removing an item e.g. foo/1
153                     } else if (isDelete(request)) {
154                         mapping.setMethod("remove");
155                     
156                     // Updating an item e.g. foo/1    
157                     }  else if (isPut(request)) {
158                         mapping.setMethod("update");
159                     }
160                     
161                     if (idParameterName != null) {
162                     	if (mapping.getParams() == null) {
163                             mapping.setParams(new HashMap());
164                         }
165                     	mapping.getParams().put(idParameterName, id);
166                     }
167                 }
168                 
169                 if (idParameterName != null && lastSlashPos > -1) {
170                 	actionName = actionName.substring(0, lastSlashPos);
171                 }
172             }
173 
174             // Try to determine parameters from the url before the action name
175             int actionSlashPos = actionName.lastIndexOf('/', lastSlashPos - 1);
176             if (actionSlashPos > 0 && actionSlashPos < lastSlashPos) {
177                 String params = actionName.substring(0, actionSlashPos);
178                 HashMap<String,String> parameters = new HashMap<String,String>();
179                 try {
180                     StringTokenizer st = new StringTokenizer(params, "/");
181                     boolean isNameTok = true;
182                     String paramName = null;
183                     String paramValue;
184 
185                     while (st.hasMoreTokens()) {
186                         if (isNameTok) {
187                             paramName = URLDecoder.decode(st.nextToken(), "UTF-8");
188                             isNameTok = false;
189                         } else {
190                             paramValue = URLDecoder.decode(st.nextToken(), "UTF-8");
191 
192                             if ((paramName != null) && (paramName.length() > 0)) {
193                                 parameters.put(paramName, paramValue);
194                             }
195 
196                             isNameTok = true;
197                         }
198                     }
199                     if (parameters.size() > 0) {
200                         if (mapping.getParams() == null) {
201                             mapping.setParams(new HashMap());
202                         }
203                         mapping.getParams().putAll(parameters);
204                     }
205                 } catch (Exception e) {
206                     LOG.warn(e);
207                 }
208                 mapping.setName(actionName.substring(actionSlashPos+1));
209             }
210         }
211 
212         return mapping;
213     }
214 
215     protected boolean isGet(HttpServletRequest request) {
216         return "get".equalsIgnoreCase(request.getMethod());
217     }
218 
219     protected boolean isPost(HttpServletRequest request) {
220         return "post".equalsIgnoreCase(request.getMethod());
221     }
222 
223     protected boolean isPut(HttpServletRequest request) {
224         if ("put".equalsIgnoreCase(request.getMethod())) {
225             return true;
226         } else {
227             return isPost(request) && "put".equalsIgnoreCase(request.getParameter(HTTP_METHOD_PARAM));
228         }
229     }
230 
231     protected boolean isDelete(HttpServletRequest request) {
232         if ("delete".equalsIgnoreCase(request.getMethod())) {
233             return true;
234         } else {
235             return isPost(request) && "delete".equalsIgnoreCase(request.getParameter(HTTP_METHOD_PARAM));
236         }
237     }
238 
239 	public String getIdParameterName() {
240 		return idParameterName;
241 	}
242 
243 	@Inject(required=false,value=StrutsConstants.STRUTS_ID_PARAMETER_NAME)
244 	public void setIdParameterName(String idParameterName) {
245 		this.idParameterName = idParameterName;
246 	}
247     
248     
249 
250 }