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
3 comments:
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
I am glad you found it useful. Thanks for your feedback
Many thanks, this is just what I was looking for the last 3 hours :s
Post a Comment