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). Any changes you make to the script are automatically recompiled when you execute it.
Caution though, this is harder than most other customizations. You will almost certainly require a working IDE.
There are two types of custom functions in essence:
A query on issues, where you should return a lucene query object
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.
Quick Start
Use one of the examples below, and modify it to suit.
Modify the class name to something of your choice
Specify arguments if required
Implement argument validation
Click the scan link at ScriptRunner → JQL Functions.
The scan function is only required when you first add a JQL function to a running Jira instance. 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. See Scanning for JQL functions for more information on scanning.
When you are happy, test in the issue navigator. Make sure you test your error handling by entering incorrect parameters, the wrong number of parameters, and as different users.
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.
Scanning for JQL functions
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 agetQuery()
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.
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 as follows:
- Navigate to ScriptRunner → Script Editor.
- Select the Add Folder icon.
- Enter com/onresolve/jira/groovy/jql.
- Select Add.
This creates the nested folder structure needed. - Add the JQL function script under the jql folder calling it the same name as the class name in the script.
Examples
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}" JqlQueryParser queryParser = ComponentAccessor.getComponent(JqlQueryParser) LuceneQueryBuilder 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.jira.security.PermissionManager import com.atlassian.query.clause.TerminalClause import com.atlassian.query.operand.FunctionOperand class VersionIsStarted extends AbstractScriptedJqlFunction implements JqlFunction { VersionManager versionManager = ComponentAccessor.getComponent(VersionManager) PermissionManager 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> } } }
- No arguments for this function
- We must specify the type that we are returning
- Versions not released, having a start date, and the start date is before today
- Filter just those current user has permission to see the project of
- Create the
QueryLiteral
from the version identifier
Further Examples
Custom function to return the last Friday of any given month - on Answers