Extend your Jira Server/Data Center app with ModuleType

Hello!

In this article we will talk how to extend your own Jira Server/Data Center app with the ModuleType annotation.

The ModuleType annotation lets you expose a module from you Jira Server/Data Center app.

Why would we need it?

Let’s take Configuration Manager. This app allows you to migrate Jira configuration from one instance to another. During this migration we also migrate custom fields. But the developers of Configuration Manager can not migrate all types of custom fields because there are custom fields which contain its own settings in the Jira database. For example, Insight. The developers of Configuration Manager would have to learn how the Insight app stores configuration data for the Insight custom fields to implement migration of these custom fields. That is why it would be much easier for Configuration Manager developers to provide an interface which Insight developers would use to implement migration of their custom fields. And Configuration Manager developers did it. They created modules which can be extended by other developers. You can read more about it here.

And in this article we will create such a module for our app.

We will create a calculator app.

Our app will be able to sum two values and we will add a module which will let other developers to add new operations in their apps. And also we will develop an app which will add the minus operation this way.

Ok. Let’s get started!

Calculator

You can see the source code for this section here.

We will get the result from our Calculator app by a rest call. That is why I created a rest endpoint in the Calculator app.

src/main/java/ru/matveev/alexey/atlassian/calculator/rest/CalculatorRest.java

@Path("/calculate")
public class CalculatorRest {
    private final CalculatorService calculatorService;

    public CalculatorRest(CalculatorService calculatorService) {
        this.calculatorService = calculatorService;
    }

    @POST
    @AnonymousAllowed
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces({MediaType.APPLICATION_JSON})
    public Response calculate(CalculatorModel body) throws OperationNotFoundException {
        int result = this.calculatorService.calculate(body.getOperation(),
                                                      body.getValue1(),
                                                      body.getValue2());
        return Response.ok(new CalculatorResponseModel(result)).build();
    }

We add a POST REST endpoint. We accept CalculatorModel as the request body for this endpoint then we calculate the result and return this result as CalculatorResponseModel.

Here is our CalculatorModel.

src/main/java/ru/matveev/alexey/atlassian/calculator/rest/CalculatorModel.java

@Data
public class CalculatorModel {

    private String operation;
    private int value1;
    private int value2;

}

Which means that our request body should look like this:

{"operation": "sum", "value1": 1, "value2" : 3}

And here is our CalculatorResponseModel.

src/main/java/ru/matveev/alexey/atlassian/calculator/rest/CalculatorResponseModel.java

@XmlRootElement(name = "result")
@XmlAccessorType(XmlAccessType.FIELD)
@Data
public class CalculatorResponseModel {

    @XmlElement(name = "result")
    private int result;

    public CalculatorResponseModel(int result) {
        this.result = result;
    }

}

Which means that our response will be like this:

{"result": 4}

Have a look at our REST endpoint (CalculatorRest.java). We get the result with the CalculatorService service:

int result = this.calculatorService.calculate(body.getOperation(),
                                                      body.getValue1(),
                                                      body.getValue2());

Here is our CalculatorService.

src/main/java/ru/matveev/alexey/atlassian/calculator/service/CalculatorService.java

@Named
public class CalculatorService {
    private final SumOperation sumOperation;
    public CalculatorService(SumOperation sumOperation) {
        this.sumOperation = sumOperation;
    }

    public int calculate(String operation, int val1, int val2) throws OperationNotFoundException {
        if (operation.equals(sumOperation.getName())) {
            return sumOperation.calculate(val1, val2);
        }
        throw new OperationNotFoundException(String.format("Operation %s not found", operation));
    }
}

As you can see we have the calculate method where we check if the operation parameter equals to the operation name defined in SumOperation, we call the calculate method of the SumOperation class instance.

If the value of the operation parameter does not equal to the name defined in SumOperation, we throw the OperationNotFoundException.

src/main/java/ru/matveev/alexey/atlassian/calculator/exception/OperationNotFoundException.java

public class OperationNotFoundException extends Exception {
    public OperationNotFoundException(String errorMessage) {
        super(errorMessage);
    }
}

Here is our SumOperation class:

src/main/java/ru/matveev/alexey/atlassian/calculator/impl/SumOperation.java

@Named
public class SumOperation implements Operation
{

    @Override
    public String getName() {
        return "sum";
    }

    @Override
    public int calculate(int val1, int val2) {
        return val1 + val2;
    }
}

We just implemented Operation interface which should be implemented by all operations for our calculator.

src/main/java/ru/matveev/alexey/atlassian/calculator/api/Operation.java

public interface Operation
{
    String getName();
    int calculate(int val1, int val2);
}

That is all. Let’s run our app.

Run app

Run our app and open the following URL in your browser:

http://localhost:2990/jira/plugins/servlet/restbrowser#/resource/calculator-1-0-calculate

You will see an interface to run our REST endpoint.

First, pass this json as the request body:

{"operation": "sum", "value1": 1, "value2" : 3}

And you will get the following result:

The result is correct.

Let’s try to perform the minus operation with a request body like this:

{"operation": "minus", "value1": 1, "value2" : 3}

And OperationNotFoundException will be thrown:

Everything is correct. Now let’s extend our calculator with a moduletype to allow other apps to implement other operations.

Change our app

Now we need to add support for modules to our calculator.

First uncomment the jira-core dependency in the pom.xml file.

<dependency>
            <groupId>com.atlassian.jira</groupId>
            <artifactId>jira-core</artifactId>
            <version>${jira.version}</version>
            <scope>provided</scope>
</dependency>

Then add com.atlassian.plugin.osgi.bridge.external to Import-Package in the pom.xml file

<Import-Package>org.springframework.osgi.*;resolution:="optional", com.atlassian.plugin.osgi.bridge.external, org.eclipse.gemini.blueprint.*;resolution:="optional", *</Import-Package>

Next add our module descriptor.

src/main/java/ru/matveev/alexey/atlassian/calculator/module/OperationModuleDescriptor.java

public class OperationModuleDescriptor extends AbstractModuleDescriptor<Operation>
{
    public OperationModuleDescriptor(final @ComponentImport ModuleFactory moduleFactory)
    {
        super(moduleFactory);
    }

    @Override
    public Operation getModule()
    {
        return moduleFactory.createModule(moduleClassName, this);
    }
}

As you can see we created this module descriptor as AbstractModuleDescriptor of the Operation interface type.

Next, we need to register our module descriptor.

src/main/java/ru/matveev/alexey/atlassian/calculator/module/BasicModuleTypeFactory.java

@ModuleType(ListableModuleDescriptorFactory.class)
@Named
public class BasicModuleTypeFactory extends
        SingleModuleDescriptorFactory<OperationModuleDescriptor>
{
    @Autowired
    public BasicModuleTypeFactory(HostContainer hostContainer)
    {
        super(hostContainer, "calculatorOperation", OperationModuleDescriptor.class);
    }
}

As you can see We registered our module descriptor under the calculatorOperation name.

Done.

Now apps can define calculatorOperation in their atlassian-plugin.xml file.

But let’s also change our CalculatorService.

When we need to look for operations in modules of external apps.

I changed src/main/java/ru/matveev/alexey/atlassian/calculator/service/CalculatorService.java to this one:

@Named
public class CalculatorService {
    private final SumOperation sumOperation;
    private final PluginAccessor pluginAccessor;
    public CalculatorService(final @ComponentImport PluginAccessor pluginAccessor,  SumOperation sumOperation) {
        this.sumOperation = sumOperation;
        this.pluginAccessor = pluginAccessor;
    }

    public int calculate(String operation, int val1, int val2) throws OperationNotFoundException {
        if (operation.equals(sumOperation.getName())) {
            return sumOperation.calculate(val1, val2);
        }
        Operation operationModule = this.getModuleForOperationName(operation);
        if (operationModule != null) {
            return operationModule.calculate(val1, val2);
        }
        throw new OperationNotFoundException(String.format("Operation %s not found", operation));
    }

    private Operation getModuleForOperationName(String operationName) {
        List<OperationModuleDescriptor> operationModuleDescriptors =
                pluginAccessor.getEnabledModuleDescriptorsByClass(OperationModuleDescriptor.class);
        for (OperationModuleDescriptor operationModuleDescriptor : operationModuleDescriptors)
        {
            if (operationName.equals(operationModuleDescriptor.getModule().getName())) {
                return operationModuleDescriptor.getModule();
            }
        }
        return null;
    }
}

I added getModuleForOperationName which returns a reference to a module in an external app if the operation name we want to execute equals to the name of the operation provided by this module.

And then I added these lines to the calculate method:

Operation operationModule = this.getModuleForOperationName(operation);
if (operationModule != null) {
   return operationModule.calculate(val1, val2);
}

First, we look for a module in another app which provides implementation of the operation we need and then we run the calculate method on the found module.

That is all with the calculator app. Now let’s write an extension to our app.

Calculator-extension

I created an ordinary plugin from Atlassian SDK.

I added dependency to the jar file of the calculator app. I need it to use the Operation interface defined in the calculator app.

<dependency>
            <groupId>ru.matveev.alexey.atlassian</groupId>
            <artifactId>calculator</artifactId>
            <version>1.0.0-SNAPSHOT</version>
            <scope>system</scope>
            <systemPath>${project.basedir}/../calculator/target/calculator-1.0.0-SNAPSHOT.jar</systemPath>
</dependency>

Then I created the minus operation.

src/main/java/ru/matveev/alexey/atlassian/operation/MinusOperation.java

public class MinusOperation implements Operation {
    @Override
    public String getName() {
        return "minus";
    }

    @Override
    public int calculate(int val1, int val2) {
        return val1 - val2;
    }
}

As you can see I implemented this class from the Operation interface and defined two methods: getName (returns the “minus” value) and calculate.

And now I can add calculatorOperation module to the atlassian-plugin.xml file.

src/main/resources/atlassian-plugin.xml

 <calculatorOperation key="minus-operation"  class="ru.matveev.alexey.atlassian.operation.MinusOperation"/>

And that is it. Install this app to the same Jira instance where the Calculator app is installed.

Test extension app

I installed our two apps to the same Jira instance.

And now I will execute our http://localhost:2990/jira/rest/calculator/1.0/calculate REST endpoint with the following request body:

{“operation”: “minus”, “value1”: 1, “value2” : 3}

As you can see I am trying to use the minus operation from our calculator-extension app. And here is the result:

And the result is -2 as expected. We did what we wanted. We added a new operation in an external app for our calculator app. Now other apps can extend our calculator with their own operations.

You can find the final code here.

If you have found a spelling error, please, notify us by selecting that text and pressing Ctrl+Enter.

Leave a Reply

%d bloggers like this:

Spelling error report

The following text will be sent to our editors: