__Searching in JIRA

New Searching

The way a search is performed in JIRA has significantly changed. The introduction of advanced searching (JQL) necessitated a rewrite of the JIRA searching subsystem. In the process, the API for searching has also been changed (and improved) significantly. Unfortunately these changes will almost certainly mean that plugins that search will need to be updated for JIRA 4.0.

In JIRA 3.x and earlier, searching was achieved using a SearchRequest in combination with SearchParameters and SearchSorts. While the SearchRequest still continues to exist in JIRA 4.0, the SearchParameters have been replaced with the Query object.

/**
 * The representation of a query.
 *
 */
public interface Query
{
    /**
     * @return the main clause of the search which can be any number of nested clauses that will make up the full
     * search query. Null indicates that no where clause is available and all issues should be returned.
     */
    Clause getWhereClause();

    /**
     * @return the sorting portion of the search which can be any number of
     * {@link com.atlassian.query.order.SearchSort}s that will make up the full order by clause. Null indicates that
     * no order by clause has been entered and we will not sort the query, empty sorts will cause the default
     * sorts to be used.
     */
    OrderBy getOrderByClause();

    /**
     * @return the original query string that the user inputted into the system. If not provided, will return null.
     */
    String getQueryString();
}

The Query is JIRA's internal representation of a JQL search. It contains the search condition (i.e. the "where" clause) and the search order (i.e. the "order by" clause). The Query object can be created using the JqlQueryBuilder. For example, to create a query "find all issues assigned to either Dylan or Tokes that are unresolved and due in the next week" you would call:

  final JqlQueryBuilder builder = JqlQueryBuilder.newBuilder();
  builder.where().assignee().in("Dylan", "Tokes").and().unresolved().and().due().lt().string("+1w");
  builder.orderBy().dueDate(SortOrder.ASC);
  Query query = builder.buildQuery();

Once the Query has been obtained, it can be used to execute a search. In JIRA 4.0 a new SearchService has been added to provide a central location for Query related operations. To run the search you can simply call SearchService.search() as documented on the SearchService. The SearchProvider is still available for those who need to control the finer details of searching.

The Query object is immutable; once it is created it cannot be changed. The JqlQueryBuilder represents the mutable version of a Query object. The JqlQueryBuilder can be primed with an already existing Query by calling JqlQueryBuilder.newBuilder(existingQuery).

In JIRA 3.x the SearchRequest was the object that was passed to the searching system to perform a search. The Query object has taken over this role in JIRA 4.0; the SearchProvider and SearchService now take in Query objects rather than SearchRequests. The SearchRequest object has been reworked in JIRA 4.0 to significantly reduce its responsibility. For instance, ordering information is now stored on the Query object rather than on the SearchRequest object. The SearchRequest really represents a saved search (aka. filter). You should only need to deal with SearchRequests if you are working with filters. Even in this case, all searching operations need to be performed on Query objects by calling SearchRequest.getQuery().

It is often necessary to get a URL for a particular Query. The SearchService provides the getQueryString(query) method for this. The method returns a parameter snippet of the form jqlQuery=<jqlUrlEncodedQuery>, which can be appended safely to an existing URL that points at the Issue Navigator. Note that the links that JIRA 4.0 generates are JQL based, so are incompatible with JIRA 3.x and before. Old valid JIRA 3.x URLs will still work with JIRA 4.0.

Given a Query object it is possible to retrieve its JQL representation by calling either getGeneratedJqlString(query) or getJqlString(query) on the SearchService. The service makes sure that any values in the Query that need to be escaped are handled correctly. Importantly, the Query.toString() method does not return valid JQL (on purpose).

The SearchService.parseQuery(jqlString) method can be used to turn a JQL string into its Query representation. The return from this method has details on any parse errors encountered.

A Query object, especially those parsed directly from the user, may not be valid. For example, the user may be trying to find issues in a status that does not exist. The SearchService.validateQuery(query) method can be used to see if a particular Query object is valid. Errors are returned with messages that can be displayed to the user. Executing an invalid Query will not result in any errors and in fact may return results. To run an invalid query, JIRA will just make the invalid conditions equate to false and run the query. For example, searching for status = "I don't Exist" or user = bbain will result in the query <false> or user = bbain actually being run.

There are some methods on the SearchService that we did not discuss here. Check out documentation on the SearchService for more information.

Examples

Here's a complete example how to obtain search results for the query "project is JRA and the reporter is the currently logged in user and custom field with id 10490 contains 'xss'":

        String jqlQuery = "project = JRA and reporter = currentUser() and cf[10490] = xss";
        final SearchService.ParseResult parseResult =
                searchService.parseQuery(authenticationContext.getUser(), jqlQuery);
        if (parseResult.isValid())
        {
            try
            {
                final SearchResults results = searchService.search(authenticationContext.getUser(),
                        parseResult.getQuery(), PagerFilter.getUnlimitedFilter());
                final List<Issue> issues = results.getIssues();

            }
            catch (SearchException e)
            {
                log.error("Error running search", e);
            }
        }
        else
        {
            log.warn("Error parsing jqlQuery: " + parseResult.getErrors());
        }

The preceding search could have also been written using the QueryBuilder:

        final JqlQueryBuilder builder = JqlQueryBuilder.newBuilder();
        builder.where().project("JRA").and().reporterIsCurrentUser().and().customField(10490L).eq("xss");
        Query query = builder.buildQuery();
        try
        {
            final SearchResults results = searchService.search(authenticationContext.getUser(),
                    query, PagerFilter.getUnlimitedFilter());
            final List<Issue> issues = results.getIssues();

        }
        catch (SearchException e)
        {
            log.error("Error running search", e);
        }

Plugging into JQL and what happened to my Custom Field Searchers

The introduction of advanced searching (JQL) necessitated a rewrite of the JIRA searching subsystem. Unfortunately these changes will certainly mean that any CustomFieldSearchers will need to be updated to work in 4.0.

The most fundamental change is that all JIRA 4.0 searching is implemented using JQL. A JQL search consists of two components: firstly, a number of conditions, or Clauses, that must be matched for an issue to be returned; and secondly, a collection of search orderings that define the order in which the issues should be returned. The Query object is JIRA's internal representation of a search. It is now the responsibility of the CustomFieldSearcher to take a relevant Query, validate its correctness and generate a Lucene query to find issues that match it. By doing this your custom field becomes searchable using JQL.

The CustomFieldSearcher and/or the custom field is also responsible for ordering results if the order in the search includes the custom field. If your custom field ordered correctly in JIRA 3.x, then it will order correctly in JIRA 4.0. While the internal representation of an order has changed in JIRA 4.0, it still uses the same interfaces to order the search results. We will not address ordering again.

What is a JQL Clause?

