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
25 import javax.servlet.http.HttpServletRequest;
26 import java.util.HashMap;
27 import java.util.StringTokenizer;
28 import java.net.URLDecoder;
29
30 import org.apache.commons.logging.Log;
31 import org.apache.commons.logging.LogFactory;
32
33 /***
34 * <!-- START SNIPPET: description -->
35 *
36 * Improved restful action mapper that adds several ReST-style improvements to
37 * action mapping, but supports fully-customized URL's via XML. The two primary
38 * ReST enhancements are:
39 * <ul>
40 * <li>If the method is not specified (via '!' or 'method:' prefix), the method is
41 * "guessed" at using ReST-style conventions that examine the URL and the HTTP
42 * method.</li>
43 * <li>Parameters are extracted from the action name, if parameter name/value pairs
44 * are specified using PARAM_NAME/PARAM_VALUE syntax.
45 * </ul>
46 * <p>
47 * These two improvements allow a GET request for 'category/action/movie/Thrillers' to
48 * be mapped to the action name 'movie' with an id of 'Thrillers' with an extra parameter
49 * named 'category' with a value of 'action'. A single action mapping can then handle
50 * all CRUD operations using wildcards, e.g.
51 * </p>
52 * <pre>
53 * <action name="movie/*" className="app.MovieAction">
54 * <param name="id">{0}</param>
55 * ...
56 * </action>
57 * </pre>
58 * <p>
59 * The following URL's will invoke its methods:
60 * </p>
61 * <ul>
62 * <li><code>GET: /movie => method="index"</code></li>
63 * <li><code>GET: /movie/Thrillers => method="view", id="Thrillers"</code></li>
64 * <li><code>GET: /movie/Thrillers!edit => method="edit", id="Thrillers"</code></li>
65 * <li><code>GET: /movie/new => method="editNew"</code></li>
66 * <li><code>POST: /movie/Thrillers => method="create"</code></li>
67 * <li><code>PUT: /movie/ => method="update"</code></li>
68 * <li><code>DELETE: /movie/Thrillers => method="remove"</code></li>
69 * </ul>
70 * <p>
71 * To simulate the HTTP methods PUT and DELETE, since they aren't supported by HTML,
72 * the HTTP parameter "__http_method" will be used.
73 * </p>
74 * <p>
75 * The syntax and design for this feature was inspired by the ReST support in Ruby on Rails.
76 * See <a href="http://ryandaigle.com/articles/2006/08/01/whats-new-in-edge-rails-simply-restful-support-and-how-to-use-it">
77 * http://ryandaigle.com/articles/2006/08/01/whats-new-in-edge-rails-simply-restful-support-and-how-to-use-it
78 * </a>
79 * </p>
80 *
81 * <!-- END SNIPPET: description -->
82 */
83 public class Restful2ActionMapper extends DefaultActionMapper {
84
85 protected static final Log LOG = LogFactory.getLog(Restful2ActionMapper.class);
86 private static final String HTTP_METHOD_PARAM = "__http_method";
87
88
89
90
91
92
93 public ActionMapping getMapping(HttpServletRequest request, ConfigurationManager configManager) {
94
95 ActionMapping mapping = super.getMapping(request, configManager);
96
97 if (mapping == null) {
98 return null;
99 }
100
101 String actionName = mapping.getName();
102
103
104 if (actionName != null && actionName.length() > 0) {
105 int lastSlashPos = actionName.lastIndexOf('/');
106
107
108 if (mapping.getMethod() == null) {
109
110 if (lastSlashPos == actionName.length() -1) {
111
112
113 if (isGet(request)) {
114 mapping.setMethod("index");
115
116
117 } else if (isPut(request)) {
118 mapping.setMethod("create");
119 }
120
121 } else if (lastSlashPos > -1) {
122 String id = actionName.substring(lastSlashPos+1);
123
124
125 if (isGet(request) && "new".equals(id)) {
126 mapping.setMethod("editNew");
127
128
129 } else if (isGet(request)) {
130 mapping.setMethod("view");
131
132
133 } else if (isPost(request)) {
134 mapping.setMethod("update");
135
136
137 } else if (isDelete(request)) {
138 mapping.setMethod("remove");
139 }
140 }
141 }
142
143
144 int actionSlashPos = actionName.lastIndexOf('/', lastSlashPos - 1);
145 if (actionSlashPos > 0 && actionSlashPos < lastSlashPos) {
146 String params = actionName.substring(0, actionSlashPos);
147 HashMap<String,String> parameters = new HashMap<String,String>();
148 try {
149 StringTokenizer st = new StringTokenizer(params, "/");
150 boolean isNameTok = true;
151 String paramName = null;
152 String paramValue;
153
154 while (st.hasMoreTokens()) {
155 if (isNameTok) {
156 paramName = URLDecoder.decode(st.nextToken(), "UTF-8");
157 isNameTok = false;
158 } else {
159 paramValue = URLDecoder.decode(st.nextToken(), "UTF-8");
160
161 if ((paramName != null) && (paramName.length() > 0)) {
162 parameters.put(paramName, paramValue);
163 }
164
165 isNameTok = true;
166 }
167 }
168 if (parameters.size() > 0) {
169 if (mapping.getParams() == null) {
170 mapping.setParams(new HashMap());
171 }
172 mapping.getParams().putAll(parameters);
173 }
174 } catch (Exception e) {
175 LOG.warn(e);
176 }
177 mapping.setName(actionName.substring(actionSlashPos+1));
178 }
179 }
180
181 return mapping;
182 }
183
184 protected boolean isGet(HttpServletRequest request) {
185 return "get".equalsIgnoreCase(request.getMethod());
186 }
187
188 protected boolean isPost(HttpServletRequest request) {
189 return "post".equalsIgnoreCase(request.getMethod());
190 }
191
192 protected boolean isPut(HttpServletRequest request) {
193 if ("put".equalsIgnoreCase(request.getMethod())) {
194 return true;
195 } else {
196 return isPost(request) && "put".equalsIgnoreCase(request.getParameter(HTTP_METHOD_PARAM));
197 }
198 }
199
200 protected boolean isDelete(HttpServletRequest request) {
201 if ("delete".equalsIgnoreCase(request.getMethod())) {
202 return true;
203 } else {
204 return isPost(request) && "delete".equalsIgnoreCase(request.getParameter(HTTP_METHOD_PARAM));
205 }
206 }
207
208 }