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. |
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:
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.
To complete this tutorial, you need to know the following:
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 2git 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
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.
If you have not already set up the Atlassian Plugin SDK, do that now: Set up the Atlassian Plugin SDK and Build a Project.
Open a terminal and navigate to the directory in which you want to create the plugin project.
Enter the following command to create a plugin skeleton:
1 2atlas-create-jira-plugin
Choose the option to create a plugin for JIRA 5.0.
As prompted, enter the following information for your plugin:
group-id |
|
artifact-id |
|
version |
|
package |
|
Confirm your entries when prompted.
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.
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.
In the command window, switch to the new directory created for your project, tutorial-jira-ual.
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>
Update the <description>
element:
1 2<description>This is the UAL plugin tutorial for Atlassian JIRA.</description>
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>
Save the file.
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:
pom.xml
is located).atlas-create-jira-plugin-module
.ConfluenceSpaceTabPanel
com.example.plugins.tutorial.jira.tabpanels
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 |
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 |
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 |
Now adjust the plugin descriptor file, atlassian-plugin.xml
, to import the Application Links component (EntityLinkService
), as follows:
In the command window, change to the src/main/resources directory.
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.
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:
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.
First, add some import statments to the ones that the SDK gave us:
1 2import 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 2import com.opensymphony.user.User;
After the line that instantiates the logger, add the following code:
1 2private final EntityLinkService entityLinkService; public ConfluenceSpaceTabPanel(EntityLinkService entityLinkService) { this.entityLinkService = entityLinkService; }
Notice we're injecting the EntityLinkService
into the ConfluenceSpaceTabPanel
via the class constructor.
The getActions
method determines what appears in our tab. Replace its contents with the following:
1 2EntityLink 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.
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 2ApplicationLinkRequestFactory 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.
Initialize a few variables:
1 2final String query = issue.getKey(); String confluenceContentType = "page"; final String spaceKey = entityLink.getKey();
Next, add a Java try
statement with the contents shown:
1 2try { 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.
Now parse the response of the REST GET request, put the results in a list we can present in the issue tab:
1 2Document 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;
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
Finally, add the parseResponse
method we invoked earlier.
1 2private 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); }
Save and close the file.
Putting it all together, your class should look something like this:
1 2package 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.
Now start JIRA with the plugin:
atlas-run
.atlas-run
, for example, by navigating to http://localhost:2990/jira.admin
and a password of admin
.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.
Start the Confluence instance against which you'll test your plugin:
1 2atlas-run-standalone --product confluence
When Confluence finishes starting up, return to the browser window with JIRA opened, and navigate to the administration console.
In the JIRA administration console, click the Applications Links link in the left menu.
Follow the instructions in the wizard to set up a reciprocal Application Link between JIRA and your Confluence instance.
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.
Create a new issue in your JIRA project.
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.
Save the page.
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: