import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.security.AccessController;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import sun.security.action.GetPropertyAction;

import com.opensymphony.webwork.dispatcher.ServletDispatcher;

/**
 * This is a custom servlet dispatcher whose functionality is loosely based off
 * of <code>com.opensymphony.webwork.dispatcher.CoolUriServletDispatcher</code>.
 * 
 * The purpose of this dispatcher is to allow for urls where the http parameters
 * are not passed in as ?key=value&key=value pairs but as /key/value/key/value
 * pairs. Some call these pretty or cool urls.
 * 
 * This servlet differs from the webwork included CoolUriServletDispatcher in
 * that it:
 * <ol>
 * <li>doesn't throw a String.subtring index out of bounds exception on my
 * deployment ;)</li>
 * <li>can handle conventional actionName.action (or any other extension) url
 * formats</li>
 * <li>assumes a format of actionName/value to set the id property on the
 * action and NOT to set the id property on the actionName property of the
 * action</li>
 * <li>can simultaneously handle both pretty url parameters (/key1/value1...)
 * and conventional parameters (/key1/value1?key2=value2)
 * </ol>
 * 
 * Usage:
 * 
 * <ol>
 * <li>Determine the url path you want to use for urls that will utilize the
 * pretty format. I find that using: context<b>/link</b>/actionName works
 * well. In this case "/link" is extra url path that will signify a pretty url.
 * If you will always be using a pretty url format and never the standard
 * context/actionName.action format, this step is unecessary. This is only
 * useful when both formats should be supported and helps in the trickiness that
 * is the servlet and web.xml request mapping. </li>
 * <li>Set this servlet to be your webwork dispatcher servlet in web.xml and
 * add an init-param signifying the url portion that you've added to your pretty
 * urls (if any - again, if you are only using pretty formats, this is
 * unecessary): <p/>
 * 
 * <pre>
 *  &lt;servlet&gt;
 *    &lt;servlet-name&gt;webwork&lt;/servlet-name&gt;
 *    &lt;servlet-class&gt;your.package.UriMappingServletDispatcher&lt;/servlet-class&gt;
 *    &lt;load-on-startup&gt;1&lt;/load-on-startup&gt;
 *    &lt;init-param&gt;
 *       &lt;param-name&gt;ignoreURIPortion&lt;/param-name&gt;
 *       &lt;param-value&gt;&lt;b&gt;/link&lt;/b&gt;&lt;/param-value&gt;
 *    &lt;/init-param&gt;
 *  &lt;/servlet&gt;
 * </pre>
 * 
 * </li>
 * <li>Set up your request mappings. If you are using both conventional and
 * pretty url formats then your mappings in web.xml will look something like
 * this: <p/>
 * 
 * <pre>
 *  &lt;servlet-mapping&gt;
 *    &lt;servlet-name&gt;webwork&lt;/servlet-name&gt;
 *    &lt;url-pattern&gt;*.action&lt;/url-pattern&gt;
 *  &lt;/servlet-mapping&gt;
 *  &lt;servlet-mapping&gt;
 *    &lt;servlet-name&gt;webwork&lt;/servlet-name&gt;
 *    &lt;url-pattern&gt;&lt;b&gt;/link/*&lt;/b&gt;&lt;/url-pattern&gt;
 *  &lt;/servlet-mapping&gt;
 * </pre>
 * 
 * <p/> If you are only using pretty url formats then your mappings in web.xml
 * will look something like more like this (or whatever will map all requests to
 * the servlet) <p/>
 * 
 * <pre>
 *  &lt;servlet-mapping&gt;
 *    &lt;servlet-name&gt;webwork&lt;/servlet-name&gt;
 *    &lt;url-pattern&gt;/*&lt;/url-pattern&gt;
 *  &lt;/servlet-mapping&gt;
 * </pre>
 * 
 * </li>
 * </ol>
 * <p>
 * Once your web.xml is set up, you should be good to go. Now for the
 * functionality you can expect to see:
 * <ul>
 * <li>Conventional requests will be handled as always:<br/>
 * <tt>http://HOST/CONTEXT/ACTION_NAME.action?id=1&key1=value1</tt><br/> will be handled
 * by the ACTION_NAME action with the given parameters</li>
 * <li>Pretty urls in this format (when using both conventional and pretty url
 * formats):<br/> <tt>http://HOST/CONTEXT<b>/link</b>/ACTION_NAME/id/1/key1/value1</tt><br/>
 * will be handled by the ACTION_NAME action with parameters (id=1, key1=value1)</li>
 * <li>Pretty urls in this format (when using both conventional and pretty url
 * formats):<br/> <tt>http://HOST/CONTEXT<b>/link</b>/ACTION_NAME/id/1?key1=value1</tt><br/>
 * will be handled by the ACTION_NAME action with parameters (id=1, key1=value1)</li>
 * <li>Pretty urls in this format (when using both conventional and pretty url
 * formats):<br/> <tt>http://HOST/CONTEXT<b>/link</b>/ACTION_NAME/1?key1=value1</tt><br/>
 * will be handled by the ACTION_NAME action with parameters (id=1, key1=value1)</li>
 * <li>When only the pretty url format is used and all requests are mapped to
 * this dispatcher servlet, the <b>/link</b> portion can be removed and you
 * will see the same functionality:<br/>
 * <tt>http://HOST/CONTEXT/ACTION_NAME/id/1/key1/value1</tt><br/> will be handled by the
 * ACTION_NAME action with parameters (id=1, key1=value1)<br/> Same goes for:
 * <tt>http://HOST/CONTEXT/ACTION_NAME/1/key1/value1</tt></li>
 * </ul>
 * 
 * @author Ryan Daigle
 */
public class UriMappingServletDispatcher extends ServletDispatcher {

    private static final long serialVersionUID = 1L;

    private static final String DEFAULT_ENCODING = (String) AccessController
            .doPrivileged(new GetPropertyAction("file.encoding"));

    public static final String URI_IGNORE_CONFIG_KEY = "ignoreURIPortion";

    private String ignoreURIPortion = "";

    /*
     * (non-Javadoc)
     * 
     * @see javax.servlet.Servlet#init(javax.servlet.ServletConfig)
     */
    public void init(ServletConfig servletConfig) throws ServletException {
        super.init(servletConfig);

        // Get if we need to ignore any portion of the URL
        String ignore = servletConfig.getInitParameter(URI_IGNORE_CONFIG_KEY);
        if (ignore != null) {
            ignoreURIPortion = ignore;
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see javax.servlet.http.HttpServlet#service(javax.servlet.http.HttpServletRequest,
     *      javax.servlet.http.HttpServletResponse)
     */
    public void service(HttpServletRequest request, HttpServletResponse response)
            throws ServletException {

        // Determine action name
        String actionName = getActionName(getAdjustedString(request
                .getServletPath()));
        if (actionName != null && !"".equals(actionName)) {
            serviceVanillaRequest(request, response, actionName);
        } else {
            actionName = getUriMappedActionName(request);
            serviceUriMappedRequest(request, response, actionName);
        }
    }

    /**
     * This request has been identified as not being a vanilla request, so
     * handle it as though it's a URI mapped request.
     * 
     * @param request
     * @param response
     * @param actionName
     */
    protected void serviceUriMappedRequest(HttpServletRequest request,
            HttpServletResponse response, String actionName) {

        // Place to store our parsed parameters
        Map parameters = new HashMap();

        // Get the part of the URL from the actionName onwards
        String requestURI = getAdjustedString(request.getRequestURI());
        String paramPortion = requestURI.substring(requestURI
                .indexOf(actionName), requestURI.length());
        StringTokenizer st = new StringTokenizer(paramPortion, "/");

        // Collect the parameters
        List paramsList = new ArrayList(st.countTokens());
        while (st.hasMoreTokens()) {
            try {
                paramsList.add(URLDecoder.decode(st.nextToken(),
                        DEFAULT_ENCODING));
            } catch (UnsupportedEncodingException e) {
                paramsList.add(st.nextToken());
            }
        }

        // Which tokens the params start on. Default is the the second,
        // since the first token is the action
        int tokenStart = 1;

        // If there are an even number of params then we can assume that
        // the first token is the name of the action and the second
        // is the id property to set on that action.
        if (paramsList.size() % 2 == 0) {
            parameters.put("id", paramsList.get(1));
            tokenStart = 2;
        }

        // Parse the rest of the parameters as key/value/key/value...
        for (int i = tokenStart; i < paramsList.size(); i = i + 2) {
            parameters.put(paramsList.get(i), paramsList.get(i + 1));
        }

        // Add in any 'normal' request parameters that are present (which
        // will override the url mapped ones)
        parameters.putAll(request.getParameterMap());

        // Pass of our parameters and action on to the default processing
        // This part is wholly copied from
        // <code>com.opensymphony.webwork.dispatcher.CoolUriServletDispatcher</code>
        try {
            request = wrapRequest(request);
            serviceAction(request, response, "", actionName,
                    getRequestMap(request), parameters, getSessionMap(request),
                    getApplicationMap());
        } catch (IOException e) {
            String message = "Could not wrap servlet request with MultipartRequestWrapper!";
            sendError(request, response,
                    HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
                    new ServletException(message, e));
        }

    }

    /**
     * This request has been identified as being a vanilla request:
     * actionName.action?key=value&key=value...., handle it appropriately
     * 
     * @param request
     * @param response
     * @param actionName
     */
    protected void serviceVanillaRequest(HttpServletRequest request,
            HttpServletResponse response, String actionName) {
        try {
            serviceAction(wrapRequest(request), response,
                    getNameSpace(request), actionName, getRequestMap(request),
                    getParameterMap(request), getSessionMap(request),
                    getApplicationMap());
        } catch (IOException e) {
            String message = "Could not wrap servlet request with MultipartRequestWrapper!";
            sendError(request, response,
                    HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
                    new ServletException(message, e));
        }
    }

    /**
     * The action could not be determined when the vanilla action.xxx format was
     * used - revert to other more manual means to determine the action name.
     * 
     * @param request
     * @return
     */
    protected String getUriMappedActionName(HttpServletRequest request) {

        // Safest way seems to be to take the request URI, and strip off the
        // context path from the beginning, then strip off everything after the
        // first slash
        String uri = request.getRequestURI();
        String contextPath = request.getContextPath();
        String servletPath = request.getServletPath();

        // Strip off the context and servlet paths to get at our action string
        String baseUri = uri.substring(contextPath.length(), uri.length());
        baseUri = baseUri.substring(servletPath.length(), baseUri.length());

        // Strip off trailing args past the action name (and not
        // including leftover leading "/")
        int trailingSlash = baseUri.indexOf("/", 1);
        baseUri = baseUri.substring(1, (trailingSlash > 0 ? trailingSlash
                : baseUri.length()));
        return baseUri;
    }

    /**
     * Get the portion of the request URI that should be analyzed for action
     * names, parameters etc... This is basically the URI minus the text
     * specified in the "ignoreURIPortion" servlet init param. (only takes out
     * the first occurance of the string - override for other functionality)
     * 
     * @param request
     * @return
     */
    protected String getAdjustedString(String adjustable) {
        return adjustable.replace(ignoreURIPortion, "");
    }
}
