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