1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 * <action name="movie/*" className="app.MovieAction">
58 * <param name="id">{0}</param>
59 * ...
60 * </action>
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
109
110
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
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
136 if (mapping.getMethod() == null) {
137
138 if (lastSlashPos == actionName.length() -1) {
139
140
141 if (isGet(request)) {
142 mapping.setMethod("index");
143
144
145 } else if (isPost(request)) {
146 mapping.setMethod("create");
147 }
148
149 } else if (lastSlashPos > -1) {
150
151 if (isGet(request) && "new".equals(id)) {
152 mapping.setMethod("editNew");
153
154
155 } else if (isGet(request)) {
156 mapping.setMethod("view");
157
158
159 } else if (isDelete(request)) {
160 mapping.setMethod("remove");
161
162
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
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 }