Last updated Dec 23, 2024

Implementing application links in JIRA

Applicable:

This tutorial applies to JIRA 5.0.

Level of experience:

This is an intermediate tutorial. You should have completed at least one beginner tutorial before working through this tutorial. See the list of developer tutorials.

Time estimate:

It should take you approximately 1 hour to complete this tutorial.

Overview of the Tutorial

This tutorial shows you how to build a JIRA plugin that communicates with a Confluence instance through an application link. The plugin will add a panel to the issue view page in JIRA. When a user opens the issue in JIRA, the plugin searches the Confluence space linked to the JIRA project and lists the pages that mention the issue. In other words, the panel lists any page in a Confluence space that references the currently viewed issue by issue key.

Here's how it'll look:

Getting this done will require a little administration work along with your development work. First, you'll need to set up an Application Link between the JIRA instance and the Confluence installation. You will then need to configure a project link between a JIRA project and Confluence space.

Your JIRA plugin will consist of the following components:

  • Java classes encapsulating the plugin logic.
  • Resources for display of the plugin user interface (UI).
  • A plugin descriptor (XML file) to enable the plugin module in the Atlassian application.

When you have finished, all these components will be packaged in a single JAR file.

About these Instructions

You can use any supported combination of OS and IDE to construct this plugin. These instructions were written using Ubuntu Linux. If you are using another OS or IDE combination, you should use the equivalent operations for your specific environment.

This tutorial was last tested with JIRA 6.0

Prerequisite Knowledge

To complete this tutorial, you need to know the following:

  • The basics of Java development: classes, interfaces, methods, how to use the compiler, and so on.
  • How to create an Atlassian plugin project using the Atlassian Plugin SDK.
  • How to open the plugin project in your IDE, such as Eclipse or IDEA.

Plugin Source

We encourage you to work through this tutorial. If you want to skip ahead or check your work when you have finished, you can find the plugin source code on Atlassian Bitbucket. Bitbucket serves a public Git repository containing the tutorial's code. To clone the repository, issue the following command:

1
2
git clone https://bitbucket.org/atlassian_tutorial/jira-applinks

Alternatively, you can download the source as a ZIP archive by choosing download here:https://bitbucket.org/atlassian_tutorial/jira-applinks

Step 1. Create the plugin project

In this step, you'll use two atlas- commands to generate stub code for your plugin. The atlas- commands are part of the Atlassian Plugin SDK, and automate much of the work of plugin development for you.

  1. If you have not already set up the Atlassian Plugin SDK, do that now: Set up the Atlassian Plugin SDK and Build a Project.

  2. Open a terminal and navigate to the directory in which you want to create the plugin project.

  3. Enter the following command to create a plugin skeleton:

    1
    2
    atlas-create-jira-plugin
    
  4. Choose the option to create a plugin for JIRA 5.0.

  5. As prompted, enter the following information for your plugin:

    group-id

    com.example.plugins.tutorial

    artifact-id

    tutorial-jira-ual

    version

    1.0-SNAPSHOT

    package

    com.example.plugins.tutorial

  6. Confirm your entries when prompted.

Step 2. Review and tweak the generated stub code

It's a good idea to familiarise yourself with the project configuration file, known as the POM (Project Object Model definition file). In this section, you will review and tweak the POM file.

Add plugin metadata to the POM

The POM is located at the root of your project and declares the project dependencies and other information.

Add some metadata about your plugin and your company or organisation. You also need to add a dependency on Application Links here.

  1. In the command window, switch to the new directory created for your project, tutorial-jira-ual. 

  2. Open the pom.xml file for editing, and add your company or organisation name and your website to the <organization> element:

    1
    2
    <organization>
        <name>Example Company</name>
        <url>http://www.example.com/</url>
    </organization>
    
  3. Update the <description> element:

    1
    2
    <description>This is the UAL plugin tutorial for Atlassian JIRA.</description>
    
  4. Add a dependency on Application Links

    1
    2
    <dependency>
        <groupId>com.atlassian.applinks</groupId>
        <artifactId>applinks-api</artifactId>
        <version>3.2</version>
        <scope>provided</scope>
    </dependency>
    
  5. Save the file.

Step 3. Add your plugin module to the plugin descriptor

Now use the plugin module generator (another atlas- command) to generate the stub code for the modules needed by the plugin. The plugin module type is an issue tab panel plugin module.

