Applicable: | 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 estimate: | It should take you approximately half an hour to complete this tutorial. |
This tutorial shows you how to add a JQL function to Jira. You will then be able to use the function in the advanced search form to find issues only in projects you have recently accessed. In the real world, a user would likely use this function combined with another search clause. This would be useful in systems that have many projects, and where the users typically only care only about a few.
How are functions used in JQL queries? A JQL query is made up of one or more clauses. Each clause consists of a field,
operator, and operand. For example, assignee = fred
where:
assignee
.=
.fred
.In this case the operand is a literal string fred
. But it can also be a function. Jira comes with many
built-in functions.
And you can add them, as we'll do here.
In this tutorial, you will create a JQL function app consisting of these 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 IntelliJ IDEA 2017.3 on macOS Sierra. If you use a different 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, AMPS 6.3.15, and Atlassian SDK version 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 finished, 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/jira-simple-jql-function.git
Alternatively, you can download the source as a ZIP archive.
In this step, you'll use an atlas-
command to generate stub code for your app and set up the stub code. 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 have not done it yet.
To create an app skeleton, run the following command:
1 2atlas-create-jira-plugin
To identify your app, enter the following information.
group-id |
|
artifact-id |
|
version |
|
package |
|
Confirm your entries when prompted.
The SDK generates the initial app project files in the jira-simple-jql-function
directory.
It is a good idea to familiarize yourself with the project configuration file, known as the POM (that is, Project Object
Model definition file). In this step, you will review and tweak the pom.xml
file. The file declares the
project dependencies and other information.
Navigate to the new jira-simple-jql-function
directory and open the pom.xml
file.
Add your company or organization name and your website URL to the organization
element
(the following code block shows how it looks in plain text):
1 2<organization> <name>Example Company</name> <url>http://www.example.com/</url> </organization>
Update the description
element:
1 2<description>Adds a custom JQL function named recentProjects to JIRA.</description>
Save and close the file.
Now you will use the plugin module generator (that is, another atlas-
command) to generate the stub code for
modules required by the app.
For this tutorial, you need a JQL Function plugin module. You'll add it using
the atlas-create-HOSTAPP-plugin-module
command.
Navigate to the app root folder where the pom.xml
is located and run the following command:
1 2atlas-create-jira-plugin-module
Select the JQL Function
option.
Enter the following information when prompted.
Enter New Classname |
|
Package Name |
|
Select N
for Show Advanced Setup.
Select N
for Add Another Plugin Module.
Confirm your selection.
The SDK added a JQL Function module to our app descriptor, which describes the app to Jira. Let's tweak the module declaration it added.
Navigate to src/main/resources/
and open the atlassian-plugin.xml
file.
Find the jql-function
element, and then add fname
and list
elements after the description.
1 2<jql-function name="Recent Project Function" i18n-name-key="recent-project-function.name" key="recent-project-function" class="com.example.plugins.tutorial.jira.jql.RecentProjectFunction"> <description key="recent-project-function.description">The Recent Project Function Plugin</description> <fname>recentProjects</fname> <list>true</list> </jql-function>
The fname
represents the name for our function as it will be used in JQL statements. The list
indicates
whether this function returns a list of issues or a single value.
Save and close the file.
The SDK gave us the stub code for our class. In this step, we add the logic for our function.
Navigate to src/main/java/com/example/plugins/tutorial/jira/jql/
and open the RecentProjectFunction.java
file.
Replace import com.opensymphony.user.User;
with the following import statement:
1 2import com.atlassian.jira.user.ApplicationUser;
Update validate()
method to use ApplicationUser
instead of User
1 2public MessageSet validate(ApplicationUser searcher, FunctionOperand operand, TerminalClause terminalClause) { return validateNumberOfArgs(operand, 1); }
Add the following import statements:
1 2import com.atlassian.plugin.spring.scanner.annotation.component.Scanned; import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport; import com.atlassian.jira.user.UserHistoryItem; import com.atlassian.jira.user.UserProjectHistoryManager; import java.util.LinkedList;
Before the class definition, add a @Scanned
annotation:
1 2@Scanned public class RecentProjectFunction extends AbstractJqlFunction { ...
To determine what projects the current user has recently accessed, we can use the UserProjectHistoryManager
that Jira gives us. Inject it into a constructor for our recentProjectFunction
class as follows:
1 2@ComponentImport private final UserProjectHistoryManager userProjectHistoryManager; public RecentProjectFunction(UserProjectHistoryManager userProjectHistoryManager) { this.userProjectHistoryManager = userProjectHistoryManager; }
Our function doesn't take any arguments yet. In the validate()
method,
change the number of arguments from 1 to 0:
1 2return validateNumberOfArgs(operand, 0);
The getValues()
function is where most of the work happens. Replace the one that the SDK gave us with this one:
1 2public List<QueryLiteral> getValues(QueryCreationContext queryCreationContext, FunctionOperand operand, TerminalClause terminalClause) { final List<QueryLiteral> literals = new LinkedList<>(); final List<UserHistoryItem> projects = userProjectHistoryManager.getProjectHistoryWithoutPermissionChecks(queryCreationContext.getApplicationUser()); for (final UserHistoryItem userHistoryItem : projects) { final String value = userHistoryItem.getEntityId(); try { literals.add(new QueryLiteral(operand, Long.parseLong(value))); } catch (NumberFormatException e) { log.warn(String.format("User history returned a non numeric project IS '%s'.", value)); } } return literals; }
The function returns a list of QueryLiterals
that represent the list of IDs of projects recently visited, as offered
by the userProjectHistoryManager
, and populates a linked list with the results converted to QueryLiterals
. Any user
can use this function, so we use getProjectHistoryWithoutPermissionChecks()
instead.
Alternatively, we can use
getProjectHistoryWithPermissionChecks()
that performs a permission check based on the permissions that the user
must have for the project.
In getMinimumNumberOfExpectedArguments()
, change the return value to 0:
1 2public int getMinimumNumberOfExpectedArguments() { return 0; }
In getDataType()
, change the data type returned from TEXT
to PROJECT
, because we return only a list of projects.
1 2return JiraDataTypes.PROJECT;
The entire class should look something like this:
1 2package com.example.plugins.tutorial.jira.jql; import com.atlassian.jira.user.ApplicationUser; import com.atlassian.jira.user.UserHistoryItem; import com.atlassian.jira.user.UserProjectHistoryManager; import com.atlassian.plugin.spring.scanner.annotation.component.Scanned; import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.atlassian.jira.JiraDataType; import com.atlassian.jira.JiraDataTypes; import com.atlassian.jira.jql.operand.QueryLiteral; import com.atlassian.jira.jql.query.QueryCreationContext; import com.atlassian.jira.plugin.jql.function.AbstractJqlFunction; import com.atlassian.jira.util.MessageSet; import com.atlassian.query.clause.TerminalClause; import com.atlassian.query.operand.FunctionOperand; import java.util.LinkedList; import java.util.List; @Scanned public class RecentProjectFunction extends AbstractJqlFunction { private static final Logger log = LoggerFactory.getLogger(RecentProjectFunction.class); @ComponentImport private final UserProjectHistoryManager userProjectHistoryManager; public RecentProjectFunction(UserProjectHistoryManager userProjectHistoryManager) { this.userProjectHistoryManager = userProjectHistoryManager; } public MessageSet validate(ApplicationUser searcher, FunctionOperand operand, TerminalClause terminalClause) { return validateNumberOfArgs(operand, 0); } public List<QueryLiteral> getValues(QueryCreationContext queryCreationContext, FunctionOperand operand, TerminalClause terminalClause) { final List<QueryLiteral> literals = new LinkedList<>(); final List<UserHistoryItem> projects = userProjectHistoryManager.getProjectHistoryWithoutPermissionChecks(queryCreationContext.getApplicationUser()); for (final UserHistoryItem userHistoryItem : projects) { final String value = userHistoryItem.getEntityId(); try { literals.add(new QueryLiteral(operand, Long.parseLong(value))); } catch (NumberFormatException e) { log.warn(String.format("User history returned a non numeric project IS '%s'.", value)); } } return literals; } public int getMinimumNumberOfExpectedArguments() { return 0; } public JiraDataType getDataType() { return JiraDataTypes.PROJECT; } }
The SDK was helpful enough to give us unit and integration test stub files for our app code. However, these are really meant to be starting points for your testing coverage, so they require more attention to be useful. Testing is a big topic, so we leave that for another tutorial called Writing and running app tests.
For now, just remove the tests so that we can check the app without modifying them.
1 2rm -rf src/main/test
We're ready to give the app a try.
atlas-run
command
(or atlas-debug
if you want to launch the debugger in your IDE).project in recentProjects()
To extend your JQL function, you can make it accept a parameter. Also, functions typically need to check permissions in Jira, so that they only return projects to which the user has access. In our case, because we use the recent history function, we already know that the user can access the returned projects. For more information on these topics, see JQL function plugin module.
For more information on custom JQL functions, see:
Rate this page: