JQL function

Available:

Jira 4.0 and later.

The introduction of advanced searching (that is, JQL) significantly enhances Jira searching functionality.

JQL functions are among the extension points that JQL provides to developers. Functions provide a way for values within a JQL query to be calculated at runtime. They are simple to write and can be surprisingly powerful.

For example, consider the issueKey clause in JQL. It matches an issue with a particular issue key. This in itself is not very useful, but when combined with a function that returns all of a user's watched issues (watchedIssues), it provides a way to find all the issues that the current user is watching (issuekey in watchedIssues()).

JQL functions can only provide values to a query; most importantly, they cannot be used to process the results.

For example, it is not possible to write a JQL function that will calculate the total time remaining from all issues returned from a search. Consequently, functions can only be used with JQL clauses that already exist. The only way to implement new JQL clauses is to implement a new searchable custom field. While this gives more control to the app developer, it is much more complicated.

JQL functions can take arguments. These arguments must take the form of simple string values. For example, fixVersion in releasedVersions('JIRA') contains a function call to releasedVersions to find all the released versions in the JIRA project. Making the arguments simple strings means that JQL lists and other JQL functions cannot be used as arguments. For example, it is not possible to do something like myFunction(currentUser()).

A JQL function is an implementation of the JqlFunction interface that is registered in Jira as a jql-function app. The registered JqlFunction will only be instantiated once per jql-function app. All queries that use the function will share the single instance. Consequently, a function can be called by multiple threads at the same time and as such must be thread-safe.

The plugin module descriptor

Here is an example of jql-function defined in the atlassian-plugin.xml file:

1
2
3
4
5
6
<jql-function key="example-function" i18n-name-key="example.plugin.name" name="Example Plugin Function"
        class="com.atlassian.example.jira.ExampleFunction">
    <description key="example.plugin.description">JQL function to make something cool</description>
    <fname>exampleFunc</fname>
    <list>true</list>
</jql-function>

Attributes

Name

Description

key

The unique identifier of the plugin module. You refer to this key to use the resource from other contexts in your app, such as from the plugin Java code or JavaScript resources.


<jql-function key="myJqlFunc"/>

Required: yes.

i18n-name-key

The localization key for the human-readable name of the plugin module.

Required: no.

name

The app name for the human-readable name of the plugin module. Will be used if i18n-name-key is not specified.

Required: no.

class

The Java class of the JQL function module. The custom JQL function class must implement the com.atlassian.jira.plugin.jql.function.JqlFunction interface, or extend a class that does.

Required: yes.

Elements

Name

Description

fnameThis element specifies the name of the function.
list

This element specifies whether this function returns a list of values or a single value. If omitted, the default is false.

description

The description of the plugin module. The key attribute can be specified to declare a localization key for the value instead of text in the element body.

That is, the description of the JQL function.

JQL function methods

In the following sections we go through the JqlFunction methods.

JQL Function init method

The JqlFunction.init() method is called by Jira to tell the JqlFunction about its associated JqlFunctionModuleDescriptor. This object represents Jira's view of the JqlFunction and can be used to find app resources. The init method is only called once and is guaranteed to be called before the function is actually used by Jira.

JQL function get function name method

The JqlFunction.getFunctionName method returns the name that can be used in JQL to invoke the function.

1
2
3
public String getFunctionName() {
    return "exampleFunction";
}

You can extend AbstractJqlFunction, so the fname element's value will be returned. Jira must get the same name each time it calls getFunctionName. Importantly, this means that the function name cannot be translated. The function name does not have to be in English, however, it must be in the same language for every user in Jira irrespective of their language settings.

The function name should also be unique across all instances of Jira where it is expected to run. Having two JQL functions of the same name in Jira will produce confusing results. Jira will only register the first function for use in Jira and will simply ignore any others of the same name. The app that Jira determines to be first is somewhat arbitrary and may result in different JQL functions of the same name being registered on each start.

The moral of the story: try very hard to make your function names unique.

JQL function get minimum number of expected arguments method

The JqlFunction.getMinimumNumberOfExpectedArguments returns the smallest number of arguments that the function can accept. The value returned from this method must be consistent across method invocations.

1
2
3
public int getMinimumNumberOfExpectedArguments() {
    return 1;
}

JQL function is list method

The JqlFunction.isList should return:

  • true if the function returns a list.
  • false if it returns a scalar.

The main difference is that a list type can be used with the IN and NOT IN operators while a scalar type can be used with =, !=, <, >, <=, >=, IS, and IS NOT.

1
2
3
public boolean isList() {
    return true;
}

You can extend AbstractJqlFunction, so the list element's value will be returned. The value returned from this method must be constant. It cannot change based on the parameters or the function's result. The function must either always return a list or must always return a scalar.

The easiest way to work out whether the function should return a list or not is to simply consider where it is going to be used. If the function makes sense with the IN or NOT IN operators, it returns a list and needs to return true for this method. This will normally be the case when the function logically returns more than one value (for example, releasedVersons(), membersOf()). On the other hand, if the function should be used with =, !=, <, >, <=, >=, IS, and IS NOT, it should return false. This will normally be the case when a function logically returns one value (for example, now(), currentUser()).

JQL function get data type method

The JqlFunction.getDataType method is called to determine the type of data the function returns. The value tells Jira which JQL clauses the function can be expected to work with. For example, returning JiraDataTypes.VERSION indicates that the function should only be used with clauses that work with Jira versions. You can return JiraDataTypes.ALL if you wish the function to be available across all JQL conditions.

1
2
3
public JiraDataType getDataType() {
    return JiraDataTypes.ALL;
}

Again, the value returned must be consistent across all invocations of this method.

JQL function validate method

The JqlFunction.validate method is called by Jira when the function needs to be validated. The job of this method is to check the arguments to the function to ensure that it is used correctly.

Here is the interface:

1
@NotNull MessageSet validate(ApplicationUser searcher, @NotNull FunctionOperand operand, @NotNull TerminalClause terminalClause);

The most important argument is the FunctionOperand. It contains all of the functions arguments as given by the FunctionOperand.getArgs method.

All JQL function arguments come in as Strings and it is up to the function to interpret them correctly.

The searcher is the user for whom the function should be validated, that is, the user for whom any security checks should be performed.

The TerminalClause is Jira's representation of the JQL condition we validate. For functions it represents a JQL condition of the form name operator function(arg1, arg2, ..., argn). The name, operator, and function can be returned by calling TerminalClause.getName, TerminalClause.getOperator, and TerminalClause.getOperand respectively.

The value returned from getOperand() will be the FunctionOperand that is passed to this method.

This method is only called when the passed arguments are relevant to the JQL function, that is, the validation does not need to check if the FunctionOperand has the correct function name.

The validate method must always return a MessageSet as its result; a null return is prohibited. A MessageSet is an object that contains all of the errors and warnings that occur during validation.

All messages in the MessageSet need to be translated with respect to the passed searching user. An empty MessageSet indicates that no errors have occurred. A MessageSet with errors indicates that the JQL is invalid and should not be allowed to run. The returned messages will be displayed to the user so that any problems may be rectified. A MessageSet with warnings indicates that the JQL may have problems but that it can still be run. Any warning messages are displayed above the results.

Functions need to respect Jira security. A function should not return references to Jira objects (for example, projects, issues) that the user is not allowed to see. Further, a function should not leak information about Jira objects that the searcher does not have permission to use. For example, a function should not differentiate between a project not existing and a project that the user has no permission to see. A function that behaves badly will not cause JQL to expose issues that the searcher is not allowed to see (since JQL does permission checks when it runs the filter), though it does open up an attack vector for information disclosure.

Only one instance of each JQL function is created. This means that your function can (and probably will) be called by two threads at the same time. To accommodate this, your function must be thread-safe or unexpected behavior can result.

1
2
3
public MessageSet validate(final ApplicationUser searcher, final FunctionOperand operand, final TerminalClause terminalClause) {
    return new MessageSetImpl();
}

The implementation of this method must be thread-safe. The dependencies should be thread-safe and stored in final or volatile variables to ensure visibility. All method state is kept local to ensure that it is not visible to other threads.

JQL function get values method

The JqlFunction.getValues method is called by Jira when it needs to execute the function so that it can perform a query.

1
2
@NotNull
List<QueryLiteral> getValues(@NotNull QueryCreationContext queryCreationContext, @NotNull FunctionOperand operand, @NotNull TerminalClause terminalClause);

The FunctionOperand and the TerminalClause are as described previously in the JqlFunction.validate method. The new argument here is the QueryCreationContext. This object contains the variables that may be necessary when executing the function.

The QueryCreationContext.getUser method returns the user that runs the search and as such should be used to perform any security checks that may be necessary.

The QueryCreationContext.isSecurityOverriden method indicates whether or not this function should actually perform security checks.

  • When method returns true, the function should assume that the searcher has permission to see everything in Jira.
  • When method returns false, the function should perform regular Jira security checks and make sure it only returns things that the searcher has permission to see. This parameter is used by Jira in certain administrative operations where finding all issues is important.

The JQL function returns a list of QuerylLiteral. A QueryLiteral represents either a String, Long, or EMPTY value. These three represent JQL's distinguishable types. The type of the QueryLiteral is determined at construction time and cannot be changed:

  • Construct it with no value and it represents EMPTY.
  • Construct it with a String and it represents a String.
  • Construct it with a Long and it represents a Long.

Most JQL clauses will treat each type differently. For example, let's consider the affectsVersion clause. When passed a Long QueryLiteral, it will look for all issues with an Affects Version of the specified ID. This is useful when a function would need to identify a particular version exactly. Where possible, we suggest that functions try to return IDs so that query results are unambiguous.

When passed a String QueryLiteral, the affectsVersion clause will run one of two searches depending upon the value in the QueryLiteral:

  • If version(s) with the name given in the QueryLiteral exist, then return all issues with the specified Affects Version(s). This may return empty results.
  • If the value given in the QueryLiteral can be parsed into a version ID and that version exists, then return all issues that have an Affects Version of the parsed ID. This may return empty results.

JQL functions may return String QueryLiterals. However, the result of the query will depend on the lookup procedure of the JQL clause it is used with. Finally, the EMPTY QueryLiteral will make the affectsVersion condition look for all issues that have no Affects Version set.

The function always returns a list of QueryLiteral objects. It is even valid for a scalar function (that is, a function whose JqlFunction.isList method returns false) to return multiple QueryLiteral objects. In such a situation it is the JQL clause the function is being used with that decides what this means. All of the core Jira JQL clauses simply treat such a situation as an OR between each of the returned values. The function must return an empty list of QueryLiteral objects (not an empty QueryLiteral) to indicate an error. Importantly, the function can never return a null list.

The JqlFunction.getValues method may be called with arguments that would not pass the JqlFunction.validate method. Under this situation it is important that the function does not throw an error, as JQL is designed to try and run invalid queries where possible. The function should run, if possible, or otherwise return an empty list. The only thing the function can assume is that the FunctionOperand argument is meant to be executed by the function.

Only one instance of each JQL function is created. This means that your function can (and probably will) be called by two threads at the same time. To accommodate this, your function must be thread-safe or unexpected behavior can result.

The JqlFunction.getValues method must execute quickly. Keep in mind, that your function will be executed each time the query is run. If your function takes 10 seconds to run, then the entire JQL query will take at least 10 seconds. Functions also need to perform well under concurrent load. Keep synchronization (locking) down to a minimum. The simplest way to do this is to keep all the functions' calculation state on the stack and out of member variables.

1
2
3
public List<QueryLiteral> getValues(final QueryCreationContext queryCreationContext, final FunctionOperand operand, final TerminalClause terminalClause) {
    return Collections.emptyList();
}

Function sanitization (Optional)

Function sanitization is important to make function production ready. A saved JQL search (filter) can be shared with multiple users. While this functionality is very useful, it also allows information to be leaked. For example, let's say you have a filter that contains assignee in exampleFunc(Administrators, Proj) and you share the filter with Janice who cannot see Proj. The search will not return any results, however, Janice will know that a project called Proj exists even though she does not have permission to see it.

A JQL function that can expose sensitive information (that is, a function that does security checks) should also implement the optional ClauseSanitisingJqlFunction interface.

The interface has one method:

1
@NotNull FunctionOperand sanitiseOperand(User searcher, @NotNull FunctionOperand operand);

This method takes a searcher and a FunctionOperand and returns an equivalent operand that hides any privileged information the passed searcher should not see. The returned function is what the passed searcher will see when trying to load the filter.

It is important that the FunctionOperand that is returned from sanitization is equivalent to the passed operand. If this is not the case, then it is possible for two people running the exact same filter to be actually running two different searches.

1
2
3
public FunctionOperand sanitiseOperand(final User user, final FunctionOperand functionOperand)  {
    return functionOperand;
}

Important points

  • Your function will be executed when the query is run. Make sure your function runs quickly even under concurrent load.
  • Only one instance of a function is created. This instance is shared by JQL queries that use the function. This means that a JQL function may be called concurrently by different threads. As a result, your JQL function must be thread-safe.
  • Ensure that you take notice of the QueryCreationContext.isSecurityOverriden when running the function.
  • JQL functions need to respect Jira security. If a function does not respect Jira security, then it becomes an attack vector for information disclosure.