Add it as follows:

  1. In a command window, go to the root folder of your plugin project (where the pom.xml is located).
  2. Run atlas-create-jira-plugin-module.
  3. For the module type to add, choose Issue Tab Panel.
  4. When prompted, enter the following information to describe your plugin module:
    1. Enter New Classname: ConfluenceSpaceTabPanel
    2. Enter Package Name: com.example.plugins.tutorial.jira.tabpanels
  5. When prompted with Show Advanced Setup, choose 'N' (for 'No').
  6. When prompted with Add Another Plugin Module, choose 'N'.

At this point, your plugin contains the following files:

File

Description

LICENSE

The license for this plugin.

pom.xml

The Maven Project Object Model file.

README

The 'readme' file for this project.

src/main/java/com/example/plugins/

tutorial/jira/tabpanels/ConfluenceSpaceTabPanel.java

The Issue Tab Panel class generated.

src/main/java/com/example/plugins/

tutorial/MyPlugin.java

An empty Java class file generated by atlas-create-jira-plugin. Ignore or delete this.

src/main/resources/atlassian-plugin.properties

The file containing i18n key/value pairs.

src/main/resources/atlassian-plugin.xml

The Atlassian plugin descriptor.

src/main/resources/templates/

tabpanels/confluence-space-tab-panel.vm

The Velocity macro for rendering content on the Issue Tab Panel.

src/test/java/com/example/plugins/tutorial/

jira/tabpanels/ConfluenceSpaceTabPanelTest.java

A test file for the Issue Tab Panel class.

src/test/java/com/example/plugins/tutorial/

MyPluginTest.java

An empty Java file generated by atlas-create-jira-plugin. Ignore or delete this.

src/test/java/it/MyPluginTest.java

An empty Java file generated by atlas-create-jira-plugin. Ignore or delete this.

src/test/resources/TEST_RESOURCES_README

A readme file for test resources.

src/test/xml/TEST_XML_RESOURCES_README

A readme file for test resources.

velocity.log

The log output file created by atlas-create-jira-plugin-module. Ignore or delete this.

Step 4. Modify the plugin descriptor

Now adjust the plugin descriptor file, atlassian-plugin.xml, to import the Application Links component (EntityLinkService), as follows: 

  1. In the command window, change to the src/main/resources directory.

  2. Add the component-import element below to your atlassian-plugin.xml file:

    1
    2
    <atlassian-plugin ...>
      ...
        <issue-tabpanel key="confluence-space-tab-panel" ...>
          ...
        </issue-tabpanel>
    
        <component-import key="entityLinkService">
          <interface>com.atlassian.applinks.api.EntityLinkService</interface>
        </component-import>
      ...
    </atlassian-plugin>
    

    Notice the issue-tabpanel element. We added this when we ran the atlas-create-jira-plugin-module command.

Step 5. Develop your plugin code

You have already generated the stubs for your plugin modules. Now you'll write some code that will make your plugin do something.

As a reminder, this plugin will communicate with a Confluence site via Application Links. It searches the Confluence search for mentions of the currently viewed issue, and displays the results on the view issue page.

To do this, our Issue Tab Panel needs to use the EntityLinkService to retrieve and use the Application Link to Confluence.

The SDK code generator gave us a few of the methods we need, such as getActions() and showPanel(). We'll extend the code as follows:

  1. Open the ConfluenceSpaceTabPanel.java file for editing. You can find the file in the directory src/main/java/com/example/plugins/tutorial/jira/tabpanels under your project home. 

  2. First, add some import statments to the ones that the SDK gave us:

    1
    2
    import com.atlassian.applinks.api.ApplicationLinkRequest;
    import com.atlassian.applinks.api.ApplicationLinkRequestFactory;
    import com.atlassian.applinks.api.ApplicationLinkResponseHandler;
    import com.atlassian.applinks.api.CredentialsRequiredException;
    import com.atlassian.applinks.api.EntityLink;
    import com.atlassian.applinks.api.EntityLinkService;
    import com.atlassian.applinks.api.application.confluence.ConfluenceSpaceEntityType;
    import com.atlassian.crowd.embedded.api.User;
    
    import com.atlassian.jira.plugin.issuetabpanel.AbstractIssueTabPanel;
    import com.atlassian.jira.plugin.issuetabpanel.IssueAction;
    
    import com.atlassian.jira.util.http.JiraUrl;
    import com.atlassian.jira.web.ExecutingHttpRequest;
    import com.atlassian.sal.api.net.Request;
    import com.atlassian.sal.api.net.Response;
    import com.atlassian.sal.api.net.ResponseException;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.w3c.dom.Document;
    import org.w3c.dom.NamedNodeMap;
    import org.w3c.dom.Node;
    import org.w3c.dom.NodeList;
    import org.xml.sax.SAXException;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.xml.parsers.DocumentBuilder;
    import javax.xml.parsers.DocumentBuilderFactory;
    import javax.xml.parsers.ParserConfigurationException;
    import java.io.ByteArrayInputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.net.URI;
    import java.util.ArrayList;
    

    The other import statement can remain, except for the following. You need to remove it since we're using the Crowd package's user:

    1
    2
    import com.opensymphony.user.User;
    
  3. After the line that instantiates the logger, add the following code:

    1
    2
    private final EntityLinkService entityLinkService;
    
    public ConfluenceSpaceTabPanel(EntityLinkService entityLinkService)
    {
       this.entityLinkService = entityLinkService;
    }
    
  4. Notice we're injecting the EntityLinkService into the ConfluenceSpaceTabPanel via the class constructor.

  5. The getActions method determines what appears in our tab. Replace its contents with the following:

    1
    2
    EntityLink entityLink = entityLinkService.getPrimaryEntityLink(issue.getProjectObject(), ConfluenceSpaceEntityType.class);
    if (entityLink == null)
    {
        return Collections.singletonList(new GenericMessageAction("No Link to a Confluence for this JIRA Project configured"));
    } 
    

    This code uses the EntityLinkService to find the Application Link that administrators have configured between the JIRA project and a Confluence space. If it doesn't find a link, it simply prints a message to the tab panel saying so.

  6. Now that you have an EntityLink object, use this to create a request factory to send authenticated HTTP requests to the linked Confluence installation:

    1
    2
    ApplicationLinkRequestFactory requestFactory = entityLink.getApplicationLink().createAuthenticatedRequestFactory(); 
    

    This request factory takes care of authentication to Confluence, using the authentication type configured by the system administrator. With this request factory, you can now make an authenticated request to the Confluence REST API to perform the search.

  7. Initialize a few variables:

    1
    2
    final String query = issue.getKey();
    String confluenceContentType = "page";
    final String spaceKey = entityLink.getKey();
    
  8. Next, add a Java try statement with the contents shown:

    1
    2
    try
       {
          ApplicationLinkRequest request = requestFactory.createRequest(Request.MethodType.GET, "/rest/prototype/1/search?query=" + query + "&spaceKey=" + spaceKey + "&type=" + confluenceContentType);
          String responseBody = request.execute(new ApplicationLinkResponseHandler<String>()
          {
             public String credentialsRequired(final Response response) throws ResponseException
             {
                return response.getResponseBodyAsString();
             }
    
             public String handle(final Response response) throws ResponseException
             {
                return response.getResponseBodyAsString();
             }
          });
    

    This creates a request to the Confluence REST API that looks for the issue's key issue and executes the request, storing the results in the responseBody string.

  9. Now parse the response of the REST GET request, put the results in a list we can present in the issue tab:

    1
    2
    Document document = parseResponse(responseBody);
    NodeList results = document.getDocumentElement().getChildNodes();
    
    List<IssueAction> issueActions = new ArrayList<IssueAction>();
    
    for (int j = 0; j < results.getLength(); j++)
    {
       NodeList links = results.item(j).getChildNodes();
       for (int i = 0; i < links.getLength(); i++)
       {
          Node linkNode = links.item(i);
          if ("link".equals(linkNode.getNodeName()))
          {
             NamedNodeMap attributes = linkNode.getAttributes();
             Node type = attributes.getNamedItem("type");
             if (type != null && "text/html".equals(type.getNodeValue()))
             {
                Node href = attributes.getNamedItem("href");
                URI uriToConfluencePage = URI.create(href.getNodeValue());
                IssueAction searchResult = new GenericMessageAction(String.format("Reference to Issue found in Confluence page <a target=\"_new\" href=%1$s>%1$s</a>", uriToConfluencePage.toString()));
                issueActions.add(searchResult);
             }
          }
       }
    }
    return issueActions;
    
  10. Close the`` try block and add catch statements:

    1
    2
    }
    
    catch (CredentialsRequiredException e)
    {
       final HttpServletRequest req = ExecutingHttpRequest.get();
       URI authorisationURI = e.getAuthorisationURI(URI.create(JiraUrl.constructBaseUrl(req) + "/browse/" + issue.getKey()));
       String message = "You have to authorise this operation first. <a target=\"_new\" href=%s>Please click here and login into the remote application.</a>";
       IssueAction credentialsRequired = new GenericMessageAction(String.format(message, authorisationURI));
       return Collections.singletonList(credentialsRequired);
    }
    
    catch (ResponseException e)
    {
       return Collections.singletonList(new GenericMessageAction("Response exception. Message: " + e.getMessage()));
    }
    
    catch (ParserConfigurationException e)
    {
       return Collections.singletonList(new GenericMessageAction("Failed to read response from Confluence." + e.getMessage()));
    }
    
    catch (SAXException e)
    {
       return Collections.singletonList(new GenericMessageAction("Failed to read response from Confluence." + e.getMessage()));
    }
    
    catch (IOException e)
    {
       return Collections.singletonList(new GenericMessageAction("Failed to read response from Confluence." + e.getMessage()));
    }
    }
    

    Notice the catch statement for CredentialsRequiredException. The request may throw this exception if this request factory attempts to use OAuth authentication. OAuth authentication requires a user (logged in to the local application) to obtain an "access token" from the remote application. This allows the user (of the local application) to make requests to the remote application on behalf of that user. As part of the OAuth authentication process, the user may be prompted to log in to an account on the remote application.

    CredentialsRequiredException is thrown if the user has not yet obtained the required access token. This exception contains a method which returns the authorisation URI through which users can authorise themselves on the remote application (and hence, may require them to log in to that application), to obtain the required access token. You should link the user to this URI so they can be prompted for an access token

  11. Finally, add the parseResponse method we invoked earlier.

    1
    2
    private Document parseResponse(String body) throws ParserConfigurationException, IOException, SAXException
    {
       DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
       DocumentBuilder db = dbf.newDocumentBuilder();
       InputStream is = new ByteArrayInputStream(body.getBytes("UTF-8"));
       return db.parse(is);
    } 
    
  12. Save and close the file. 

