View Javadoc

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