PSA - Confluence moves to Struts in 8.0 - EAP

Hi @ggautam,

I could remove com.opensymphony.webwork and com.opensymphony.xwork imports from a shared library used in most of our apps. I did this by using com.atlassian.confluence.web.context.StaticHttpContext instead of com.opensymphony.webwork.ServletActionContext to retrieve the current request.

The library previously also used com.opensymphony.xwork.config.entities.ActionConfig to access action <param> values as defined in atlassian-plugin.xml.
I realized that xwork/struts actually provide an interceptor for this purpose, which is also configured, but not contained in any of the default interceptor stacks in Confluence’s xwork.xml:

<interceptor name="static-params" class="com.opensymphony.xwork.interceptor.StaticParametersInterceptor"/>

I’m planning to use the interceptor as follows in my actions:

	<xwork key="test-actions" name="Test Actions">
		<package name="test-actions" extends="default" namespace="/pages/test">
			<action name="test" class="test.TestAction" method="execute">
				<interceptor-ref name="defaultStack"/>
				<interceptor-ref name="static-params"/>
				<param name="SomeTestParam">this is injected into the action</param>
				<result name="success" type="velocity">/test-action.vm</result>
			</action>
		</package>
	</xwork>

And inside the action:

public class TestAction extends ConfluenceActionSupport {
    private String someTestParam;

    public String getSomeTestParam() {
        return someTestParam;
    }

    public void setSomeTestParam(String someTestParam) {
        this.someTestParam = someTestParam;
    }

    public String execute() {
        return SUCCESS;
    }
}

Could you confirm if both StaticHttpContext and this interceptor will continue to work in Confluence 8?
I think it would be important to keep the key of the interceptor as it currently is - which seems to be different from the xwork / struts defaults (which register it at staticParams; Confluence: static-params).
If Confluence 8 would register the new interceptor class from the xwork2 package with the same key static-params our apps would work with both Confluence 7 and 8.

That would help me a lot :slight_smile:

Thanks,
Jens

Hi @jens,
Thanks for reaching out.
Yes, StaticHttpContext will continue to work as is, it uses com.atlassian.core.filters.ServletContextThreadLocal internally and you can use that too if you need. We have mentioned something similar here in last week’s update.
The feedback for keeping the interceptors name same does make sense, because in some point in future, we would want our default package in struts.xml to rely on struts-default’s package which has different names. We would keep the old & new both names for compatibility so that your app could continue to work.
Since there is no way to deprecate the usage of XWork names, we would just put a comment for deprecation of old name like “static-params” and look at removing it in next major release.

Thanks,
Ganesh

1 Like

Thanks for confirming :slight_smile:

If you want to deprecate the non-standard names I think it would be a good idea to do that with Confluence 8.0.0 and introduce the replacements at the same time. This should be trivial because it’d only require duplicating the <interceptor> elements; but it would give us vendors the possibility to switch over to the new names when we drop support for Confluence 7, meaning we could support both 8.x and 9.x at the same time.

Cheers,
Jens

1 Like

@jens
Yes, duplication is the only way out for the moment. You can expect that the same, we had a discussion on it and agreed on the same earlier.
Which means you can expect both:

<interceptor name="static-params" class="com.opensymphony.xwork2.interceptor.StaticParametersInterceptor"/>
<interceptor name="staticParams" class="com.opensymphony.xwork2.interceptor.StaticParametersInterceptor"/>

in Confluence 8.x

1 Like

Hi everyone,

The Confluence DC team merged Struts2 work to master and release a regular EAP 8.0.0-m69 early this week. This means we will now have only one EAP release at any moment for 8.0. Please read Oct 11 update for more details.

We thank you for all the feedback which helped in delivering this massive framework uplift. Please continue to reach out to us in case of any broken behaviour caused by Struts2.

Regards,
The Confluence DC Team

1 Like

Hi @ggautam,

We saw some unexpected changes in -m69, compared to the previous Struts release, with some previously-accessible classes no longer being exposed via OSGi.

For example, the following class was previously accessible to plugins in struts-m48 (and in all <8.0 Confluence releases):

com.atlassian.confluence.plugin.descriptor.PluginAwareActionConfig

It has now, however, been moved to the following location:

com.atlassian.confluence.impl.struts.PluginAwareActionConfig

None of the classes in impl.* seem to be exported (on purpose, I imagine). I understand the desire not to expose additional Confluence internals, but this is a class that had already been available for a long time, and it breaks a de-facto API that we had been using for the last 10 years. Can at least this class be moved somewhere where it is accessible to plugins?

Thanks!
Scott

Hi @ggautam ,

We’ve been using the confluence-compat-lib (v1.5.0) to address the discrepancies between webwork/Struts 2 and maintain compatibility for both Confluence 7 and 8.

We have been testing this on Confluence 8.0.0-m76.

However, we’ve noticed that when using ActionContextCompatManager, it fails at the method:

private ActionContextCompat initialiseActionContextCompat(ClassLoader classLoader)

because it attempts to initialise the setParameters method with an argument of type Map when in Struts 2, it’s actually of type HttpParameters. Refer to the 2 snippets below:

Initialisation code:

ActionContextStruts2AndWWCompat(String actionContextClass, ClassLoader classLoader) throws ReflectiveOperationException {
...
    this.setParameters = this.getACStruts2Method("setParameters", classLoader, Map.class);
...
}

Struts 2 ActionContext setParameters signature:

public void setParameters(HttpParameters parameters) {
    this.put("com.opensymphony.xwork2.ActionContext.parameters", parameters);
}

This causes the JVM to throw a NoSuchMethodException in runtime when it attempts the initialization of the ActionContextCompat.

Please help us clarify whether this is some intentional change we’ve missed the context of or whether it was an accidental oversight and if so, whether it would be addressed soon. We believe this is currently blocking us from using ActionContextCompatManager.

Hi @Zabed,

We recently received this feedback on the original compat issue and are working on getting setParameters compat-layer and you can track it under following issue: [CONFSERVER-80174] Fix for ActionContextCompatManager.setParameters method in compat-lib - Create and track feature requests for Atlassian products.

Thanks for raising it and please let me know if you have some questions.

Regards,
Ganesh

Hi @scott.dudley,
While we look at your request, can you please provide us the use case for this class for plugins?
cc: @Kusal

Thanks,
Ganesh

Hi @ggautam,

We are experiencing significant issues while trying to test on the latest milestones which include the Struts2 work. We have integrated the recently released compatibility lib. However, this does not solve our problem of being able to have custom actions that can be used across versions. We are seeking more information on how we can implement custom actions that are compatible with both Confluence 7.x.x and 8.x.x.

The docs recommend extending ConfluenceActionSupport. We’d effectively need two different base classes for 7 and 8, because ConfluenceActionSupport on 7.x.x extends com.opensymphony.xwork.ActionSupport and 8.x.x extends com.opensymphony.xwork2.ActionSupport.

This is a major issue for us as this breaks some of most popular macros. Can you please advise on how we can implement custom actions that are compatible with both 7 and 8?

Thanks!

Hi @jyamdogo

ConfluenceActionSupport in Confluence 8.x is backwards compatible with the implementation in Confluence 7.x. By extending solely the ConfluenceActionSupport class instead of the corresponding XWork ActionSupport class, your custom action should be insulated from the underlying changes.

Hi @Kusal

We are not extending XWork ActionSupport class directly. We have a custom action that extends Atlassian’s CreatePageAction. CreatePageAction eventually extends AbstractPageAwareAction, which extends ConfluenceActionSupport, which extends the XWork ActionSupport class.

We compile against the 7.x.x version of Confluence, because we need to maintain compatibility across Confluence 7 and 8. The 7.x.x version of Confluence gives the old XWork classes because ConfluenceActionSupport extends ActionSupport.

What you’re describing does not sound like backwards compatibility. It seems like we would need to compile the same code against two different APIs. Something along the lines of having two separate implementations compiled against different versions and then registering/using the right one based on the runtime app version.

We would think that that we should still be able to compile against 7.x.x and still have compatibility with 8, without the need to implement a customized solution for a specific API.

Let me know if you need more info. Thx for your help :smile:

@jyamdogo Yes what you’ve described is correct. What are you finding that is not working in your plugin on Confluence 8.x when compiled against the ConfluenceActionSupport class in Confluence 7.x?

@Kusal Compiling against Confluence 7.x and then trying to use our macro + custom action on Confluence 8 produces a NoClassDefFoundError:

Caused by: java.lang.NoClassDefFoundError: com/opensymphony/xwork/ActionSupport
	at java.lang.Class.getDeclaredMethods0(Native Method) ~[?:?]
	at java.lang.Class.privateGetDeclaredMethods(Class.java:3166) ~[?:?]
	at java.lang.Class.getDeclaredMethods(Class.java:2309) ~[?:?]

