View Javadoc

    1   /*
    2    * $Id: Restful2ActionMapper.java 496932 2007-01-17 04:12:46Z mrdon $
    3    *
    4    * Licensed to the Apache Software Foundation (ASF) under one
    5    * or more contributor license agreements.  See the NOTICE file
    6    * distributed with this work for additional information
    7    * regarding copyright ownership.  The ASF licenses this file
    8    * to you under the Apache License, Version 2.0 (the
    9    * "License"); you may not use this file except in compliance
    10   * with the License.  You may obtain a copy of the License at
    11   *
    12   *  http://www.apache.org/licenses/LICENSE-2.0
    13   *
    14   * Unless required by applicable law or agreed to in writing,
    15   * software distributed under the License is distributed on an
    16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    17   * KIND, either express or implied.  See the License for the
    18   * specific language governing permissions and limitations
    19   * under the License.
    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   *   &lt;action name="movie/*" className="app.MovieAction"&gt;
    54   *     &lt;param name="id"&gt;{0}&lt;/param&gt;
    55   *     ...
    56   *   &lt;/action&gt;
    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      * (non-Javadoc)
    90      *
    91      * @see org.apache.struts2.dispatcher.mapper.ActionMapper#getMapping(javax.servlet.http.HttpServletRequest)
    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         // Only try something if the action name is specified
    104         if (actionName != null && actionName.length() > 0) {
    105             int lastSlashPos = actionName.lastIndexOf('/');
    106
    107             // If a method hasn't been explicitly named, try to guess using ReST-style patterns
    108             if (mapping.getMethod() == null) {
    109
    110                 if (lastSlashPos == actionName.length() -1) {
    111
    112                     // Index e.g. foo/
    113                     if (isGet(request)) {
    114                         mapping.setMethod("index");
    115
    116                     // Creating a new entry on POST e.g. foo/
    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                     // Viewing the form to create a new item e.g. foo/new
    125                     if (isGet(request) && "new".equals(id)) {
    126                         mapping.setMethod("editNew");
    127
    128                     // Viewing an item e.g. foo/1
    129                     } else if (isGet(request)) {
    130                         mapping.setMethod("view");
    131
    132                     // Updating an item e.g. foo/1
    133                     } else if (isPost(request)) {
    134                         mapping.setMethod("update");
    135
    136                     // Removing an item e.g. foo/1
    137                     } else if (isDelete(request)) {
    138                         mapping.setMethod("remove");
    139                     }
    140                 }
    141             }
    142
    143             // Try to determine parameters from the url before the action name
    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 }