Struts 2.1 doesn't directly support hierarchical actions. Nonetheless I still was able to implement them at
visualmandarin.
Notice how the podcasts and exercises urls are all organized into folders and even the lesson title is present in the url:
Lesson - Can I have a menu please?If you are interested in SEO optimized urls for your website this kind of feature is a MUST. But how to do it struts 2.1? In short the steps you need to follow are:
Step 1If you are running on struts 2, migrate to struts 2.1
Step 2Make sure you are using the convention plugin
Step 3Ensure that all your actions have an explicit namespace. If you don't you might find that those actions are being called when they shouldn't.
Step 4Create a wildcard action in the namespace that you want to be hierarchical. In my case I have created one in namespace /lessons:
<action name="*" namespace="/lessons" method="execute" class="lessonsLocator">
<result name="lister"></result>
<result name="ResourceNotFoundException" type="chain">404</result>
</action>
Your lessons locator action should inspect the uri to determine which resource to display. In my case I was dealing with lessons but you could be dealing with categories and products or anything else. It shouldn't make any difference.
Step 5Extend org.struts2.impl.StrutsActionProxy and override the following prepare() method:
@Override
protected void prepare() {
String profileKey = "create DefaultActionProxy: ";
try {
UtilTimerStack.push(profileKey);
// we backtrack the namespace until we find an action that matches
this.findActionByBacktracking();
if (config == null && unknownHandler != null) {
config = unknownHandler.handleUnknownAction(namespace, actionName);
}
if (config == null) {
String message;
if ((namespace != null) && (namespace.trim().length() > 0)) {
message = LocalizedTextUtil.findDefaultText(XWorkMessages.MISSING_PACKAGE_ACTION_EXCEPTION, Locale.getDefault(), new String[]{
namespace, actionName
});
} else {
message = LocalizedTextUtil.findDefaultText(XWorkMessages.MISSING_ACTION_EXCEPTION, Locale.getDefault(), new String[]{
actionName
});
}
throw new ConfigurationException(message);
}
resolveMethod();
if (!config.isAllowedMethod(method)) {
throw new ConfigurationException("Invalid method: "+method+" for action "+actionName);
}
invocation.init(this);
} finally {
UtilTimerStack.pop(profileKey);
}
}
/**
* Returns a list of all possible namespaces. The one with biggest length
* is top of the list up to /xxx... bottom of a list. Note that we don't backtrack until
* the empty namespace
*
* @param namespace
* @return
*/
private String [] getAllPossibleNamespaces(String namespace) {
List<String> namespaces = new ArrayList<String>();
// e.g. /lessons/hsk/.../1 ->
String [] tokens = namespace.split("/");
tokens=removeEmptyStrings(tokens);
if (tokens != null && tokens.length >= 0) {
for (int i=0; i< tokens.length; i ++) {
String gnamespace="";
for (int j=0;j<tokens.length- i;j++) {
gnamespace += "/" + tokens[j] ;
}
namespaces.add(gnamespace);
}
}
return namespaces.toArray(new String[0]);
}
private String [] removeEmptyStrings(String [] tokens) {
List<String> result = new ArrayList();
for (String token: tokens) {
if (token == null || token.equals("")) {
}
else {
result.add(token);
}
}
return result.toArray(new String[0]);
}
/**
* Searches for the given action in one or more namespaces
*
*/
private void findActionByBacktracking() {
if (namespace != null && !namespace.equals("") && !namespace.equals("/")) {
String [] namespaces =this.getAllPossibleNamespaces(namespace);
for (String namespace: namespaces) {
config = configuration.getRuntimeConfiguration().getActionConfig(namespace, actionName);
if (config != null ) break;
}
}
else {
config = configuration.getRuntimeConfiguration().getActionConfig(namespace, actionName);
}
}
Step 6Extend org.apache.struts2.impl.StrutsActionProxyFactory.
My implementation as an example:
public class HierarchicalActionProxyFactory extends DefaultActionProxyFactory {
@Override
public ActionProxy createActionProxy(ActionInvocation inv, String namespace, String actionName, String methodName, boolean executeResult, boolean cleanupContext) {
HierarchicalActionProxy proxy = new HierarchicalActionProxy(inv, namespace, actionName, methodName, executeResult, cleanupContext);
container.inject(proxy);
proxy.prepare();
return proxy;
}
}
Step 7Finally the last step: override the
property struts.actionProxyFactoryin struts.properties and point to your implementation of the proxy factory.
My implementation was:
struts.actionProxyFactory=com.visualmandarin.web.config.HierarchicalActionProxyFactory
If you have followed these steps correctly you should be able to have the same kind of urls that I have in visualmandarin.com website. If there's something that you don't understand don't hesitate to ask!
You can find a working example
here. This war file was tested in jboss 4.2.3 but should work in any compliant application server.
What you need to get this example working:
- Add the struts 2 library files to the lib directory
- Add the convention plugin jar file to the lib directory
- Specify the correct path to your application server
Once you build the war file and deploy it, you can access the example at http://localhost/example3 or http://localhost:8080/example3