View Javadoc

1   /*
2    * $Id: Restful2ActionMapper.java 768855 2009-04-27 02:09:35Z wesw $
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     	if (!isSlashesInActionNames()) {
114     		throw new IllegalStateException("This action mapper requires the setting 'slashesInActionNames' to be set to 'true'");
115     	}
116         ActionMapping mapping = super.getMapping(request, configManager);
117         
118         if (mapping == null) {
119             return null;
120         }
121 
122         String actionName = mapping.getName();
123 
124         String id = null;
125 
126         // Only try something if the action name is specified
127         if (actionName != null && actionName.length() > 0) {
128 
129             int lastSlashPos = actionName.lastIndexOf('/');
130             if (lastSlashPos > -1) {
131                 id = actionName.substring(lastSlashPos+1);
132             }
133 
134 
135             // If a method hasn't been explicitly named, try to guess using ReST-style patterns
136             if (mapping.getMethod() == null) {
137 
138                 if (lastSlashPos == actionName.length() -1) {
139 
140                     // Index e.g. foo/
141                     if (isGet(request)) {
142                         mapping.setMethod("index");
143                         
144                     // Creating a new entry on POST e.g. foo/
145                     } else if (isPost(request)) {
146                         mapping.setMethod("create");
147                     }
148 
149                 } else if (lastSlashPos > -1) {
150                     // Viewing the form to create a new item e.g. foo/new
151                     if (isGet(request) && "new".equals(id)) {
152                         mapping.setMethod("editNew");
153 
154                     // Viewing an item e.g. foo/1
155                     } else if (isGet(request)) {
156                         mapping.setMethod("view");
157 
158                     // Removing an item e.g. foo/1
159                     } else if (isDelete(request)) {
160                         mapping.setMethod("remove");
161                     
162                     // Updating an item e.g. foo/1    
163                     }  else if (isPut(request)) {
164                         mapping.setMethod("update");
165                     }
166                     
167                 }
168                 
169                 if (idParameterName != null && lastSlashPos > -1) {
170                 	actionName = actionName.substring(0, lastSlashPos);
171                 }
172             }
173 
174             if (idParameterName != null && id != null) {
175                 if (mapping.getParams() == null) {
176                     mapping.setParams(new HashMap<String, Object>());
177                 }
178                 mapping.getParams().put(idParameterName, id);
179             }
180 
181             // Try to determine parameters from the url before the action name
182             int actionSlashPos = actionName.lastIndexOf('/', lastSlashPos - 1);
183             if (actionSlashPos > 0 && actionSlashPos < lastSlashPos) {
184                 String params = actionName.substring(0, actionSlashPos);
185                 HashMap<String,String> parameters = new HashMap<String,String>();
186                 try {
187                     StringTokenizer st = new StringTokenizer(params, "/");
188                     boolean isNameTok = true;
189                     String paramName = null;
190                     String paramValue;
191 
192                     while (st.hasMoreTokens()) {
193                         if (isNameTok) {
194                             paramName = URLDecoder.decode(st.nextToken(), "UTF-8");
195                             isNameTok = false;
196                         } else {
197                             paramValue = URLDecoder.decode(st.nextToken(), "UTF-8");
198 
199                             if ((paramName != null) && (paramName.length() > 0)) {
200                                 parameters.put(paramName, paramValue);
201                             }
202 
203                             isNameTok = true;
204                         }
205                     }
206                     if (parameters.size() > 0) {
207                         if (mapping.getParams() == null) {
208                             mapping.setParams(new HashMap<String, Object>());
209                         }
210                         mapping.getParams().putAll(parameters);
211                     }
212                 } catch (Exception e) {
213                     LOG.warn("Unable to determine parameters from the url", e);
214                 }
215                 mapping.setName(actionName.substring(actionSlashPos+1));
216             }
217         }
218 
219         return mapping;
220     }
221 
222     protected boolean isGet(HttpServletRequest request) {
223         return "get".equalsIgnoreCase(request.getMethod());
224     }
225 
226     protected boolean isPost(HttpServletRequest request) {
227         return "post".equalsIgnoreCase(request.getMethod());
228     }
229 
230     protected boolean isPut(HttpServletRequest request) {
231         if ("put".equalsIgnoreCase(request.getMethod())) {
232             return true;
233         } else {
234             return isPost(request) && "put".equalsIgnoreCase(request.getParameter(HTTP_METHOD_PARAM));
235         }
236     }
237 
238     protected boolean isDelete(HttpServletRequest request) {
239         if ("delete".equalsIgnoreCase(request.getMethod())) {
240             return true;
241         } else {
242             return isPost(request) && "delete".equalsIgnoreCase(request.getParameter(HTTP_METHOD_PARAM));
243         }
244     }
245 
246 	public String getIdParameterName() {
247 		return idParameterName;
248 	}
249 
250 	@Inject(required=false,value=StrutsConstants.STRUTS_ID_PARAMETER_NAME)
251 	public void setIdParameterName(String idParameterName) {
252 		this.idParameterName = idParameterName;
253 	}
254 }