View Javadoc

1   /*
2    * $Id: RestActionMapper.java 680686 2008-07-29 12:53:18Z 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  
22  package org.apache.struts2.rest;
23  
24  import com.opensymphony.xwork2.config.Configuration;
25  import com.opensymphony.xwork2.config.ConfigurationManager;
26  import com.opensymphony.xwork2.config.entities.PackageConfig;
27  import com.opensymphony.xwork2.inject.Inject;
28  import com.opensymphony.xwork2.util.logging.Logger;
29  import com.opensymphony.xwork2.util.logging.LoggerFactory;
30  import org.apache.struts2.StrutsConstants;
31  import org.apache.struts2.dispatcher.mapper.ActionMapping;
32  import org.apache.struts2.dispatcher.mapper.DefaultActionMapper;
33  
34  import javax.servlet.http.HttpServletRequest;
35  import java.util.HashMap;
36  import java.util.Iterator;
37  
38  /***
39   * <!-- START SNIPPET: description -->
40   *
41   * This Restful action mapper enforces Ruby-On-Rails Rest-style mappings.  If the method 
42   * is not specified (via '!' or 'method:' prefix), the method is "guessed" at using 
43   * ReST-style conventions that examine the URL and the HTTP method.  Special care has
44   * been given to ensure this mapper works correctly with the codebehind plugin so that
45   * XML configuration is unnecessary.
46   *  
47   * <p>
48   *   This mapper supports the following parameters:
49   * </p>
50   * <ul>
51   *   <li><code>struts.mapper.idParameterName</code> - If set, this value will be the name
52   *       of the parameter under which the id is stored.  The id will then be removed
53   *       from the action name.  Whether or not the method is specified, the mapper will 
54   *       try to truncate the identifier from the url and store it as a parameter.
55   *   </li>
56   *   <li><code>struts.mapper.indexMethodName</code> - The method name to call for a GET
57   *       request with no id parameter. Defaults to 'index'.
58   *   </li>
59   *   <li><code>struts.mapper.getMethodName</code> - The method name to call for a GET
60   *       request with an id parameter. Defaults to 'show'.
61   *   </li>
62   *   <li><code>struts.mapper.postMethodName</code> - The method name to call for a POST
63   *       request with no id parameter. Defaults to 'create'.
64   *   </li>
65   *   <li><code>struts.mapper.putMethodName</code> - The method name to call for a PUT
66   *       request with an id parameter. Defaults to 'update'.
67   *   </li>
68   *   <li><code>struts.mapper.deleteMethodName</code> - The method name to call for a DELETE
69   *       request with an id parameter. Defaults to 'destroy'.
70   *   </li>
71   *   <li><code>struts.mapper.editMethodName</code> - The method name to call for a GET
72   *       request with an id parameter and the 'edit' view specified. Defaults to 'edit'.
73   *   </li>
74   *   <li><code>struts.mapper.newMethodName</code> - The method name to call for a GET
75   *       request with no id parameter and the 'new' view specified. Defaults to 'editNew'.
76   *   </li>
77   * </ul>
78   * <p>
79   * The following URL's will invoke its methods:
80   * </p>
81   * <ul> 
82   *  <li><code>GET:    /movies                => method="index"</code></li>
83   *  <li><code>GET:    /movies/Thrillers      => method="show", id="Thrillers"</code></li>
84   *  <li><code>GET:    /movies/Thrillers;edit => method="edit", id="Thrillers"</code></li>
85   *  <li><code>GET:    /movies/Thrillers/edit => method="edit", id="Thrillers"</code></li>
86   *  <li><code>GET:    /movies/new            => method="editNew"</code></li>
87   *  <li><code>POST:   /movies                => method="create"</code></li>
88   *  <li><code>PUT:    /movies/Thrillers      => method="update", id="Thrillers"</code></li>
89   *  <li><code>DELETE: /movies/Thrillers      => method="destroy", id="Thrillers"</code></li>
90   * </ul>
91   * <p>
92   * To simulate the HTTP methods PUT and DELETE, since they aren't supported by HTML,
93   * the HTTP parameter "_method" will be used.
94   * </p>
95   * <!-- END SNIPPET: description -->
96   */
97  public class RestActionMapper extends DefaultActionMapper {
98  
99      protected static final Logger LOG = LoggerFactory.getLogger(RestActionMapper.class);
100     public static final String HTTP_METHOD_PARAM = "_method";
101     private String idParameterName = "id";
102     private String indexMethodName = "index";
103     private String getMethodName = "show";
104     private String postMethodName = "create";
105     private String editMethodName = "edit";
106     private String newMethodName = "editNew";
107     private String deleteMethodName = "destroy";
108     private String putMethodName = "update";
109     
110     public RestActionMapper() {
111     }
112     
113     public String getIdParameterName() {
114         return idParameterName;
115     }
116 
117     @Inject(required=false,value=StrutsConstants.STRUTS_ID_PARAMETER_NAME)
118     public void setIdParameterName(String idParameterName) {
119         this.idParameterName = idParameterName;
120     }
121 
122     @Inject(required=false,value="struts.mapper.indexMethodName")
123     public void setIndexMethodName(String indexMethodName) {
124         this.indexMethodName = indexMethodName;
125     }
126 
127     @Inject(required=false,value="struts.mapper.getMethodName")
128     public void setGetMethodName(String getMethodName) {
129         this.getMethodName = getMethodName;
130     }
131 
132     @Inject(required=false,value="struts.mapper.postMethodName")
133     public void setPostMethodName(String postMethodName) {
134         this.postMethodName = postMethodName;
135     }
136 
137     @Inject(required=false,value="struts.mapper.editMethodName")
138     public void setEditMethodName(String editMethodName) {
139         this.editMethodName = editMethodName;
140     }
141 
142     @Inject(required=false,value="struts.mapper.newMethodName")
143     public void setNewMethodName(String newMethodName) {
144         this.newMethodName = newMethodName;
145     }
146 
147     @Inject(required=false,value="struts.mapper.deleteMethodName")
148     public void setDeleteMethodName(String deleteMethodName) {
149         this.deleteMethodName = deleteMethodName;
150     }
151 
152     @Inject(required=false,value="struts.mapper.putMethodName")
153     public void setPutMethodName(String putMethodName) {
154         this.putMethodName = putMethodName;
155     }
156 
157     public ActionMapping getMapping(HttpServletRequest request,
158             ConfigurationManager configManager) {
159         ActionMapping mapping = new ActionMapping();
160         String uri = getUri(request);
161 
162         uri = dropExtension(uri, mapping);
163         if (uri == null) {
164             return null;
165         }
166 
167         parseNameAndNamespace(uri, mapping, configManager);
168 
169         handleSpecialParameters(request, mapping);
170 
171         if (mapping.getName() == null) {
172             return null;
173         }
174 
175         // handle "name!method" convention.
176         String name = mapping.getName();
177         int exclamation = name.lastIndexOf("!");
178         if (exclamation != -1) {
179             mapping.setName(name.substring(0, exclamation));
180             mapping.setMethod(name.substring(exclamation + 1));
181         }
182 
183         String fullName = mapping.getName();
184         // Only try something if the action name is specified
185         if (fullName != null && fullName.length() > 0) {
186 
187             // cut off any ;jsessionid= type appendix but allow the rails-like ;edit
188             int scPos = fullName.indexOf(';');
189             if (scPos > -1 && !"edit".equals(fullName.substring(scPos+1))) {
190                 fullName = fullName.substring(0, scPos);
191             }
192 
193             int lastSlashPos = fullName.lastIndexOf('/');
194             String id = null;
195             if (lastSlashPos > -1) {
196 
197                 // fun trickery to parse 'actionName/id/methodName' in the case of 'animals/dog/edit'
198                 int prevSlashPos = fullName.lastIndexOf('/', lastSlashPos - 1);
199                 if (prevSlashPos > -1) {
200                     mapping.setMethod(fullName.substring(lastSlashPos+1));
201                     fullName = fullName.substring(0, lastSlashPos);
202                     lastSlashPos = prevSlashPos;
203                 }
204                 id = fullName.substring(lastSlashPos+1);
205             }
206 
207 
208 
209             // If a method hasn't been explicitly named, try to guess using ReST-style patterns
210             if (mapping.getMethod() == null) {
211 
212                 // Handle uris with no id, possibly ending in '/'
213                 if (lastSlashPos == -1 || lastSlashPos == fullName.length() -1) {
214 
215                     // Index e.g. foo
216                     if (isGet(request)) {
217                         mapping.setMethod(indexMethodName);
218                         
219                     // Creating a new entry on POST e.g. foo
220                     } else if (isPost(request)) {
221                         mapping.setMethod(postMethodName);
222                     }
223 
224                 // Handle uris with an id at the end
225                 } else if (id != null) {
226                     
227                     // Viewing the form to edit an item e.g. foo/1;edit
228                     if (isGet(request) && id.endsWith(";edit")) {
229                         id = id.substring(0, id.length() - ";edit".length());
230                         mapping.setMethod(editMethodName);
231                         
232                     // Viewing the form to create a new item e.g. foo/new
233                     } else if (isGet(request) && "new".equals(id)) {
234                         mapping.setMethod(newMethodName);
235 
236                     // Removing an item e.g. foo/1
237                     } else if (isDelete(request)) {
238                         mapping.setMethod(deleteMethodName);
239                         
240                     // Viewing an item e.g. foo/1
241                     } else if (isGet(request)) {
242                         mapping.setMethod(getMethodName);
243                     
244                     // Updating an item e.g. foo/1    
245                     }  else if (isPut(request)) {
246                         mapping.setMethod(putMethodName);
247                     }
248                 }
249             }
250 
251             // cut off the id parameter, even if a method is specified
252             if (id != null) {
253                 if (!"new".equals(id)) {
254                     if (mapping.getParams() == null) {
255                         mapping.setParams(new HashMap());
256                     }
257                     mapping.getParams().put(idParameterName, new String[]{id});
258                 }
259                 fullName = fullName.substring(0, lastSlashPos);
260             }
261 
262             mapping.setName(fullName);
263         }
264 
265         return mapping;
266     }
267     
268     /***
269      * Parses the name and namespace from the uri.  Uses the configured package 
270      * namespaces to determine the name and id parameter, to be parsed later.
271      *
272      * @param uri
273      *            The uri
274      * @param mapping
275      *            The action mapping to populate
276      */
277     protected void parseNameAndNamespace(String uri, ActionMapping mapping,
278             ConfigurationManager configManager) {
279         String namespace, name;
280         int lastSlash = uri.lastIndexOf("/");
281         if (lastSlash == -1) {
282             namespace = "";
283             name = uri;
284         } else if (lastSlash == 0) {
285             // ww-1046, assume it is the root namespace, it will fallback to
286             // default
287             // namespace anyway if not found in root namespace.
288             namespace = "/";
289             name = uri.substring(lastSlash + 1);
290         } else {
291             // Try to find the namespace in those defined, defaulting to ""
292             Configuration config = configManager.getConfiguration();
293             String prefix = uri.substring(0, lastSlash);
294             namespace = "";
295             // Find the longest matching namespace, defaulting to the default
296             for (Iterator i = config.getPackageConfigs().values().iterator(); i
297                     .hasNext();) {
298                 String ns = ((PackageConfig) i.next()).getNamespace();
299                 if (ns != null && prefix.startsWith(ns) && (prefix.length() == ns.length() || prefix.charAt(ns.length()) == '/')) {
300                     if (ns.length() > namespace.length()) {
301                         namespace = ns;
302                     }
303                 }
304             }
305 
306             name = uri.substring(namespace.length() + 1);
307         }
308 
309         mapping.setNamespace(namespace);
310         mapping.setName(name);
311     }
312 
313     protected boolean isGet(HttpServletRequest request) {
314         return "get".equalsIgnoreCase(request.getMethod());
315     }
316 
317     protected boolean isPost(HttpServletRequest request) {
318         return "post".equalsIgnoreCase(request.getMethod());
319     }
320 
321     protected boolean isPut(HttpServletRequest request) {
322         if ("put".equalsIgnoreCase(request.getMethod())) {
323             return true;
324         } else {
325             return isPost(request) && "put".equalsIgnoreCase(request.getParameter(HTTP_METHOD_PARAM));
326         }
327     }
328 
329     protected boolean isDelete(HttpServletRequest request) {
330         if ("delete".equalsIgnoreCase(request.getMethod())) {
331             return true;
332         } else {
333             return "delete".equalsIgnoreCase(request.getParameter(HTTP_METHOD_PARAM));
334         }
335     }
336 
337 }