Last updated Oct 9, 2024

Adding a JQL function to Jira

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.

Overview of the 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:

  • Field is assignee.
  • Operator is =.
  • Operand is 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:

  1. Java classes encapsulating the app logic.
  2. An app descriptor to enable the plugin module in Jira.

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.

Before you begin

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

  1. The basics of Java development: classes, interfaces, methods, how to use the compiler, and so on.
  2. How to create an Atlassian plugin project using the Atlassian Plugin SDK.
  3. The basics of using and administering Jira, as well as how to use JQL Advanced Searching.

App source

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
2
git clone https://bitbucket.org/atlassian_tutorial/jira-simple-jql-function.git

Alternatively, you can download the source as a ZIP archive.

Step 1. Create the app project

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.

  1. Set up the Atlassian Plugin SDK and build a project if you have not done it yet.

  2. To create an app skeleton, run the following command:

    1
    2
    atlas-create-jira-plugin
    
  3. To identify your app, enter the following information.

    group-id

    com.example.plugins.tutorial.jira

    artifact-id

    jira-simple-jql-function

    version

    1.0-SNAPSHOT

    package

    com.example.plugins.tutorial.jira

  4. Confirm your entries when prompted.

The SDK generates the initial app project files in the jira-simple-jql-function directory.

Step 2. Review and tweak the POM

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.

  1. Navigate to the new jira-simple-jql-function directory and open the pom.xml file.

  2. 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>
    
  3. Update the description element:

    1
    2
    <description>Adds a custom JQL function named recentProjects to JIRA.</description>
    
  4. Save and close the file.

Step 3. Add your plugin modules

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.

  1. Navigate to the app root folder where the pom.xml is located and run the following command:

    1
    2
    atlas-create-jira-plugin-module
    
  2. Select the JQL Function option.

  3. Enter the following information when prompted.

    Enter New Classname

    RecentProjectFunction

    Package Name

    com.example.plugins.tutorial.jira.jql

  4. Select N for Show Advanced Setup.

  5. Select N for Add Another Plugin Module.

  6. Confirm your selection.

Step 4. Review and tweak the app descriptor

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. 

  1. Navigate to src/main/resources/ and open the atlassian-plugin.xml file.

  2. 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.

  3. Save and close the file.

Step 5. Write the app code

The SDK gave us the stub code for our class. In this step, we add the logic for our function.

  1. Navigate to src/main/java/com/example/plugins/tutorial/jira/jql/ and open the RecentProjectFunction.java file. 

  2. Replace import com.opensymphony.user.User; with the following import statement:

    1
    2
    import com.atlassian.jira.user.ApplicationUser;
    
  3. Update validate() method to use ApplicationUser instead of User

    1
    2
    public MessageSet validate(ApplicationUser searcher, FunctionOperand operand, TerminalClause terminalClause) {
        return validateNumberOfArgs(operand, 1);
    }
    
  4. Add the following import statements:

    1
    2
    import 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;
    
  5. Before the class definition, add a @Scanned annotation:

    1
    2
    @Scanned
    public class RecentProjectFunction extends AbstractJqlFunction {
       ...
    
  6. 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;
    } 
    
  7. Our function doesn't take any arguments yet. In the validate() method, change the number of arguments from 1 to 0:

    1
    2
    return validateNumberOfArgs(operand, 0); 
    
  8. The getValues() function is where most of the work happens. Replace the one that the SDK gave us with this one:

    1
    2
    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;
    }
    

    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.

  9. In getMinimumNumberOfExpectedArguments(), change the return value to 0:

    1
    2
    public int getMinimumNumberOfExpectedArguments() {
      return 0;
    }
    
  10. In getDataType(), change the data type returned from TEXT to PROJECT, because we return only a list of projects.

    1
    2
    return JiraDataTypes.PROJECT;
    

The entire class should look something like this:

1
2
package 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;
    }
}

Step 6. Remove the test files

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. In Terminal, navigate to the project root directory and run the following command:
1
2
rm -rf src/main/test

Step 7. Start Jira and test the app

We're ready to give the app a try.

  1. Open a Terminal window, navigate to the app root folder and run atlas-run command  (or atlas-debug if you want to launch the debugger in your IDE).
  2. Open the Jira instance in a browser (the URL is indicated in the Terminal output).
  3. Log in using the default admin/admin.
  4. Create two or three projects and populate them with a few issues. This will give us some data to work with.
  5. On the Jira header, click IssuesSearch for Issues.  
  6. To switch the form to advance search mode using JQL, click Advanced
  7. In the search field, enter the following: project in recentProjects() 
    Notice that autocomplete offers you the new function in its project completion suggestions.
  8. Click the search icon to run the search. The list of issues in recently visited projects appears.

Next steps

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: