Applicable: | This tutorial applies to Jira 7.0.0 and later. |
Level of experience: | Intermediate. You should have completed at least one beginner tutorial before working through this tutorial. See the list of developer tutorials. |
Time to Complete: | It should take you approximately 1 hour to complete this tutorial. |
This tutorial takes you through the steps for developing an app that can perform CRUD operations in a Jira project. In the tutorial, you will create a servlet that presents a page in Jira where users can:
In addition to CRUD operations, this tutorial demonstrates how to use a servlet module to perform a simple issue listing
with the IssueService
and SearchService
interfaces.
The completed app will consist of the following components:
When you are finished, all these components will be packaged in a single JAR file.
About these instructions
You can use any supported combination of operating system and IDE to create this app.These instructions were written using macOS Sierra and IntelliJ IDEA 2017.3. If you use another operating system or IDE combination, you should use the equivalent operations for your specific environment.
This tutorial was last tested with Jira 7.7.1 using Atlassian Plugin SDK 6.3.10.
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 are done, you can find the app source code on Atlassian Bitbucket.
To clone the repository, run the following command:
1 2git clone https://bitbucket.org/atlassian_tutorial/tutorial-jira-simple-issue-crud
Alternatively, you can download the source as ZIP archive.
In this step, you'll use two atlas-
commands to generate stub code for your app. The atlas-
commands are part of
the Atlassian Plugin SDK and automate much of the work of app development for you.
Set up the Atlassian Plugin SDK and build a project if you did not do that yet.
Open a Terminal and navigate to the directory where you would like to keep the app code.
To create the initial project files and source code for a Jira app, run the following command:
1 2atlas-create-jira-plugin
To identify the app, enter the following information.
group-id |
|
artifact-id |
|
version |
|
package |
|
Confirm your entries when prompted.
Navigate to the directory created by SDK.
1 2cd tutorial-jira-simple-issue-crud
Remove auto generated test directories.
1 2rm -rf ./src/test/java rm -rf ./src/test/resources/
Delete the unneeded Java class files.
1 2rm -rf ./src/main/java/com/example/plugins/tutorial/*
Import the project to your favorite IDE.
It is a good idea to familiarize yourself with the stub app code. In this step, we'll check the version value and tweak the generated stub class.
The POM (that is, Project Object Model definition file) is located at the root of your project and declares the project dependencies and other information.
In this step you'll add some metadata about your app and your company or organization to the POM.
Navigate to the root folder and open the pom.xml
file.
Add your company or organization name and your website URL 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 plugin demonstrates how to perform basic CRUD operations on JIRA Issues using the IssueService and SearchService interface through a servlet module.</description>
Save the file.
Your stub code contains an app descriptor file atlassian-plugin.xml
. This is an XML file that identifies the app
to the host application (that is, to Jira) and defines the required app functionality.
src/main/resources
and open the descriptor file.You should see something like this (comments removed):
1 2<?xml version="1.0" encoding="UTF-8"?> <atlassian-plugin key="${atlassian.plugin.key}" name="${project.name}" plugins-version="2"> <plugin-info> <description>${project.description}</description> <version>${project.version}</version> <vendor name="${project.organization.name}" url="${project.organization.url}"/> <param name="plugin-icon">images/pluginIcon.png</param> <param name="plugin-logo">images/pluginLogo.png</param> </plugin-info> <resource type="i18n" name="i18n" location="tutorial-jira-simple-issue-crud"/> <web-resource key="tutorial-jira-simple-issue-crud-resources" name="tutorial-jira-simple-issue-crud Web Resources"> <dependency>com.atlassian.auiplugin:ajs</dependency> <resource type="download" name="tutorial-jira-simple-issue-crud.css" location="/css/tutorial-jira-simple-issue-crud.css"/> <resource type="download" name="tutorial-jira-simple-issue-crud.js" location="/js/tutorial-jira-simple-issue-crud.js"/> <resource type="download" name="images/" location="/images"/> <context>tutorial-jira-simple-issue-crud</context> </web-resource> </atlassian-plugin>
In later steps, we'll use the plugin module generator (that is, another atlas-
command) to generate the stub
code for additional modules required by the app.
Open a Terminal window and navigate to the app root folder where the pom.xml
is located.
Run the following command:
1 2atlas-create-jira-plugin-module
Select the Servlet
option.
Add the following information when prompted.
New Classname |
|
Package Name |
|
Show Advanced Setup |
|
Select N
for Add Another Plugin Module.
The generator added the following elements to your app:
IssueCRUD
Java class.servlet
module in your app descriptor.If you open the atlassian-plugin.xml
file in your IDE, you will see the following module
information added by the generator:
1 2<servlet name="Issue CRUD" i18n-name-key="issue-crud.name" key="issue-crud" class="com.example.plugins.tutorial.servlet.IssueCRUD"> <description key="issue-crud.description">The Issue CRUD Plugin</description> <url-pattern>/issuecrud</url-pattern> </servlet>
If you open the pom.xml
file in your IDE, you will see the following new entries in the dependencies
section:
1 2<dependencies> ... <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>2.4</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.6.6</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.1.1</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-all</artifactId> <version>1.8.5</version> <scope>test</scope> </dependency> ... <dependencies>
By default, the servlet module is not preconfigured to use Velocity templates (Atlassian's preferred template engine
for servlets). Let's set up Velocity so that we don't write HTML inside our servlet code. We'll use Atlassian Spring Scanner
to import the TemplateRenderer
which imports the Velocity template renderer. To add this component, do the following:
In the root of your project, open the pom.xml
file.
Find the dependencies
section and insert the following:
1 2<dependency> <groupId>com.atlassian.templaterenderer</groupId> <artifactId>atlassian-template-renderer-api</artifactId> <version>2.0.0</version> <scope>provided</scope> </dependency>
Save your file.
At this point, you haven't actually written any Java code. You can, however, run Jira and see your app with its server in action. In this step, you will start Jira, create a project you'll use later, and test the servlet.
Make sure you have saved all your code changes to this point.
Open a Terminal window and navigate to the app root folder where the pom.xml
file is located.
Run the following command:
1 2atlas-run
This command builds your app code, starts a Jira instance, and installs your app in it. This may take several minutes or so. When the process completes you will see many status lines on your screen concluding with something like the following:
1 2[INFO] jira started successfully in 71s at http://localhost:2990/jira [INFO] Type CTRL-D to shutdown gracefully [INFO] Type CTRL-C to exit
Notice the URL for the Jira instance.
Go to the local Jira instance in your browser (the URL is indicated in the Terminal output).
Log in using default admin/admin.
The first time you start a Jira instance, the New Project wizard appears.
Create a new blank project called "TUTORIAL" with key "TUTORIAL". We'll need it for later.
To keep the focus on coding IssueCRUD
, we rely on hard coded project identifiers in our app.
It's important that the Jira project has the name and key "TUTORIAL".
If the new project wizard doesn't appear, make sure to create a project with these values.
To open your servlet, go to localhost:2990/jira/plugins/servlet/issuecrud
in your browser.
You should see a "Hello World" message in the browser window. To see where the /issuecrud
path is specified,
navigate to src/main/resources
, open the atlassian-plugin.xml
file and look for the servlet
module. It includes a
<url-pattern>
element that identifies this path.
Leave Jira running in browser.
We'll need Velocity templates to create, edit, and list issues.
Open a new Terminal window and navigate to src/main/resources
.
Create a subdirectory named templates
.
Create a new file named edit.vm
, then add the following content:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55<html> <head> <title>Edit Issue — Issue CRUD Tutorial</title> <meta name="decorator" content="atl.general"> </head> <body class="page-type-admin"> <div class="content-container"> <div class="content-body"> <h1>Edit issue $issue.getKey()</h1> #if ($errors.size()>0) <div class="aui-message error shadowed"> #foreach($error in $errors) <p class="title"> <span class="aui-icon icon-error"></span> <strong>$error</strong> </p> #end </div> <!-- .aui-message --> #end <div class="create-issue-panel"> <form method="post" id="h" action="issuecrud" class="aui"> <input type="hidden" name="edit" value="y"> <input type="hidden" name="key" value="$issue.getKey()"> <div class="field-group"> <label for="h-fsummary"> Summary <span class="aui-icon icon-required"></span> <span class="content">required</span> </label> <input id="h-fsummary" class="text long-field" type="text" name="summary" value="$issue.getSummary()"> </div> <div class="field-group"> <label for="h-fdescription"> Description <span class="aui-icon icon-required"></span> <span class="content">required</span> </label> <textarea id="h-fdescription" name="description">$issue.getDescription()</textarea> </div> <div class="buttons"> <input class="button" type="submit" value="Update"> <a href="issuecrud">Cancel</a> </div> </form> </div> </div> </div> </body> </html>
Create a file named list.vm
, then add the following content:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91<html> <head> <title>All Tutorial Issues — Issue CRUD Tutorial</title> <meta name="decorator" content="atl.general"> <script> AJS.$(document).ready(function() { jQuery('.delete-issue').click(function() { console.log('deleting'); var self = jQuery(this); jQuery.ajax({ type: "delete", url: "issuecrud?key=" + self.data("key"), success: function(data) { console.log('dom', self, data); self.parent().parent().remove(); }, error: function() { console.log('error', arguments); } }); return false; }); }); </script> </head> <body class="page-type-admin"> <div class="content-container"> <div class="content-body"> <h1>You've Got #if($issues.size()==0)<span style="color:red">NO</span>#end Issues!</h1> #if ($errors.size()>0) <div class="aui-message error shadowed"> #foreach($error in $errors) <p class="title"> <span class="aui-icon icon-error"></span> <strong>$error</strong> </p> #end </div> <!-- .aui-message --> #end #if ($issues.size() > 0) <div class="issues"> <table class="aui"> <thead> <tr> <th>Key</th> <th>Summary</th> <th>Description</th> <th>Assignee</th> <th>Reporter</th> <th></th> </tr> </thead> <tbody> #foreach( $issue in $issues ) <tr> <td>$issue.getKey()</td> <td>$issue.getSummary()</td> <td> #if($issue.getDescription()) $issue.getDescription() #end </td> <td> $issue.getAssignee().getName() </td> <td> $issue.getReporter().getName() </td> <td> <a href="issuecrud?edit=y&key=$issue.getKey()">Edit</a> <a href="#" class="delete-issue" data-key="$issue.getKey()">Delete</a> </td> </tr> #end </tbody> </table> </div> #end <form method="get" action="issuecrud" class="aui"> <input type="hidden" name="new" value="y"> <input type="submit" class="button" value="Create new issue"> </form> </div> </div> </body> </html>
Create a file named new.vm
, then add the following content:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41<html> <head> <title>Create Issue — Issue CRUD Tutorial</title> <meta name="decorator" content="atl.general"> </head> <body class="page-type-admin"> <div class="content-container"> <div class="content-body"> <h1>Create issue</h1> <div class="create-issue-panel"> <form method="post" id="h" action="issuecrud" class="aui"> <div class="field-group"> <label for="h-fsummary"> Summary <span class="aui-icon icon-required"></span> <span class="content">required</span> </label> <input id="h-fsummary" class="text long-field" type="text" name="summary"> </div> <div class="field-group"> <label for="h-fdescription"> Description <span class="aui-icon icon-required"></span> <span class="content">required</span> </label> <textarea id="h-fdescription" name="description"></textarea> </div> <div class="buttons"> <input class="button" type="submit" value="Create"> <a href="issuecrud">Cancel</a> </div> </form> </div> </div> </div> </body> </html>
This is how we will use the files:
list.vm
to render a list of available issues.edit.vm
to edit selected issue.new.vm
to render a simple issue creation form.Now you are ready to write Java code.
In this step, you'll update the servlet code so that it displays something more interesting than "Hello World".
All work in this section will be in the IssueCRUD.java
file that is located here:
/src/main/java/com/example/plugins/tutorial/servlet/
Let's configure our basic servlet.
Open the IssueCRUD.java
file.
Replace the existing imports section (without disturbing the package definition) so that it looks like this:
1 2import com.atlassian.jira.bc.issue.IssueService; import com.atlassian.jira.bc.issue.search.SearchService; import com.atlassian.jira.bc.project.ProjectService; import com.atlassian.jira.config.ConstantsManager; import com.atlassian.jira.issue.Issue; import com.atlassian.jira.issue.IssueInputParameters; import com.atlassian.jira.issue.MutableIssue; import com.atlassian.jira.issue.issuetype.IssueType; import com.atlassian.jira.issue.search.SearchException; import com.atlassian.jira.issue.search.SearchResults; import com.atlassian.jira.web.bean.PagerFilter; import com.atlassian.plugin.spring.scanner.annotation.component.Scanned; import com.atlassian.plugin.spring.scanner.annotation.imports.JiraImport; import com.atlassian.query.Query; import com.atlassian.templaterenderer.TemplateRenderer; import com.atlassian.jira.jql.builder.JqlClauseBuilder; import com.atlassian.jira.jql.builder.JqlQueryBuilder; import com.atlassian.jira.project.Project; import com.atlassian.jira.security.JiraAuthenticationContext; import com.atlassian.jira.user.ApplicationUser; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional;
Put a @Scanned
annotation on IssueCRUD
class and create a constructor.
1 2@Scanned public class IssueCRUD extends HttpServlet { private static final Logger log = LoggerFactory.getLogger(IssueCRUD.class); @JiraImport private IssueService issueService; @JiraImport private ProjectService projectService; @JiraImport private SearchService searchService; @JiraImport private TemplateRenderer templateRenderer; @JiraImport private JiraAuthenticationContext authenticationContext; @JiraImport private ConstantsManager constantsManager; private static final String LIST_ISSUES_TEMPLATE = "/templates/list.vm"; private static final String NEW_ISSUE_TEMPLATE = "/templates/new.vm"; private static final String EDIT_ISSUE_TEMPLATE = "/templates/edit.vm"; public IssueCRUD(IssueService issueService, ProjectService projectService, SearchService searchService, TemplateRenderer templateRenderer, JiraAuthenticationContext authenticationContext, ConstantsManager constantsManager) { this.issueService = issueService; this.projectService = projectService; this.searchService = searchService; this.templateRenderer = templateRenderer; this.authenticationContext = authenticationContext; this.constantsManager = constantsManager; }
The @JiraImport
annotations instruct Atlassian Spring Scanner to import specified interface Atlassian services from host
application and inject them into our servlet object. @Scanned
annotation is used to mark class for Atlassian Spring Scanner.
After wiring the Jira API services that the app needs, we can start working on our request handlers.
Replace the existing doGet
method.
This new method generates a page that, depending on the user action, lists all issues, creates new issues,
and updates existing issues.
1 2@Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { String action = Optional.ofNullable(req.getParameter("actionType")).orElse(""); Map<String, Object> context = new HashMap<>(); resp.setContentType("text/html;charset=utf-8"); switch (action) { case "new": templateRenderer.render(NEW_ISSUE_TEMPLATE, context, resp.getWriter()); break; case "edit": IssueService.IssueResult issueResult = issueService.getIssue(authenticationContext.getLoggedInUser(), req.getParameter("key")); context.put("issue", issueResult.getIssue()); templateRenderer.render(EDIT_ISSUE_TEMPLATE, context, resp.getWriter()); break; default: List<Issue> issues = getIssues(); context.put("issues", issues); templateRenderer.render(LIST_ISSUES_TEMPLATE, context, resp.getWriter()); } }
If you look closely, accessing the issue is done using the IssueService
. We'll also need a method for creating
the list of Jiras.
Add the getIssues
method that will look for issues belonging to the "TUTORIAL" project.
1 2private List<Issue> getIssues() { ApplicationUser user = authenticationContext.getLoggedInUser(); JqlClauseBuilder jqlClauseBuilder = JqlQueryBuilder.newClauseBuilder(); Query query = jqlClauseBuilder.project("TUTORIAL").buildQuery(); PagerFilter pagerFilter = PagerFilter.getUnlimitedFilter(); SearchResults searchResults = null; try { searchResults = searchService.search(user, query, pagerFilter); } catch (SearchException e) { e.printStackTrace(); } return searchResults != null ? searchResults.getIssues() : null; }
If we want to get a list of issues for our project, we access the SearchService
with a specified JQL clause.
The request parameter that the servlet receives determines which page to render:
actionType
parameter is edit
, the servlet renders the edit.vm
template.actionType
parameter is new
, the servlet renders the new.vm
template.list.vm
template.Now that we can render the GET request, let's focus on creating and updating of an issue. This happens in a
POST request and is handled in our code by the doPost
method. We will also create handleIssueEdit
and handleIssueCreation
methods to separate logic. Notice that the code assumes a hard coded "TUTORIAL" project key.
1 2@Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String actionType = req.getParameter("actionType"); switch (actionType) { case "edit": handleIssueEdit(req, resp); break; case "new": handleIssueCreation(req, resp); break; default: resp.sendError(HttpServletResponse.SC_NOT_FOUND); } } private void handleIssueEdit(HttpServletRequest req, HttpServletResponse resp) throws IOException { ApplicationUser user = authenticationContext.getLoggedInUser(); Map<String, Object> context = new HashMap<>(); IssueInputParameters issueInputParameters = issueService.newIssueInputParameters(); issueInputParameters.setSummary(req.getParameter("summary")) .setDescription(req.getParameter("description")); MutableIssue issue = issueService.getIssue(user, req.getParameter("key")).getIssue(); IssueService.UpdateValidationResult result = issueService.validateUpdate(user, issue.getId(), issueInputParameters); if (result.getErrorCollection().hasAnyErrors()) { context.put("issue", issue); context.put("errors", result.getErrorCollection().getErrors()); resp.setContentType("text/html;charset=utf-8"); templateRenderer.render(EDIT_ISSUE_TEMPLATE, context, resp.getWriter()); } else { issueService.update(user, result); resp.sendRedirect("issuecrud"); } } private void handleIssueCreation(HttpServletRequest req, HttpServletResponse resp) throws IOException { ApplicationUser user = authenticationContext.getLoggedInUser(); Map<String, Object> context = new HashMap<>(); Project project = projectService.getProjectByKey(user, "TUTORIAL").getProject(); if (project == null) { context.put("errors", Collections.singletonList("Project doesn't exist")); templateRenderer.render(LIST_ISSUES_TEMPLATE, context, resp.getWriter()); return; } IssueType taskIssueType = constantsManager.getAllIssueTypeObjects().stream().filter( issueType -> issueType.getName().equalsIgnoreCase("task")).findFirst().orElse(null); if(taskIssueType == null) { context.put("errors", Collections.singletonList("Can't find Task issue type")); templateRenderer.render(LIST_ISSUES_TEMPLATE, context, resp.getWriter()); return; } IssueInputParameters issueInputParameters = issueService.newIssueInputParameters(); issueInputParameters.setSummary(req.getParameter("summary")) .setDescription(req.getParameter("description")) .setAssigneeId(user.getName()) .setReporterId(user.getName()) .setProjectId(project.getId()) .setIssueTypeId(taskIssueType.getId()); IssueService.CreateValidationResult result = issueService.validateCreate(user, issueInputParameters); if (result.getErrorCollection().hasAnyErrors()) { List<Issue> issues = getIssues(); context.put("issues", issues); context.put("errors", result.getErrorCollection().getErrors()); resp.setContentType("text/html;charset=utf-8"); templateRenderer.render(LIST_ISSUES_TEMPLATE, context, resp.getWriter()); } else { issueService.create(user, result); resp.sendRedirect("issuecrud"); } }
Just as we did in the doGet
code, in the doPost
we separate a creation from an update by checking for a request
parameter actionType
.
Creating and updating an issue requires that you perform a validation step. For creation, you must use the validateCreate
method of the IssueService
. The validation step will return an error collection if there are errors with
the validation results. In this case, we will pass those into the template context so that Velocity can render it in the HTML.
We can delete issues using our app.
1 2@Override protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws IOException { ApplicationUser user = authenticationContext.getLoggedInUser(); String respStr; IssueService.IssueResult issueResult = issueService.getIssue(user, req.getParameter("key")); if (issueResult.isValid()) { IssueService.DeleteValidationResult result = issueService.validateDelete(user, issueResult.getIssue().getId()); if (result.getErrorCollection().hasAnyErrors()) { respStr = "{ \"success\": \"false\", error: \"" + result.getErrorCollection().getErrors().get(0) + "\" }"; } else { issueService.delete(user, result); respStr = "{ \"success\" : \"true\" }"; } } else { respStr = "{ \"success\" : \"false\", error: \"Couldn't find issue\"}"; } resp.setContentType("application/json;charset=utf-8"); resp.getWriter().write(respStr); }
Now you are ready to test your cool new servlet for creating issues.
Make sure you have saved all your code changes to this point.
Open a Terminal window and navigate to the app root folder where the pom.xml
file is located.
If you left Jira running, use atlas-package
command to trigger QuickReload.
In case you closed Jira, use atlas-run
command to run it.
In your browser, go to the local Jira instance.
Log in with the default admin/admin.
Go to your servlet page: localhost:2990/jira/plugins/servlet/issuecrud.
Click Create new issue.
Enter some test data to Summary and Description fields.
Click Create.
The servlet page appears again, this time with your new issue listed.
Congratulations, that's it!
Have a treat!
Rate this page: