JIRA Plugin using classes from another one

Am looking for a way to use a class from plugin A in plugin B without having to add plugin A as dependency; I tried to follow this tutorial by adding component-import in plugin B and component in plugin A (https://developer.atlassian.com/server/framework/atlassian-sdk/component-plugin-module/) I tried also to use Spring expressions such as @ExportAsService and @ComponentImport and tried to modify the beans with a osgi service; I even added the Export Package annotation in plugin A (without the dependency in plugin B) but it did not work and the compiler can still not recognize the class in plugin B

Thanks

Hi @ChristianK,

did you try to add them in the pom.xml’s (Plugin A: <export-package>; Plugin B: <import-package>)? Have a look at the answer of @wseliga here: https://community.atlassian.com/t5/Answers-Developer-Questions/Re-Using-classes-from-other-plugin/qaq-p/485527
That worked for me once.

Best regards
Alex

So what you want to do requires combined maven and osgi magic. So here goes - let’s build 2 very plugins. One(plugin1) that renders a servlet where you can submit some string to it. Another one(plugin2) that has a servlet reads the string that you submit to the plugin1 plugin.

API project

You’ll need to create an Interface for the service you’re creating. In my case -I’m going to create a third maven project called api (I’m super imaginative).

This is the code of the pom.xml for the api project:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>mygroup</groupId>
    <artifactId>api</artifactId>
    <version>1.0.0-SNAPSHOT</version>

    <organization>
        <name>Example Company</name>
        <url>http://www.example.com/</url>
    </organization>

    <name>api</name>

    <packaging>jar</packaging>
</project>

Note the packaging is jar (not atlassian-plugin).

In this api project we’ll create a simple interface at api.SampleService:

package api;

public interface SampleService
{
    public String fetchMessage();
    public void setMessage( String message);
}

That’s it - we have our api. Go into the directory and run atlas-mvm clean install. This will build the api and install the api into your local maven repo. Let’s move onto our apps.

plugin1 implementation

First thing - let’s import the api we created:
Add the dependency to your pom.xml:

        <dependency>
            <groupId>mygroup</groupId>
            <artifactId>api</artifactId>
            <version>1.0.0-SNAPSHOT</version>
        </dependency>

In plugin1 we’re going to implement the actual service, so we’ll create plugin.SampleServiceImpl:

package plugin1;

import api.SampleService;
import com.atlassian.plugin.spring.scanner.annotation.component.JiraComponent;
import com.atlassian.plugin.spring.scanner.annotation.export.ExportAsService;

@ExportAsService
@JiraComponent
public class SampleServiceImpl implements SampleService
{
    String theMessage = null;

    public SampleServiceImpl()
    {
        this.theMessage = "";
    }

    public String fetchMessage()
    {
        return this.theMessage;
    }

    public void setMessage(String message)
    {
        this.theMessage = message;
    }
}

Note the @ExportAsService and the @JiraComponent annotation. The first one is needed to expose the service outside of plugin1, the second one is needed to expose the plugin2 internally (or at least when I did this plugin). The reason I added the @JiraComponent annotation instead of @Component is because I’m lazy and didn’t feel like chasing after the right Component classes to use.

Note: Don’t use a class variable String in production code - you’ll get some weird results when things are reloaded.

Now that we have the api defined and implemented, we can expose it as a service for osgi. In the pom.xml - add api.* to the section so you have something like:

<plugin>
                <groupId>com.atlassian.maven.plugins</groupId>
                <artifactId>maven-jira-plugin</artifactId>
                <version>${amps.version}</version>
                <extensions>true</extensions>
                <configuration>
                    <productVersion>${jira.version}</productVersion>
                    <productDataVersion>${jira.version}</productDataVersion>
                    <!-- Uncomment to install TestKit backdoor in JIRA. -->
                    <!--
                    <pluginArtifacts>
                        <pluginArtifact>
                            <groupId>com.atlassian.jira.tests</groupId>
                            <artifactId>jira-testkit-plugin</artifactId>
                            <version>${testkit.version}</version>
                        </pluginArtifact>
                    </pluginArtifacts>
                    -->
                    <enableQuickReload>true</enableQuickReload>
                    <enableFastdev>false</enableFastdev>

                    <!-- See here for an explanation of default instructions: -->
                    <!-- https://developer.atlassian.com/docs/advanced-topics/configuration-of-instructions-in-atlassian-plugins -->
                    <instructions>
                        <Atlassian-Plugin-Key>${atlassian.plugin.key}</Atlassian-Plugin-Key>

                        <!-- Add package to export here -->
                        <Export-Package>
                            api.*,
                        </Export-Package>

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

                        <!-- Ensure plugin is spring powered -->
                        <Spring-Context>*</Spring-Context>
                    </instructions>
                </configuration>
            </plugin>

We’ll go ahead and create the servlet that consumes this servlet:

package plugin1;

import api.SampleService;

import com.atlassian.templaterenderer.TemplateRenderer;

import com.atlassian.plugin.spring.scanner.annotation.component.Scanned;
import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport;

import javax.inject.Inject;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@Scanned
public class PostingServlet extends HttpServlet
{
    @ComponentImport
    private final TemplateRenderer templateRenderer;


    private final SampleService sampleService;


    @Inject
    public PostingServlet( final TemplateRenderer templateRenderer,
                           final SampleService sampleService)
    {
        this.templateRenderer = templateRenderer;
        this.sampleService = sampleService;
    }
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
    {

        Map<String, Object> context = new HashMap<String, Object>();

        this.templateRenderer.render("/templates/submission.vm", context, response.getWriter());


    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
    {

        Map<String, Object> context = new HashMap<String, Object>();
        this.sampleService.setMessage(request.getParameter("data"));

        context.put("submitted", true);

        this.templateRenderer.render("/templates/submission.vm", context, response.getWriter());

    }
}

Note that we don’t tag the SampleService with @ComponentImport since it’s not an external service at this point.

We’ll also create the submission.vm as well (the html is pretty basic and basically gives a textarea where we can submit the data).

<html>
    <head>
        <meta name="decorator" content="atl.general"/>
    </head>
    <body>
        <form method="post" class="aui" style="margin-left:auto;margin-right:auto;width:400px;">

        <h1>Submit data</h1>
        #if($submitted)
            <h2>Data was submitted</h2>
        #end

            <div>
            <textarea class="textarea" name="data"></textarea>
            </div>
            <div>
                <input type="submit" class="aui-button aui-button-primary" value="submit"/>
            </div>
        </form>
    </body>
</html>

Then finally in the atlassian-plugin.xml for the plugin1 app we’ll need to define the servlet:

    <servlet name="My Servlet" i18n-name-key="my-servlet.name" key="my-servlet" class="plugin1.PostingServlet">
        <description key="my-servlet.description">The My Servlet Plugin</description>
        <url-pattern>/demo/plugin1</url-pattern>
    </servlet>

Note the path /demo/plugin1. You can now run this plugin by executing atlas-run from within the plugin1 app. This will make the plugin1 to be installed and you can go to: http://localhost:2990/jira/plugins/servlet/demo/plugin1 and see a pretty textarea. Now onto the reading part (plugin2).

plugin2
For this one we’ll add the api dependency in the pom.xml:

        <dependency>
            <groupId>mygroup</groupId>
            <artifactId>api</artifactId>
            <version>1.0.0-SNAPSHOT</version>
            <scope>provided</scope>
        </dependency>

Note the scope is provided - plugin1 will provide the classes since it’s not scope of provided in plugin1.

Also in the section - you need to add: api.* so you’ll get something like:

 <plugins>
            <plugin>
                <groupId>com.atlassian.maven.plugins</groupId>
                <artifactId>maven-jira-plugin</artifactId>
                <version>${amps.version}</version>
                <extensions>true</extensions>
                <configuration>
                    <productVersion>${jira.version}</productVersion>
                    <productDataVersion>${jira.version}</productDataVersion>
                    <!-- Uncomment to install TestKit backdoor in JIRA. -->
                    <!--
                    <pluginArtifacts>
                        <pluginArtifact>
                            <groupId>com.atlassian.jira.tests</groupId>
                            <artifactId>jira-testkit-plugin</artifactId>
                            <version>${testkit.version}</version>
                        </pluginArtifact>
                    </pluginArtifacts>
                    -->
                    <enableQuickReload>true</enableQuickReload>
                    <enableFastdev>false</enableFastdev>
                    <!-- See here for an explanation of default instructions: -->
                    <!-- https://developer.atlassian.com/docs/advanced-topics/configuration-of-instructions-in-atlassian-plugins -->
                    <instructions>
                        <Atlassian-Plugin-Key>${atlassian.plugin.key}</Atlassian-Plugin-Key>
                        <!-- Add package to export here -->
                        <Export-Package>mygroup.api,</Export-Package>
                        <!-- Add package import here -->
                        <Import-Package>org.springframework.osgi.*;resolution:="optional", org.eclipse.gemini.blueprint.*;resolution:="optional", *,
                            api.*,</Import-Package>
                        <!-- Ensure plugin is spring powered -->
                        <Spring-Context>*</Spring-Context>
                    </instructions>
                </configuration>
            </plugin>

We’re almost done. We’re now importing the SampleService and can make use of it. We’ll create a servlet:

package plugin2;


import api.SampleService;
import com.atlassian.plugin.spring.scanner.annotation.component.Scanned;
import com.atlassian.templaterenderer.TemplateRenderer;
import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport;

import javax.inject.Inject;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@Scanned
public class GetServlet extends HttpServlet
{
    @ComponentImport
    private final TemplateRenderer templateRenderer;


    @ComponentImport
    private final SampleService sampleService;


    @Inject
    public GetServlet( final TemplateRenderer templateRenderer,
                           final SampleService sampleService)
    {
        this.templateRenderer = templateRenderer;
        this.sampleService = sampleService;
    }

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
    {

        Map<String, Object> context = new HashMap<String, Object>();

        context.put("message", this.sampleService.fetchMessage());
        this.templateRenderer.render("/templates/reader.vm", context, response.getWriter());
    }
}

and the reader.vm will look like:

<html>
    <head>
        <meta name="decorator" content="atl.general"/>
    </head>
    <body>
        <form method="post" class="aui" style="margin-left:auto;margin-right:auto;width:400px;">

        <h1>Read data</h1>
            <div>
            #if($message!='')
            $message
            #else
                No data set yet :(
            #end
            </div>
        </form>
    </body>
</html>

Finally we’ll add the servlet to the atlassian-plugin.xml:

  <servlet name="My Servlet" i18n-name-key="my-servlet.name" key="my-servlet" class="plugin2.GetServlet">
    <description key="my-servlet.description">The My Servlet Plugin</description>
    <url-pattern>/demo/plugin2</url-pattern>
  </servlet>

Assuming that you executed atlas-run in the plugin1 app - for plugin2 - you should execute atlas-package and atlas-install-plugin. This will cause plugin2 to be uploaded to the jira instance.

Once you’ve done this, you can go to http://localhost:2990/jira/plugins/servlet/demo/plugin1 and give it a string and then go to http://localhost:2990/jira/plugins/servlet/demo/plugin2 and see the string come back there.

In theory you can skip the api project, depending on what you’re doing. However if you’re working on an api that third parties will use - I strongly recommend publishing an api project somewhere so that people don’t have to download your app and make that a maven dependency.

I went ahead and published the code for this at: Bitbucket if you want to poke around.

8 Likes

Wow; I imagined that I can use a middle plugin to handle the dependency and that looks great if going into this solution perspective of adding a third plugin. Thank you for code and solution

Loved your answer, I already knew basic way. But going through each step helped a lot.

hi. can I have a third plugin that also implements SampleService, say plugin3? do i just add the dependency to the mygroup.api and add the api.* in the Export-Package tag in plugin3’s pom.xml ?

Hi Daniel,

great job, this tutorial helped a lot.
Thanks:-)
Heiko