Reputation: 41
I'm in the process of migrating a struts 2.2.3.1 portlet application (jsr168) to the new version, 2.3.3. For the most part everything still works the same, but there was one obvious problem.
I'm using a package.properties resource bundle for things like titles and labels in my jsp's. When I first hit an action, the messages are retrieved from the resource bundle and displayed as expected. When I make a second request to an action though, the message keys are displayed, which typically means the resource was not found.
(The main difference between the first time hitting an action and any subsequent request seems to be that on the first request ONLY the event portlet phase is processed, and not the render portlet phase. Any follow up requests both phases are used.)
It took me quite a while to figure out what is going on, but here is roughly what's happening:
I started out by stepping through org.apache.struts2.components.Text. This class is used when a struts text tag is encountered in a jsp.
The end() method uses the static getText() method from the TextProviderHelper. This is where I found the root of the problem:
for (Object o : stack.getRoot()) {
if (o instanceof TextProvider) {
tp = (TextProvider) o;
msg = tp.getText(key, null, args, stack);
break;
}
}
The above code iterates through the value stack until it finds a TextProvider. Because ActionSupport implements TextProvider, and because struts tries to put the action at or near the top of the stack, the action is usually the provider.
Here's the objects in the value stack at different times for both versions:
So, for some reason, in the new version the action class is not at the top of the stack after the render phase is processed. This means that the TextProviderHelper uses the DefaultTextProvider, which does try to locate the resource bundle... but is never successful in doing so.
I did some investigation into how that DefaultTextProvider gets pushed into the stack:
Jsr168Dispatcher.serviceAction() makes the call to the ActionProxyFactory to create the action proxy. Just before returning the proxy a call is made to prepare(), which in turn makes a call to the DefaultActionInvocation's init method. The init calls createContextMap() which creates the stack
stack = valueStackFactory.createValueStack();
The constructor used:
protected OgnlValueStack(XWorkConverter xworkConverter, CompoundRootAccessor accessor, TextProvider prov, boolean allowStaticAccess) {
setRoot(xworkConverter, accessor, new CompoundRoot(), allowStaticAccess);
push(prov);
}
Aaaaaand that's about as far as I have got on the problem so far. The questions that remain are:
A possible solution (that does fix it) is to explicitly call the action's getText method using ognl, but I don't see this as ideal, and it does mean changing a bunch of jsp files.
<s:property value = "%{getText('some.key')}"/>
A sample project that demonstrates the problem can be found here (It's a maven project):
http://new-value-stack-order.googlecode.com/svn
I'm using tomcat 6.0.25 with pluto 1.1.7 as my development environment.
Upvotes: 2
Views: 649
Reputation: 41
Figured it out. The answer lies in the PortletStateInterceptor
2.2.3.1
@SuppressWarnings("unchecked")
private void restoreStack(ActionInvocation invocation) {
RenderRequest request = (RenderRequest) invocation.getInvocationContext().get(REQUEST);
if (StringUtils.isNotEmpty(request.getParameter(EVENT_ACTION))) {
if(!isProperPrg(invocation)) {
if (LOG.isDebugEnabled()) LOG.debug("Restoring value stack from event phase");
ValueStack oldStack = (ValueStack) invocation.getInvocationContext().getSession().get(
STACK_FROM_EVENT_PHASE);
if (oldStack != null) {
CompoundRoot oldRoot = oldStack.getRoot();
ValueStack currentStack = invocation.getStack();
CompoundRoot root = currentStack.getRoot();
root.addAll(0, oldRoot);
if (LOG.isDebugEnabled()) LOG.debug("Restored stack");
}
}
else {
if (LOG.isDebugEnabled()) LOG.debug("Won't restore stack from event phase since it's a proper PRG request");
}
}
}
2.3.3
@SuppressWarnings("unchecked")
private void restoreStack(ActionInvocation invocation) {
RenderRequest request = (RenderRequest) invocation.getInvocationContext().get(REQUEST);
if (StringUtils.isNotEmpty(request.getParameter(EVENT_ACTION))) {
if(!isProperPrg(invocation)) {
if (LOG.isDebugEnabled()) LOG.debug("Restoring value stack from event phase");
ValueStack oldStack = (ValueStack) invocation.getInvocationContext().getSession().get(
STACK_FROM_EVENT_PHASE);
if (oldStack != null) {
CompoundRoot oldRoot = oldStack.getRoot();
ValueStack currentStack = invocation.getStack();
CompoundRoot root = currentStack.getRoot();
root.addAll(oldRoot);
if (LOG.isDebugEnabled()) LOG.debug("Restored stack");
}
}
else {
if (LOG.isDebugEnabled()) LOG.debug("Won't restore stack from event phase since it's a proper PRG request");
}
}
}
Note the root.addAll(). In the old version it adds the old stack to the beginning of the current stack. In the new version it adds it to the end.
This was actually an old bug fixed in 2.1.3. Creating a new issue in Jira.
Upvotes: 0