Thanks for your explanation @Kusal, we are able to resolve the bytecode incompatibility problem by not overriding execute
from ActionSupport
. Instead, we have changed our custom action to use another entry method.
Hey Atlassian team,
We are also using more than just ServletActionContext
and ActionContext
and the current compat library only aims to provide compatibility for those two.
However, we are still faced with some other challenges:
For example, we use MultipartRequestWrapper
which was in webwork previously but now in struts 2. And we can’t implement struts 2 as a direct maven dependency in our apps because the Atlassian compat library implementation relies on NoClassDefFoundError
exception but having struts 2 compile as a direct dependency of our app means this error won’t be thrown since the class will be resolved.
In summary, we are unable to use the Atlassian compat library if we also want to directly import struts 2 in our apps (which brings about a host of huge changes at our implementation level). Not to mention that we shouldn’t be practicing directly importing and compiling packages already provided by the JRE.
A very broad question but are there any plans to perhaps improve the implementation of the compat library in this regard? Or perhaps some direction on what we could try on our end.
Hi @Zabed
We provide the utility method com.atlassian.xwork.FileUploadUtils#getSingleUploadedFile
for the most common scenario of obtaining a multipart upload from a request. For use cases beyond this you may need to write your own compatibility code or consider forking your plugin codebase.
It is inadvisable to include Struts2 as a compile-scope dependency; provided-scope should be sufficient. Provided is an example of how you might write cross compatible code without using reflection:
public class CrossCompatAction extends ConfluenceActionSupport {
@Override
public String execute() throws Exception {
try {
com.atlassian.core.util.ClassLoaderUtils.loadClass("org.apache.struts2.dispatcher.multipart.MultiPartRequestWrapper", this.getClass());
runStruts();
} catch (ClassNotFoundException e) {
runWebWork();
}
return super.execute();
}
private void runStruts() {
org.apache.struts2.dispatcher.multipart.MultiPartRequestWrapper wrapper; // do whatever
System.out.println("RUNNING STRUTS METHOD");
}
private void runWebWork() {
com.opensymphony.webwork.dispatcher.multipart.MultiPartRequestWrapper wrapper; // do whatever
System.out.println("RUNNING WEBWORK METHOD");
}
}
You will also need to ensure your OSGi package imports are set to optional as follows:
<!-- Required for confluence-compat-lib -->
com.opensymphony.xwork;resolution:=optional,
com.opensymphony.webwork;resolution:=optional,
org.apache.struts2;resolution:=optional,
org.apache.struts2.dispatcher;resolution:=optional,
com.opensymphony.xwork2;resolution:=optional,
<!-- As needed by any compatibility code -->
com.opensymphony.webwork.*;resolution:=optional,
org.opensymphony.xwork*;resolution:=optional,
org.apache.struts2.*;resolution:=optional,
If you find that Spring is unable to instantiate your action, you may need to move your compatibility code into a separate class.
Hey @Kusal
Thanks for your suggestions, they were very helpful and we managed to solve some of our issues using the pattern you described in conjunction with the compat-lib.
We also noticed v1.5.1 of the compat-lib was recently released. Is there somewhere we can track this more easily? We only found out because we were watching [CONFSERVER-80174] Fix for ActionContextCompatManager.setParameters method in compat-lib - Create and track feature requests for Atlassian products. and also through the command mvn versions:display-dependency-updates
.
Yes . Please find the update here for compat-lib
https://confluence.atlassian.com/doc/struts-2-upgrade-1155473773.html#Struts2upgrade-9nov
Hello! We have found a problem with the compat lib:
Under certain situations our app is expecting no context at all and ServletActionContext.getRequest()
was returning null. That was ok and we relied on this for certain operations.
Now, either with StaticHttpContext().getRequest()
or new ServletActionContextCompatManager.getRequest()
we get a different result under some circumstances. The following screenshot shows execution results and the difference returned:
Furthermore, in other situations we found that ServletContextThreadLocal.getRequest()
also returns unexpected objects:
In both cases we are retrieving an object of type BaseLoginFilter$SecurityHttpRequestWrapper
, so as a temporary fix we are ignoring the result obtained if it matches this class. But the question is that the replacement of ServletActionContext is not straightforward because of this differences.
Should we do something different? Is there any use of the compat lib that returns exactly the same as the ServletActionContext was?
Thank you!
Hi @hugo
Thank you for the detailed feedback.
The ServletContextThreadLocal stores the request earlier in its lifecycle and thus will not have the same level of wrapping as the request returned by the ServletActionContext. You will find that the underlying request is still the same.
The StaticHttpContext attempts to retrieve the request from the ServletActionContext if it exists, otherwise falls back to the ServletContextThreadLocal.
The ServletActionContextCompatManager currently exhibits the same behaviour as the StaticHttpContext, but you are correct in that it should be a direct replacement for the ServletActionContext. You may track the fix for this issue here.
We have an action extending ConfluenceActionSupport
that uses FlashScope.get
(to get messages put in the FlashScope by a previous action and then using redirectwithflash
), and this.addActionMessage
(to show the messages in a velocity template using #parse("/template/includes/actionerrors.vm")
). Our app is built against Confluence 7 (because we want to be compatible with Confluence 7.10 and newer versions).
Executing the action fails with a NoClassDefFoundError: com/opensymphony/xwork/ActionSupport
if the FlashScope contains messages, probably because of addActionMessage
that comes from com.opensymphony.xwork.ActionSupport.
Usage of either ConfluenceActionSupport#addActionMessage(String, Object...)
or ConfluenceActionSupport#addActionMessage(String)
should not cause a bytecode incompatibility. There is potentially other code in your app which references com.opensymphony.xwork.ActionSupport
directly.
Well, “should not cause” does not really help. We don’t have any reference to com.opensymphony.xwork.ActionSupport
in our code, only to ConfluenceActionSupport
.
However, we found a workaround for this problem by not using addActionMessage
, but rendering the message ourselves.
@cheinig I have not tested this but …
addActionMessage(String)
is defined in ActionSupport
from Xwork, so when calling this method your class file has a reference on Xwork.
You could try calling the other alternative that is defined in ConfluenceActionSupport
instead:
addActionMessage(String textKey, Object... args)
This should work: addActionMessage("my.i18n.key", new Object[] {});
When calling it like this your class file should only have a reference on ConfluenceActionSupport
.
Hi @Kusal,
Thanks for the update. I exactly facing the same problem when I’m extending AbstractActionBreadcrumb
and AbstractSpaceActionBreadcrumb
which load xwork
in its bytecode, and I got an incompatible error when loading my plugin in Confluence 8. I would like to follow up on more details on this, is there a fix for this issue or is there any replacement/compatible class that can produce similar behavior?
My code:
import com.opensymphony.xwork.Action;
public class AdminActionBreadcrumb extends AbstractActionBreadcrumb {
public AdminActionBreadcrumb(Action action) {
super(action);
}
@Override
protected List<Breadcrumb> getMyCrumbs() {
List<Breadcrumb> crumbs = new ArrayList<Breadcrumb>();
crumbs.add(AdminBreadcrumb.getInstance());
crumbs.add(new SimpleBreadcrumb("com.example.themeadmin.breadcrumb", null));
crumbs.add(this);
return crumbs;
}
....
}
Hi @Leon, it seems we’ve overlooked extending the necessary compatibility code to the abstract breadcrumb classes. Could you please try extending one of the corresponding concrete classes instead (eg. AdminActionBreadcrumb
and SpaceAdminActionBreadcrumb
)?
Hi @Kusal,
I’ve tried using AdminActionBreadcrumb
, but I’m still getting java.lang.ClassNotFoundException
when running my plugin on Confluence 8. I bet extending AdminActionBreadcrumb
wouldn’t do much different than extending AbstractActionBreadcrumb
seems they both import com.opensymphony.xwork2.Action
in Confluence 8?
@cheinig @Leon
I have been unable to reproduce the issues described. I’ve uploaded some sample plugin source I’ve quickly put together using the atlas-create-confluence-plugin
command. When compiled against Confluence 7.10 and installed in Confluence 8.0.0-rc1, it demonstrates Actions and Breadcrumbs working as expected.
Hi @Kusal,
Thanks for the example. I’ve extended AdminActionBreadcrumb
and SpaceAdminActionBreadcrumb
, compiled it, and installed in Confluence 8.0 rc1. i no longer have issue with com.opensymphony.xwork.Action
but I got different error which is java.lang.NoSuchFieldError: action
when executing getTitle
. I believe this somehow related to AdminActionBreadcrumb
or SpaceAdminActionBreadcrumb
, the get title method was inherited from AbstractBreadcrumb
which is the superclass. Is this normal? Or we use getTitle
in a wrong way?
My Code:
public class AdminActionBreadcrumb extends AdminActionBreadcrumb {
public AdminActionBreadcrumb(Action action) {
super(action);
}
@Override
protected List<Breadcrumb> getMyCrumbs() {
List<Breadcrumb> crumbs = new ArrayList<Breadcrumb>();
crumbs.add(AdminBreadcrumb.getInstance());
crumbs.add(new SimpleBreadcrumb("com.example.admin.breadcrumb", null));
crumbs.add(this);
return crumbs;
}
@Override
public String getTitle() {
return action instanceof ConfluenceActionSupport ? ((ConfluenceActionSupport) action).getActionName() : super.getTitle();
}
@Override
public Breadcrumb getParent() {
return AdminBreadcrumb.getInstance();
}
}
@Leon Whilst we have provided compatibility for concrete constructors, the protected field ‘action’ is of XWork2 type in Confluence 8.0 and can not be accessed when your plugin is compiled against 7.x. You could use a workaround such as follows:
public TestBreadcrumb(Action action) {
super(action);
if (action instanceof ConfluenceActionSupport) {
this.title = ((ConfluenceActionSupport) action).getActionName();
}
}
Alternatively, extend the AbstractBreadcrumb class directly and implement your own field:
public class TestBreadcrumb extends AbstractBreadcrumb {
protected Action action;
public TestBreadcrumb(Action action) {
this.action = action;
}
}
We will consider extending compatibility to abstract constructors and the protected ‘action’ field in 8.0.1.
Hi!
Related to our previous post on Nov 17th, we found out another possible problem:
ActionContext.getContext().getParameters()
which we thought should be replaced with:
actionContextCompatManager.getParameters()
is not returning the expected parameters, but the following, using ServletActionContextCompatManager, is.
servletActionContextCompatManager.getRequest().getParameterMap()
Is this ok? Are we taking the right “parameters” from ActionContextCompatManager ? Should we use ServletActionContextCompatManager instead?
Thanks!
Hi
We are having issue with Confluence 8.0 compatibility for our app. We are looking for replacement to com.opensymphony.xwork.util.XWorkList
class. We tried to replace it with com.opensymphony.xwork2.conversion.impl.XWorkList
but it is throwing null pointer exception in 7.x version though working fine in 8.0 . Has any one faced or fixed similar issue ? Thanks
Hi,
With the latest confluence-compat-lib library version (1.5.3) we are seeing different behaviours with what we had previously with ActionContext class of xwork package (tested in Confluence 7.13.11).
If we are not wrong, getContext() method of ActionContext (ActionContext.getContext()) invokes a get on the thread local instance:
ActionContext context = (ActionContext)actionContext.get();
where actionContext is new ActionContextThreadLocal(), while ActionContextCompatManager does not. This means that if we call the setName() method to set a name, this is shared across different requests. We did not see this behaviour when we used the setName() method of ActionContext (ActionContext.getContext().setName()).
Use case:
- We set in an action request the action’s name using ActionContextCompatManager, using actionContextCompatManager.setName(xxx);
- We perform a different request and the previously stored name is still there when we try to retrieve the name. With ActionContext the result was null, as you would expect as this is a different action
Is this correct and expected?
Thanks,
Gorka.