Single webwork add-on for Confluence and JIRA server

This post is about to create a single add-on with webwork actions for both Confluence and Jira server instances.

Atlassian gives us an ability to define separate components for a specific application like Confluence, Jira, Bitbucket, Bamboo, etc. As you know the components can be specified in the add-on descriptor atlassian-plugin.xml: web-items, web-resources, webworks and many others. To define in which application a certain component will work, we need to use an attribute „application“ with an appropriate value: jira, confluence or any other Atlassian application.

<xwork key="confluence-webwork-actions" name="My Webwork Actions" application="confluence">
...
</xwork>

<webwork1 key="jira-webwork-actions" name=""My Webwork Actions" class="java.lang.Object" application="jira">
...
</webwork1>

The common add-on for both applications requires also dependencies, e.g. we can have following configuration:

...
        <dependency>
            <groupId>com.atlassian.jira</groupId>
            <artifactId>jira-api</artifactId>
            <version>${jira.version}</version>
            <scope>provided</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-api</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>com.atlassian.jira</groupId>
            <artifactId>jira-core</artifactId>
            <version>${jira.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.atlassian.confluence</groupId>
            <artifactId>confluence</artifactId>
            <version>${confluence.version}</version>
            <scope>provided</scope>
        </dependency>
...

The above configuration looks fine but we need to manually add records for Import-Packages which ensure that both versions of webwork will be loaded based on the application. Otherwise we will get an error message for the missing class in the classpath.

<build>
        <plugins>
            <plugin>
                <groupId>com.atlassian.maven.plugins</groupId>
                <artifactId>maven-amps-plugin</artifactId>
                <version>${amps.version}</version>
                <extensions>true</extensions>
                <configuration>
                    <enableQuickReload>true</enableQuickReload>
                    <enableFastdev>false</enableFastdev>
                    <instructions>
                        <Atlassian-Plugin-Key>${atlassian.plugin.key}</Atlassian-Plugin-Key>

                        <!-- Add package import here -->
                        <Import-Package>
                            com.atlassian.*;resolution:="optional",
                            org.springframework.osgi.*;resolution:="optional",
                            org.eclipse.gemini.blueprint.*;resolution:="optional",
                            com.opensymphony.xwork.*;resolution:="optional",
                            webwork.action;resolution:="optional",
                            *;resolution:="optional"
                        </Import-Package>

                        <Spring-Context>*</Spring-Context>
                    </instructions>
                    <products>
                        <product>
                            <id>jira</id>
                            <version>${jira.version}</version>
                            <systemPropertyVariables>
                                <product>jira</product>
                            </systemPropertyVariables>
                        </product>
                        <product>
                            <id>confluence</id>
                            <version>${confluence.version}</version>
                            <systemPropertyVariables>
                                <product>confluence</product>
                            </systemPropertyVariables>
                        </product>
                    </products>
                </configuration>
            </plugin>
...
        </plugins>
</build>

And our webwork action can looks like:

public class MyConfluenceAction extends ConfluenceActionSupport {

    ...
    public String doData() throws Exception {
        ...
        HttpServletResponse response = ServletActionContext.getResponse();
        response.setStatus(200);
        response.setContentType("application/json");
        response.getWriter().write("{}");
        response.getWriter().flush();
        response.getWriter().close();

        return NONE;
    }
    ...
}

It works and also does not work 🙂 If the class ServletActionContext is from the Jira’s package webwork.action.ServletActionContext, it works. But if the class is from the Confluence’s package com.opensymphony.webwork.ServletActionContext, it fails. Why it fails? Maven loads Jira’s dependency with webwork v1 at first and then webwork v2 from Confluence will not be loaded because you cannot load two dependencies with same groupId and artifactId of different versions.

How we can solve it?

There are differences between webwork in Confluence and Jira. Confluence uses webwork of version 2 instead of version 1 in Jira. The most important change is that we have to use different packages which is not possible with maven. We need to bring both packages to the IDE and to the compiler. For example when you need to use ServletActionContext class to return a HttpServletResponse for a current request.

My solution is to use a fake class ServletActionContext with the same package name in the project along my add-on code.

package com.opensymphony.webwork;

import javax.servlet.http.HttpServletResponse;

/**
 * Fix for Confluence
 */
public class ServletActionContext {

    public static HttpServletResponse getResponse() {
        return null; // return whatever 
    }
}

Now I can use that class and my IDE is not complaining about missing class or dependency. Side effect of this solution is that the compiled class will be part of our add-on, included in the jar file which will not import an original class in Confluence.

Therefore I decided to remove that compiled class in the process-classes phase of maven flow, e.g. I have used the maven antrun plugin to remove it.

<build>
    <plugins>
        ...
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-antrun-plugin</artifactId>
            <executions>
                <execution>
                    <id>fix-webwork</id>
                    <phase>process-classes</phase>
                    <goals>
                        <goal>run</goal>
                    </goals>
                    <configuration>
                        <tasks>
                            <delete dir="${project.build.directory}/classes/com/opensymphony" />
                        </tasks>
                    </configuration>
                </execution>
            </executions>
        </plugin>
        ...
    </plugins>
</build>

Now I can have webwork actions for both applications Confluence and Jira in one add-on and the proper classes will be imported into add-on by the application.

If you have better solution, please, leave a comment and share it.