Saturday, June 13, 2009

Creating hierarchical actions in struts 2.1

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 1

If you are running on struts 2, migrate to struts 2.1

Step 2

Make sure you are using the convention plugin

Step 3

Ensure 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 4

Create 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 5

Extend 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 6

Extend 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 7

Finally the last step: override the property struts.actionProxyFactory
in 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

3 comments:

Anonymous said...

Hello Armindo,

You have touched a very practical and useful subject. Thank you so much for sharing your pioneering work. It is mind-blowing for me.

Qunhuan Mei
qm@qm18.wanadoo.co.uk

Armindo Cachada said...

I am glad you found it useful. Thanks for your feedback

Anonymous said...

Many thanks, this is just what I was looking for the last 3 hours :s

 
Software