1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 * <action name="movie/*" className="app.MovieAction">
56 * <param name="id">{0}</param>
57 * ...
58 * </action>
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
107
108
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
124 if (actionName != null && actionName.length() > 0) {
125 int lastSlashPos = actionName.lastIndexOf('/');
126
127
128 if (mapping.getMethod() == null) {
129
130 if (lastSlashPos == actionName.length() -1) {
131
132
133 if (isGet(request)) {
134 mapping.setMethod("index");
135
136
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
145 if (isGet(request) && "new".equals(id)) {
146 mapping.setMethod("editNew");
147
148
149 } else if (isGet(request)) {
150 mapping.setMethod("view");
151
152
153 } else if (isDelete(request)) {
154 mapping.setMethod("remove");
155
156
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
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 }