001: /*
002: * $Id: Restful2ActionMapper.java 540814 2007-05-23 02:46:05Z mrdon $
003: *
004: * Licensed to the Apache Software Foundation (ASF) under one
005: * or more contributor license agreements. See the NOTICE file
006: * distributed with this work for additional information
007: * regarding copyright ownership. The ASF licenses this file
008: * to you under the Apache License, Version 2.0 (the
009: * "License"); you may not use this file except in compliance
010: * with the License. You may obtain a copy of the License at
011: *
012: * http://www.apache.org/licenses/LICENSE-2.0
013: *
014: * Unless required by applicable law or agreed to in writing,
015: * software distributed under the License is distributed on an
016: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017: * KIND, either express or implied. See the License for the
018: * specific language governing permissions and limitations
019: * under the License.
020: */
021: package org.apache.struts2.dispatcher.mapper;
022:
023: import com.opensymphony.xwork2.config.ConfigurationManager;
024: import com.opensymphony.xwork2.inject.Inject;
025:
026: import javax.servlet.http.HttpServletRequest;
027: import java.util.HashMap;
028: import java.util.StringTokenizer;
029: import java.net.URLDecoder;
030:
031: import org.apache.commons.logging.Log;
032: import org.apache.commons.logging.LogFactory;
033: import org.apache.struts2.StrutsConstants;
034:
035: /**
036: * <!-- START SNIPPET: description -->
037: *
038: * Improved restful action mapper that adds several ReST-style improvements to
039: * action mapping, but supports fully-customized URL's via XML. The two primary
040: * ReST enhancements are:
041: * <ul>
042: * <li>If the method is not specified (via '!' or 'method:' prefix), the method is
043: * "guessed" at using ReST-style conventions that examine the URL and the HTTP
044: * method.</li>
045: * <li>Parameters are extracted from the action name, if parameter name/value pairs
046: * are specified using PARAM_NAME/PARAM_VALUE syntax.
047: * </ul>
048: * <p>
049: * These two improvements allow a GET request for 'category/action/movie/Thrillers' to
050: * be mapped to the action name 'movie' with an id of 'Thrillers' with an extra parameter
051: * named 'category' with a value of 'action'. A single action mapping can then handle
052: * all CRUD operations using wildcards, e.g.
053: * </p>
054: * <pre>
055: * <action name="movie/*" className="app.MovieAction">
056: * <param name="id">{0}</param>
057: * ...
058: * </action>
059: * </pre>
060: * <p>
061: * This mapper supports the following parameters:
062: * </p>
063: * <ul>
064: * <li><code>struts.mapper.idParameterName</code> - If set, this value will be the name
065: * of the parameter under which the id is stored. The id will then be removed
066: * from the action name. This allows restful actions to not require wildcards.
067: * </li>
068: * </ul>
069: * <p>
070: * The following URL's will invoke its methods:
071: * </p>
072: * <ul>
073: * <li><code>GET: /movie/ => method="index"</code></li>
074: * <li><code>GET: /movie/Thrillers => method="view", id="Thrillers"</code></li>
075: * <li><code>GET: /movie/Thrillers!edit => method="edit", id="Thrillers"</code></li>
076: * <li><code>GET: /movie/new => method="editNew"</code></li>
077: * <li><code>POST: /movie/ => method="create"</code></li>
078: * <li><code>PUT: /movie/Thrillers => method="update", id="Thrillers"</code></li>
079: * <li><code>DELETE: /movie/Thrillers => method="remove", id="Thrillers"</code></li>
080: * </ul>
081: * <p>
082: * To simulate the HTTP methods PUT and DELETE, since they aren't supported by HTML,
083: * the HTTP parameter "__http_method" will be used.
084: * </p>
085: * <p>
086: * The syntax and design for this feature was inspired by the ReST support in Ruby on Rails.
087: * See <a href="http://ryandaigle.com/articles/2006/08/01/whats-new-in-edge-rails-simply-restful-support-and-how-to-use-it">
088: * http://ryandaigle.com/articles/2006/08/01/whats-new-in-edge-rails-simply-restful-support-and-how-to-use-it
089: * </a>
090: * </p>
091: *
092: * <!-- END SNIPPET: description -->
093: */
094: public class Restful2ActionMapper extends DefaultActionMapper {
095:
096: protected static final Log LOG = LogFactory
097: .getLog(Restful2ActionMapper.class);
098: public static final String HTTP_METHOD_PARAM = "__http_method";
099: private String idParameterName = null;
100:
101: public Restful2ActionMapper() {
102: setSlashesInActionNames("true");
103: }
104:
105: /*
106: * (non-Javadoc)
107: *
108: * @see org.apache.struts2.dispatcher.mapper.ActionMapper#getMapping(javax.servlet.http.HttpServletRequest)
109: */
110: public ActionMapping getMapping(HttpServletRequest request,
111: ConfigurationManager configManager) {
112:
113: if (!isSlashesInActionNames()) {
114: throw new IllegalStateException(
115: "This action mapper requires the setting 'slashesInActionNames' to be set to 'true'");
116: }
117: ActionMapping mapping = super
118: .getMapping(request, configManager);
119:
120: if (mapping == null) {
121: return null;
122: }
123:
124: String actionName = mapping.getName();
125:
126: // Only try something if the action name is specified
127: if (actionName != null && actionName.length() > 0) {
128: int lastSlashPos = actionName.lastIndexOf('/');
129:
130: // If a method hasn't been explicitly named, try to guess using ReST-style patterns
131: if (mapping.getMethod() == null) {
132:
133: if (lastSlashPos == actionName.length() - 1) {
134:
135: // Index e.g. foo/
136: if (isGet(request)) {
137: mapping.setMethod("index");
138:
139: // Creating a new entry on POST e.g. foo/
140: } else if (isPost(request)) {
141: mapping.setMethod("create");
142: }
143:
144: } else if (lastSlashPos > -1) {
145: String id = actionName.substring(lastSlashPos + 1);
146:
147: // Viewing the form to create a new item e.g. foo/new
148: if (isGet(request) && "new".equals(id)) {
149: mapping.setMethod("editNew");
150:
151: // Viewing an item e.g. foo/1
152: } else if (isGet(request)) {
153: mapping.setMethod("view");
154:
155: // Removing an item e.g. foo/1
156: } else if (isDelete(request)) {
157: mapping.setMethod("remove");
158:
159: // Updating an item e.g. foo/1
160: } else if (isPut(request)) {
161: mapping.setMethod("update");
162: }
163:
164: if (idParameterName != null) {
165: if (mapping.getParams() == null) {
166: mapping.setParams(new HashMap());
167: }
168: mapping.getParams().put(idParameterName, id);
169: }
170: }
171:
172: if (idParameterName != null && lastSlashPos > -1) {
173: actionName = actionName.substring(0, lastSlashPos);
174: }
175: }
176:
177: // Try to determine parameters from the url before the action name
178: int actionSlashPos = actionName.lastIndexOf('/',
179: lastSlashPos - 1);
180: if (actionSlashPos > 0 && actionSlashPos < lastSlashPos) {
181: String params = actionName.substring(0, actionSlashPos);
182: HashMap<String, String> parameters = new HashMap<String, String>();
183: try {
184: StringTokenizer st = new StringTokenizer(params,
185: "/");
186: boolean isNameTok = true;
187: String paramName = null;
188: String paramValue;
189:
190: while (st.hasMoreTokens()) {
191: if (isNameTok) {
192: paramName = URLDecoder.decode(st
193: .nextToken(), "UTF-8");
194: isNameTok = false;
195: } else {
196: paramValue = URLDecoder.decode(st
197: .nextToken(), "UTF-8");
198:
199: if ((paramName != null)
200: && (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(e);
215: }
216: mapping.setName(actionName
217: .substring(actionSlashPos + 1));
218: }
219: }
220:
221: return mapping;
222: }
223:
224: protected boolean isGet(HttpServletRequest request) {
225: return "get".equalsIgnoreCase(request.getMethod());
226: }
227:
228: protected boolean isPost(HttpServletRequest request) {
229: return "post".equalsIgnoreCase(request.getMethod());
230: }
231:
232: protected boolean isPut(HttpServletRequest request) {
233: if ("put".equalsIgnoreCase(request.getMethod())) {
234: return true;
235: } else {
236: return isPost(request)
237: && "put".equalsIgnoreCase(request
238: .getParameter(HTTP_METHOD_PARAM));
239: }
240: }
241:
242: protected boolean isDelete(HttpServletRequest request) {
243: if ("delete".equalsIgnoreCase(request.getMethod())) {
244: return true;
245: } else {
246: return isPost(request)
247: && "delete".equalsIgnoreCase(request
248: .getParameter(HTTP_METHOD_PARAM));
249: }
250: }
251:
252: public String getIdParameterName() {
253: return idParameterName;
254: }
255:
256: @Inject(required=false,value=StrutsConstants.STRUTS_ID_PARAMETER_NAME)
257: public void setIdParameterName(String idParameterName) {
258: this.idParameterName = idParameterName;
259: }
260:
261: }
|