I recently had to develop a way to provide for non-parameterized URLs in a WebWork (2.1.x) application. Non-parameterized URLs are those that don’t have the endless list of ?key1=value1&key2=value2 parameters attached to the URL and instead have the nice tidy Rails style URL like ../key1/value1/key2/value2.
Besides being much prettier, this style of URL also helps with search engines as some don’t like to index pages with too many parameters specified (or with an “&id” parameter). When the parameters appear as part of the URL, everybody is happy.
WebWork v2.1.x doesn’t exactly have a nice plugin point to introduce this behavior, so the actual dispatcher servlet needs to be modified to parse URLs of that format. Just so happens there is a very undocumented version of the dispatcher servlet called com.opensymphony.webwork.dispatcher.CoolUriServletDispatcher that attempts to provide just such behavior (thanks, Jim). However, it puked up some String.substring() index out of bounds exceptions when I gave it a spin. I have a feeling its parsing of the URL was very dependent on the request mapping configuration in the web.xml (which is a hairy task). So, what else was I to do but create my own?
My version is pretty similar to the CoolUriServletDispatcher that’s included with WebWork except that it has some enhancements:
- doesn’t throw a String.subtring index out of bounds exception on my deployment ;)
- can handle conventional actionName.action (or any other extension) url formats
- 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
- can simultaneously handle both pretty url parameters (/key1/value1…) and conventional parameters (/key1/value1?key2=value2)
- Conventional requests will be handled as always:
http://HOST/CONTEXT/ACTION_NAME.action?id=1&key1=value1
will be handled by the ACTION_NAME action with the given parameters - Pretty urls in this format (when using both conventional and pretty url
formats):
http://HOST/CONTEXT/link/ACTION_NAME/id/1/key1/value1
will be handled by the ACTION_NAME action with parameters (id=1, key1=value1) - Pretty urls in this format (when using both conventional and pretty url
formats):
http://HOST/CONTEXT/link/ACTION_NAME/id/1?key1=value1
will be handled by the ACTION_NAME action with parameters (id=1, key1=value1) - Pretty urls in this format (when using both conventional and pretty url
formats):
http://HOST/CONTEXT/link/ACTION_NAME/1?key1=value1
will be handled by the ACTION_NAME action with parameters (id=1, key1=value1) - When only the pretty url format is used and all requests are mapped to
this dispatcher servlet, the /link portion can be removed and you
will see the same functionality:
http://HOST/CONTEXT/ACTION_NAME/id/1/key1/value1
will be handled by the ACTION_NAME action with parameters (id=1, key1=value1)
Same goes for: http://HOST/CONTEXT/ACTION_NAME/1/key1/value1
I’ve attempted to make the javadocs quite complete, so I’ll point you there for further documentation: UriMappingServletDispatcher javadocs.
And here is the actual servlet file (actual source is attached at the end of this entry as well):
UriMappingServletDispatcher.java (for Java 5)
UriMappingServletDispatcher.java (for Java 1.4 and below)
It should be noted that the recently released WebWork v2.2 does have an ActionMapper plugin point and a RestfulActionMapper implementation that does this very thing, so go use that if you can.
Actual source:
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;
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
// com.opensymphony.webwork.dispatcher.CoolUriServletDispatcher
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, ””);
}
}
