View Javadoc

1   /*
2    * $Id: RestActionMapper.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.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             int lastSlashPos = fullName.lastIndexOf('/');
187             String id = null;
188             if (lastSlashPos > -1) {
189 
190                 // fun trickery to parse 'actionName/id/methodName' in the case of 'animals/dog/edit'
191                 int prevSlashPos = fullName.lastIndexOf('/', lastSlashPos - 1);
192                 if (prevSlashPos > -1) {
193                     mapping.setMethod(fullName.substring(lastSlashPos+1));
194                     fullName = fullName.substring(0, lastSlashPos);
195                     lastSlashPos = prevSlashPos;
196                 }
197                 id = fullName.substring(lastSlashPos+1);
198             }
199 
200 
201 
202             // If a method hasn't been explicitly named, try to guess using ReST-style patterns
203             if (mapping.getMethod() == null) {
204 
205                 // Handle uris with no id, possibly ending in '/'
206                 if (lastSlashPos == -1 || lastSlashPos == fullName.length() -1) {
207 
208                     // Index e.g. foo
209                     if (isGet(request)) {
210                         mapping.setMethod(indexMethodName);
211                         
212                     // Creating a new entry on POST e.g. foo
213                     } else if (isPost(request)) {
214                         mapping.setMethod(postMethodName);
215                     }
216 
217                 // Handle uris with an id at the end
218                 } else if (id != null) {
219                     
220                     // Viewing the form to edit an item e.g. foo/1;edit
221                     if (isGet(request) && id.endsWith(";edit")) {
222                         id = id.substring(0, id.length() - ";edit".length());
223                         mapping.setMethod(editMethodName);
224                         
225                     // Viewing the form to create a new item e.g. foo/new
226                     } else if (isGet(request) && "new".equals(id)) {
227                         mapping.setMethod(newMethodName);
228 
229                     // Removing an item e.g. foo/1
230                     } else if (isDelete(request)) {
231                         mapping.setMethod(deleteMethodName);
232                         
233                     // Viewing an item e.g. foo/1
234                     } else if (isGet(request)) {
235                         mapping.setMethod(getMethodName);
236                     
237                     // Updating an item e.g. foo/1    
238                     }  else if (isPut(request)) {
239                         mapping.setMethod(putMethodName);
240                     }
241                 }
242             }
243             
244             // cut off the id parameter, even if a method is specified
245             if (id != null) {
246                 if (!"new".equals(id)) {
247                     if (mapping.getParams() == null) {
248                         mapping.setParams(new HashMap());
249                     }
250                     mapping.getParams().put(idParameterName, new String[]{id});
251                 }
252                 fullName = fullName.substring(0, lastSlashPos);
253             }
254 
255             mapping.setName(fullName);
256         }
257 
258         return mapping;
259     }
260     
261     /***
262      * Parses the name and namespace from the uri.  Uses the configured package 
263      * namespaces to determine the name and id parameter, to be parsed later.
264      *
265      * @param uri
266      *            The uri
267      * @param mapping
268      *            The action mapping to populate
269      */
270     protected void parseNameAndNamespace(String uri, ActionMapping mapping,
271             ConfigurationManager configManager) {
272         String namespace, name;
273         int lastSlash = uri.lastIndexOf("/");
274         if (lastSlash == -1) {
275             namespace = "";
276             name = uri;
277         } else if (lastSlash == 0) {
278             // ww-1046, assume it is the root namespace, it will fallback to
279             // default
280             // namespace anyway if not found in root namespace.
281             namespace = "/";
282             name = uri.substring(lastSlash + 1);
283         } else {
284             // Try to find the namespace in those defined, defaulting to ""
285             Configuration config = configManager.getConfiguration();
286             String prefix = uri.substring(0, lastSlash);
287             namespace = "";
288             // Find the longest matching namespace, defaulting to the default
289             for (Iterator i = config.getPackageConfigs().values().iterator(); i
290                     .hasNext();) {
291                 String ns = ((PackageConfig) i.next()).getNamespace();
292                 if (ns != null && prefix.startsWith(ns) && (prefix.length() == ns.length() || prefix.charAt(ns.length()) == '/')) {
293                     if (ns.length() > namespace.length()) {
294                         namespace = ns;
295                     }
296                 }
297             }
298 
299             name = uri.substring(namespace.length() + 1);
300         }
301 
302         mapping.setNamespace(namespace);
303         mapping.setName(name);
304     }
305 
306     protected boolean isGet(HttpServletRequest request) {
307         return "get".equalsIgnoreCase(request.getMethod());
308     }
309 
310     protected boolean isPost(HttpServletRequest request) {
311         return "post".equalsIgnoreCase(request.getMethod());
312     }
313 
314     protected boolean isPut(HttpServletRequest request) {
315         if ("put".equalsIgnoreCase(request.getMethod())) {
316             return true;
317         } else {
318             return isPost(request) && "put".equalsIgnoreCase(request.getParameter(HTTP_METHOD_PARAM));
319         }
320     }
321 
322     protected boolean isDelete(HttpServletRequest request) {
323         if ("delete".equalsIgnoreCase(request.getMethod())) {
324             return true;
325         } else {
326             return "delete".equalsIgnoreCase(request.getParameter(HTTP_METHOD_PARAM));
327         }
328     }
329 
330 }