A custom field must process the Clauses from a JQL search to integrate into JQL. Each Clause consists of a number of conditions (e.g. abc != 20) combined by the AND and OR logical operators (e.g. abc = 20 AND (jack < 20 OR jill > 34). In JIRA a condition is represented by a TerminalClause, the logical AND by an AndClause and a logical OR by an OrClause, all of which implement the Clause interface. Finally, the logical NOT operator can be used to negate any other Clause. It is represented by a NotClause that also implements Clause. These Clause objects are composed together to represent a complex conditions. For example, the condition abc = 20 AND NOT(jill > 34 OR NOT jack < 20) is represented by the following tree:

A Clause can be navigated by passing an instance of a ClauseVisitor to the accept method of a Clause. This follows the traditional visitor pattern.

The TerminalClause represents a Clause of the form "field operator value". Inside the TermincalClause the "operator" is one of the values from Operator enumeration while the "value" is represented as an Operand. An Operand can represent a single value (e.g. field = "single"), a list of values (e.g. field in ("one", 1235)), a function (e.g. field = function(arg1, arg2)) or EMPTY (e.g. field is EMPTY). In the end, all you want is the values from the Operand. These can be obtained as a list of QueryLiteral (see below) by calling JqlOperandResolver.getValues(). The JqlOperandResolver also has the isEmptyOperand, isFunctionOperand, isListOperand and isValidOperand methods that can be used to determine the type of the Operand.

A QueryLiteral represents either a String, Long or EMPTY value. These three represent JQL's distinguishable types. It is up to the CustomFieldSearcher to convert these values into something that makes sense to it. The type of a QueryLiteral can be determined by calling its isEmpty, getLongValue or getStringValue methods. The get methods will return null or false when the method and the QueryLiteral type do not match.

Integrating with JQL

In JIRA 3.x a CustomFieldSearcher was the way to provide customized searching functionality for custom fields. In JIRA 4.0 it is still the plugin point for searching; however, the CustomFieldSearcher interface has changed significantly to accommodate the introduction of JQL. One of the major changes is that the CustomFieldSearcher must return a CustomFieldSearcherClauseHandler in JIRA 4.0. This object is a composition of a ClauseValidator and a ClauseQueryFactory.

The ClauseValidator is used by JIRA to ensure that a JQL query is valid according to the CustomFieldSearcher.

/**
 * Validates a clause and adds human readable i18n'ed messages if there is a problem.
 *
 * @since v4.0
 */
public interface ClauseValidator
{
    /**
     * Validates a clause and adds human readable i18n'ed messages if there is a problem.
     *
     * @param searcher the user who is executing the search.
     * @param terminalClause the clause to validate.
     *
     * @return an MessageSet that will contain any messages relating to failed validation. An empty message set must
     * be returned to indicate there were no errors. null can never be returned.
     */
    @NotNull
    MessageSet validate(User searcher, @NotNull TerminalClause terminalClause);
}

It is up to the validator to ensure that the operator and the value from the passed TerminalClause makes sense for the CustomFieldSearcher and its associated custom field. Any errors can be placed in the returned MessageSet. They should be internationalised with respect to the passed user.

The validate method must always return a MessageSet as its result. A null return is not allowed. 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 will be displayed above the results.

The ClauseValidator does not need to check if the passed TerminalClause is meant for the for it, JIRA makes sure that it only passes TerminalClauses that the ClauseValidator is meant to process. It does that by only passing TerminalClauses whose "field" matches one of the names the custom field must handle.

ClauseValidators need to respect JIRA security. A ClauseValidator should not leak information about JIRA objects that the searcher does not have permission to use. For example, a ClauseValidator should not differentiate between an object not existing and an object that the user has no permission to see. A ClauseValidator 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.

The ClauseValidator must be thread-safe and re-entrant to ensure correct behavior. JIRA will only create one instance of the ClauseValidator per custom field instance. This means that multiple threads may be calling the validator at the same time.

The ClauseQueryFactory is used by JIRA to generate the Lucene search for a JQL Clause.

public interface ClauseQueryFactory
{
    /**
     * Generates a lucene query for the passed {@link TerminalClause}....
     *
     * @param queryCreationContext the context of the query creation call; used to indicate that permissions should be
     * ignored for "admin queries"
     * @param terminalClause the clause for which this factory is generating a query.
     * @return QueryFactoryResult contains the query that lucene can use to search and metadata about the query. Null
     *  cannot be returned.
     */
    @NotNull
    QueryFactoryResult getQuery(@NotNull QueryCreationContext queryCreationContext, @NotNull TerminalClause terminalClause);
}

It is the responsibility of the ClauseQueryFactory to create the Lucene search for the passed TerminalClause and QueryCreationContext. The generated Lucene search is returned in the QueryFactoryResult. The result contains the search (a Lucene Query object which is not related the the JQL Query object) and a flag to indicate whether or not the Lucene search should be negated. When set to true, JIRA will actually only match issues that do not match the returned Lucene search. For example, a ClauseQueryFactory may decide to implement a condition like field != value by returning a Lucene search that matches field = value and setting the flag to true. You can also implement this condition by returning a Lucene search that matches field != value and setting the flag to false.

The new argument here is the QueryCreationContext. This object contains the variables that may be necessary when creating the query. The QueryCreationContext.getUser method returns the user that is running 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 it returns true, the factory should assume that the searcher has permission to see everything in JIRA. When it returns false, the factory should perform regular security checks.

A ClauseQueryFactory should try to limit the queries so that issues that the user cannot see are excluded. Consider the query affectsVersion = "1.0". The ClauseQueryFactory might detect that there are two versions named "1.0", one from project1 and the other from project2. The factory might then notice that the user doing the search cannot see project1. The factory can then return a query that contains only the version from project2. This is mainly an efficiency concern as JIRA filters all search results to ensure users cannot see issues they are not allowed to.

The ClauseQueryFactory does not need to check if the passed ClauseQueryFactory is meant for it; JIRA makes sure that it only passes TerminalClauses that the ClauseQueryFactory is meant to process. It does that by only passing TerminalClauses whose "field" matches one of the JQL names the custom field must handle. Put simply, the ClauseQueryFactory must handle any passed TerminalClause.

The ClauseQueryFactory must also handle the situation when an invalid TerminalClause is passed to it. An invalid TerminalClause is one whose associated ClauseValidator would not validate. The ClauseQueryFactory must return an empty Lucene search if the passed TerminalClause is invalid. Most importantly, the ClauseQueryFactory must not throw an exception on an invalid TerminalClause.

A ClauseQueryFactory needs to be careful when implementing any of the negating operators (i.e. !=, !~, "not in"). These operators should not match what is considered empty by the custom field and CustomFieldSearcher. For example, the JQL query resolution is EMPTY will return all unresolved issues in JIRA. The query resolution != fixed will only return all resolved issues that have not been resolved as "fixed", that is, it will not return any unresolved issues. The user has to enter the query resolution != fixed or resolution is EMPTY to find all issues that are either unresolved or not resolved as "fixed".

A ClauseQueryFactory also needs to consider field visibility. A CustomFieldSearcher should not match any issues where its associated custom field is not visible. Importantly, asking for EMPTY should not match issues where the custom field is not visible. For example, the JQL query resolution is EMPTY will not return issues from a project whose resolution field has been hidden. A hidden field is assumed not to exist.

There are some extra interfaces that the CustomFieldSearcherClauseHandler may also implement to provide optional functionality to the searching subsystem:

  • ValueGeneratingClauseHandler: Gives the CustomFieldSearcher the ability to suggest some values during JQL entry auto-complete. This is really only useful for custom fields whose values come from an allowable finite set.
  • CustomFieldClauseSanitiserHandler: Gives the CustomFieldSearcher the ability pre-process the query and remove sensitive information from the query before it is displayed to the passed user.
  • CustomFieldClauseContextHandler: Gives the CustomFieldSearcher the ability to customize JIRA's query context calculation. This interface is best left alone, unexplained and unimplemented.
Last modified on Sep 16, 2013

Was this helpful?

Yes
No
Provide feedback about this article
Powered by Confluence and Scroll Viewport.