Putting it all together, your class should look something like this:

1
2
package com.example.plugins.tutorial.jira.tabpanels;

import com.atlassian.applinks.api.ApplicationLinkRequest;
import com.atlassian.applinks.api.ApplicationLinkRequestFactory;
import com.atlassian.applinks.api.ApplicationLinkResponseHandler;
import com.atlassian.applinks.api.CredentialsRequiredException;
import com.atlassian.applinks.api.EntityLink;
import com.atlassian.applinks.api.EntityLinkService;
import com.atlassian.applinks.api.application.confluence.ConfluenceSpaceEntityType;
import com.atlassian.crowd.embedded.api.User;
import com.atlassian.jira.issue.Issue;
import com.atlassian.jira.issue.tabpanels.GenericMessageAction;
import com.atlassian.jira.plugin.issuetabpanel.AbstractIssueTabPanel;
import com.atlassian.jira.plugin.issuetabpanel.IssueAction;
import com.atlassian.jira.plugin.issuetabpanel.IssueTabPanel;
import com.atlassian.jira.util.http.JiraUrl;
import com.atlassian.jira.web.ExecutingHttpRequest;
import com.atlassian.sal.api.net.Request;
import com.atlassian.sal.api.net.Response;
import com.atlassian.sal.api.net.ResponseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import javax.servlet.http.HttpServletRequest;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class ConfluenceSpaceTabPanel extends AbstractIssueTabPanel implements IssueTabPanel
{
    private static final Logger log = LoggerFactory.getLogger(ConfluenceSpaceTabPanel.class);

    private final EntityLinkService entityLinkService;

    public ConfluenceSpaceTabPanel(EntityLinkService entityLinkService)
    {
        this.entityLinkService = entityLinkService;
    }

    public List getActions(Issue issue, User remoteUser)
    {
        EntityLink entityLink = entityLinkService.getPrimaryEntityLink(issue.getProjectObject(), ConfluenceSpaceEntityType.class);
        if (entityLink == null)
        {
            return Collections.singletonList(new GenericMessageAction("No Link to a Confluence for this JIRA Project configured"));
        }
        ApplicationLinkRequestFactory requestFactory = entityLink.getApplicationLink().createAuthenticatedRequestFactory();

        final String query = issue.getKey();
        String confluenceContentType = "page";
        final String spaceKey = entityLink.getKey();

        try
        {
            ApplicationLinkRequest request = requestFactory.createRequest(Request.MethodType.GET, "/rest/prototype/1/search?query=" + query + "&spaceKey=" + spaceKey + "&type=" + confluenceContentType);
            String responseBody = request.execute(new ApplicationLinkResponseHandler<String>()
            {
                public String credentialsRequired(final Response response) throws ResponseException
                {
                    return response.getResponseBodyAsString();
                }

                public String handle(final Response response) throws ResponseException
                {
                    return response.getResponseBodyAsString();
                }
            });

            Document document = parseResponse(responseBody);
            NodeList results = document.getDocumentElement().getChildNodes();

            List<IssueAction> issueActions = new ArrayList<IssueAction>();

            for (int j = 0; j < results.getLength(); j++)
            {
                NodeList links = results.item(j).getChildNodes();
                for (int i = 0; i < links.getLength(); i++)
                {
                    Node linkNode = links.item(i);
                    if ("link".equals(linkNode.getNodeName()))
                    {
                        NamedNodeMap attributes = linkNode.getAttributes();
                        Node type = attributes.getNamedItem("type");
                        if (type != null && "text/html".equals(type.getNodeValue()))
                        {
                            Node href = attributes.getNamedItem("href");
                            URI uriToConfluencePage = URI.create(href.getNodeValue());

                            IssueAction searchResult = new GenericMessageAction(String.format("Reference to Issue found in Confluence page <a target=\"_new\" href=%1$s>%1$s</a>", uriToConfluencePage.toString()));
                            issueActions.add(searchResult);
                        }
                    }
                }
            }
            return issueActions;
        }
        catch (CredentialsRequiredException e)
        {
            final HttpServletRequest req = ExecutingHttpRequest.get();
            URI authorisationURI = e.getAuthorisationURI(URI.create(JiraUrl.constructBaseUrl(req) + "/browse/" + issue.getKey()));
            String message = "You have to authorise this operation first. <a target=\"_new\" href=%s>Please click here and login into the remote application.</a>";
            IssueAction credentialsRequired = new GenericMessageAction(String.format(message, authorisationURI));
            return Collections.singletonList(credentialsRequired);
        }
        catch (ResponseException e)
        {
            return Collections.singletonList(new GenericMessageAction("Response exception. Message: " + e.getMessage()));
        }
        catch (ParserConfigurationException e)
        {
            return Collections.singletonList(new GenericMessageAction("Failed to read response from Confluence." + e.getMessage()));
        }
        catch (SAXException e)
        {
            return Collections.singletonList(new GenericMessageAction("Failed to read response from Confluence." + e.getMessage()));
        }
        catch (IOException e)
        {
            return Collections.singletonList(new GenericMessageAction("Failed to read response from Confluence." + e.getMessage()));
        }
    }

    public boolean showPanel(Issue issue, User remoteUser)
    {
        return true;
    }

    private Document parseResponse(String body) throws ParserConfigurationException, IOException, SAXException
    {
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        DocumentBuilder db = dbf.newDocumentBuilder();
        InputStream is = new ByteArrayInputStream(body.getBytes("UTF-8"));
        return db.parse(is);
    }
}

For additional information and TODO's, see the code comments in the file on BitBucket.

Step 6. Build, install and run the plugin

Now start JIRA with the plugin:

  1. Back at the command line, change to the project home directory.
  2. Enter atlas-run.
  3. Open your browser and open the application started by atlas-run, for example, by navigating to http://localhost:2990/jira.
  4. At the login screen, enter the username of admin and a password of admin.
  5. Follow the wizard to create a new project.
  6. Keep this window open while you perform the next steps.

Next configure the Confluence instance against which you'll test your plugin as described below.  Notice that these steps include setting up an Application Link and a project link. The steps are not described in detail here. If you're new to Atlassian administration, you should see the JIRA documentation for details on how to accomplish these tasks.

  1. Start the Confluence instance against which you'll test your plugin:

    1
    2
    atlas-run-standalone --product confluence
    
  2. When Confluence finishes starting up, return to the browser window with JIRA opened, and navigate to the administration console.

  3. In the JIRA administration console, click the Applications Links link in the left menu.

  4. Follow the instructions in the wizard to set up a reciprocal Application Link between JIRA and your Confluence instance.

  5. Return to the Administration page of your newly created project and set up a project link between it and the built-in Demonstration space in Confluence.

  6. Create a new issue in your JIRA project.

  7. In the Demonstration space in Confluence, either create a new page or edit an existing one and add the issue key to the page. You can do this in the form of the JIRA issue macro.

  8. Save the page.

  9. Back in JIRA, view the issue you created and click the Confluence Space Tab Panel tab. You should see the title of the Confluence page in which you mentioned the issue. If you do not see your Confluence page, try re-indexing Confluence first and then try viewing the issue again.

Congratulations, that's it

Have a chocolate!

Rate this page: