Custom JQL Functions

The benefit of using ScriptRunner for JQL functions rather than writing your own is you get the ability to return a Lucene query using the issueFunction custom field (fast) rather than a list of literals (very slow), and any changes you make to the script with be automatically recompiled when you execute it.

Caution though, this is harder than most other customizations. You will almost certainly require a working IDE.

Quick Start

Use one of the examples below, and modify to suit.

Go to Admin → JQL Functions.

  • Modify the class name to something of your choice

  • Specify arguments if required

  • Implement argument validation

  • Click the Scan link at Admin → JQL Functions

  • Test

For the scanning process to recognize your class as a JQL function you must either:

  • implement com.onresolve.jira.groovy.jql.JqlQueryFunction for functions that utilize issueFunction (and have a getQuery() method)

  • OR implement com.onresolve.jira.groovy.jql.JqlValuesFunction for functions that return a list of QueryLiterals from the getValues() method.

To make life easy you should extend com.onresolve.jira.groovy.jql.AbstractScriptedJqlFunction.

You need to implement getFunctionName(), getDescription(), and getArguments(), which simply provide information to the drop-down list, but otherwise have no functional effect.

Unfortunately, at the moment, your function must reside under the com.onresolve.jira.groovy.jql package for it to be found. (You can create these directories under one of your script roots).

Click the scan link, which is buried in the text. The scan function is only required when you first add a JQL function to a running JIRA. From this point on it will automatically be loaded when JIRA starts, so you should never need to click the scan link more than once per function you add.

You can modify the code, when you re-run the query via the issue navigator (or REST etc), your new code will be automatically recompiled. As always, tail the log while you are working, so you can see compilation errors etc.

When you are happy, test in the issue navigator. Make sure you test your error handling by entering garbage for the parameters, the wrong number of parameters, and as different users.

There are two types of custom functions in essence:

  1. A query on issues, where you should return a lucene query object

  2. A query on anything else, eg users, projects, components, where you return a list of pointers to these objects

There is an example below of each type.

JQL Alias Example

Sometimes you will have a complex query that is required for, for example, generating release notes. Expecting users to type it all correctly is unlikely to work. This function allows you to take a complex query, and parameterize it to something simpler.

In this contrived example, running issueFunction in releaseNotes(1.1), will actually run project = JRA and fixVersion = 1.1 and affectedVersion = 1.1.

In the real world, the real query would likely be much more complex.

Note that the validator validates the aliased query, so we can trap errors like the user providing a version that doesn’t actually exist.

package com.onresolve.jira.groovy.jql

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.jql.parser.JqlQueryParser
import com.atlassian.jira.jql.query.LuceneQueryBuilder
import com.atlassian.jira.jql.query.QueryCreationContext
import com.atlassian.jira.jql.validator.NumberOfArgumentsValidator
import com.atlassian.jira.user.ApplicationUser
import com.atlassian.jira.util.MessageSet
import com.atlassian.query.clause.TerminalClause
import com.atlassian.query.operand.FunctionOperand
import org.apache.lucene.search.Query

import java.text.MessageFormat

class JqlAliasFunction extends AbstractScriptedJqlFunction implements JqlQueryFunction {

    /**
     * Modify this query as appropriate.
     *
     * See {@link java.text.MessageFormat} for details
     */
    public static final String TEMPLATE_QUERY =
        "project = JRA and fixVersion = {0} and affectedVersion = {0}"

    def queryParser = ComponentAccessor.getComponent(JqlQueryParser)
    def luceneQueryBuilder = ComponentAccessor.getComponent(LuceneQueryBuilder)

    @Override
    String getDescription() {
        "Create release notes"
    }

    @Override
    MessageSet validate(ApplicationUser user, FunctionOperand operand, TerminalClause terminalClause) {
        def messageSet = new NumberOfArgumentsValidator(1, 1, getI18n()).validate(operand)

        if (messageSet.hasAnyErrors()) {
            return messageSet
        }

        def query = mergeQuery(operand)
        messageSet = searchService.validateQuery(user, query)
        messageSet
    }

    @Override
    List<Map> getArguments() {
        [
            [
                description: "Version to generate release notes for",
                optional   : false,
            ]
        ]
    }

    @Override
    String getFunctionName() {
        "releaseNotes"
    }

    @Override
    Query getQuery(QueryCreationContext queryCreationContext, FunctionOperand operand, TerminalClause terminalClause) {

        def query = mergeQuery(operand)
        luceneQueryBuilder.createLuceneQuery(queryCreationContext, query.whereClause)
    }

    private com.atlassian.query.Query mergeQuery(FunctionOperand operand) {
        def queryStr = MessageFormat.format(TEMPLATE_QUERY, operand.args.first())
        queryParser.parseQuery(queryStr)
    }
}
 

Project Versions Example

This JQL function returns issues that have a fix or affects version where the version is not released, but the start date, if present, is in the past.

We suppose this might let you infer what will be released soon. It would be used as in: fixVersion in versionsStarted(). It is not really intended to be useful, just a guide.

Because this function is applicable to Version fields (ie fix and affects versions, single and multi version custom fields), we implement JqlFunction rather than JqlQueryFunction, and need to return a list of QueryLiteral. See the notes below the code.

package com.onresolve.jira.groovy.jql

import com.atlassian.jira.JiraDataType
import com.atlassian.jira.JiraDataTypes
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.jql.operand.QueryLiteral
import com.atlassian.jira.jql.query.QueryCreationContext
import com.atlassian.jira.permission.ProjectPermissions
import com.atlassian.jira.project.version.VersionManager
import com.atlassian.query.clause.TerminalClause
import com.atlassian.query.operand.FunctionOperand

class VersionIsStarted extends AbstractScriptedJqlFunction implements JqlFunction {

    def versionManager = ComponentAccessor.getComponent(VersionManager)
    def permissionManager = ComponentAccessor.getPermissionManager()

    @Override
    String getDescription() {
        "Issues with fixVersion started but not released"
    }

    @Override
    List<Map> getArguments() {
        Collections.EMPTY_LIST // <1>
    }

    @Override
    String getFunctionName() {
        "versionsStarted"
    }

    @Override
    JiraDataType getDataType() {
        JiraDataTypes.VERSION // <2>
    }

    @Override
    List<QueryLiteral> getValues(
        QueryCreationContext queryCreationContext, FunctionOperand operand, TerminalClause terminalClause
    ) {

        def now = new Date()
        versionManager.allVersions.findAll {
            def startDate = it.startDate

            !it.released && startDate && startDate < now // <3>
        }.findAll {
            queryCreationContext.securityOverriden || permissionManager.hasPermission(ProjectPermissions.BROWSE_PROJECTS, it.project, queryCreationContext.applicationUser) // <4>
        }.collect {
            new QueryLiteral(operand, it.id) // <5>
        }
    }
}

  1. No arguments for this function
  2. We must specify the type that we are returning
  3. Versions not released, having a start date, and the start date is before today
  4. Filter just those current user has permission to see the project of
  5. Create the QueryLiteral from the version identifier

Further Examples

  • Custom function to return the last Friday of any given month - on Answers

On this page