com.opensymphony.xwork.ActionSupport doesn’t exist in Confluence 8, it’s been replaced by com.opensymphony.xwork2.ActionSupport, right?

Again, we are not directly extending the old ActionSupport class directly. We are effectively doing what the docs say to do, which is to extend ConfluenceActionSupport (we are just doing it via extending your CreatePageAction). The problem is that ConfluenceActionSupport inherits from two different versions of ActionSupport across 7.x and 8.x. We think compiling against the 7.x.x version of ConfluenceActionSupport should still work on Confluence 8, but it currently doesn’t because of the mismatch in Xwork classes.

Let me know if that makes sense :smile:

@jyamdogo This error occurs when attempting to load a class which references the com.opensymphony.xwork package in its bytecode. Having your Action class extend ConfluenceActionSupport will not cause this.

It can be caused if your Action class explicitly imports a class from the legacy XWork package, or if you call a constructor/method that uses a legacy XWork class as a parameter or return type.

However thanks to your report we have investigated this in more depth and found that there may exist some common cases where developers may inadvertently include legacy XWork bytecode in their plugins.

Breadcrumbs which extend com.atlassian.confluence.util.breadcrumbs.AbstractActionBreadcrumb take an XWork Action as a constructor parameter and thus if your action includes some code such as the following, it is rendered incompatible, as when compiled against Confluence 7.x, it will contain legacy XWork bytecode.

@Override
public Breadcrumb getBreadcrumb() {
    return new UserProfileActionBreadcrumb(this);
}

We are investigating this further and have created a tracking issue here.

I would also be interested in knowing what line(s) of code specifically in your plugin are causing a bytecode incompatibility.

Hi @ggautam and @Kusal,

PluginAwareActionConfig Changes

With regards to the PluginAwareActionConfig being made private starting in 8.0.0-m69:

This is used by us for dynamically manipulating Xwork actions. To ensure that the correct classloader is eventually used, we need to create a PluginAwareActionConfig rather than a straight ActionConfig.

Speaking also of recent changes, we noticed that some things with Velocity work quite differently since (and including) the 8.0.0-m69 release:

Two VelocityManagers instead of one

Also since 8.0.0-m69, Confluence now seems to have two Velocity instances/engines registered instead of just one:

  1. the VelocityManager registered in the bootstrapContext (see VelocityUtils#getVelocityEngine), and
  2. a completely separate VelocityManager registered in the Struts container (StrutsAppConfig#velocityManager).

Is there any reason for this, or can Confluence go back to having one Velocity instance like it did previously? (This can result in two instances of VelocityEngines being used to render different parts of pages, for example, such as when a macro uses Velocity to render something. The macro will use the bootstrapContext engine while the page will use the Struts one.)

Struts-related beans not injectible

We also noticed that we cannot seem to inject the handful of beans that are defined in the StrutsAppConfig class (for example, ConfigurationManager). These can seemingly be instantiated directly by calling ContainerManager#getComponent though. Is there any way to inject these in a P2 context?

Thanks!
Scott

FYI we are also blocked on [CONFSERVER-80174] Fix for ActionContextCompatManager.setParameters method in compat-lib - Create and track feature requests for Atlassian products.

Thank you @scott.dudley for the detailed feedback.

Re: PluginAwareActionConfig
Thanks for providing the context, I’ve created a tracking item here.

Re: VelocityManagers
This is something we are aware of but is unfortunately not straightforward to rectify. Are you able to please provide further context or an example on why this is causing issues for your plugin? This will help us to evaluate the severity of this issue and prioritise accordingly.

Re: Struts beans
Could you please again provide context for how your plugins are utilising these beans and how you were achieving the same goal in Confluence 7.x?

Thank you!

Hi @Kusal,

Thanks for the updates!

Items #2 and #3 are interrelated. We are applying some customizations to the VelocityEngine, which is why we need to access it. Back when there was only one VelocityEngine in Confluence <=7, this was easy (VelocityUtils#getVelocityEngine).

Now that there are two VelocityEngines in Confluence 8+ (the static one and the dynamically-created instance within the Struts context), we need to get a hold of both (and so long as we can customize both consistently, that works for us too). But accessing the Struts-context instance is seemingly only possible through the beans declared in StrutsAppConfig.

I hope this helps to clarify!

Scott

Thanks @scott.dudley

I’ve created a tracking card here.

Am I correct in my understanding that this is not a blocker for you as you are currently using the workaround described in the card?