PSA - Confluence moves to Struts in 8.0 - EAP

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.

3 Likes

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

1 Like

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!

1 Like

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.

1 Like

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.

1 Like

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!