Adding a field to CQL

Applicable:

This tutorial applies to Confluence 5.9 or higher

Level of experience:

This is an advanced tutorial. You should have completed at least one intermediate tutorial before working through this tutorial.

Time estimate:

It should take you approximately 1 hour to complete this tutorial.

Overview

This tutorial shows you how to add a field to the Confluence Query Language (CQL). For more detailed reference documentation please see: CQL Field Module

Prerequisite knowledge

To complete this tutorial, you must already understand:

  • The basics of Java development: classes, interfaces, methods, how to use the compiler, and so on. 
  • How to create an Atlassian plugin project using the Atlassian Plugin SDK.

Plugin source

We encourage you to work through this tutorial. If you want to skip ahead or check your work when you are done, you can find the plugin source code on Atlassian Bitbucket. To clone the repository, use the following command:

git clone https://bitbucket.org/atlassian/confluence-query-lang-add-field-tutorial/downloads

Alternatively, you can download the source using the get source option here: https://bitbucket.org/atlassian/confluence-query-lang-add-field-tutorial/downloads

About these Instructions

You can use any supported combination of OS and IDE to construct this plugin. These instructions were written using IntelliJ IDEA Ultimate 14.0 on a MacBook Pro running Mac OS X. If you are using another combination, you should use the equivalent operations for your specific environment.

Step 1. Add the field declaration to your atlassian-plugin.xml

First, let's add a dependency on the SPI to the pom.xml:

<dependency>
	<groupId>com.atlassian.querylang</groupId>
    <artifactId>confluence-query-lang-spi</artifactId>
    <version>1.3.2</version>
	<scope>provided</scope>
</dependency>

Now to add a field, we'll need to make this addition to the atlassian-plugin.xml:

<cql-query-field fieldName="status"
                 key="status-field" name="Content Status Field"
                 class="com.example.plugins.tutorial.confluence.impl.StatusFieldHandler">
    <ui-support value-type="string" default-operator="=" i18n-key="cql.field.status" data-uri="/rest/status-field/status-values"/>
</cql-query-field>

The cql-query-field declaration is what defines a new CQL field:

Attribute Purpose
fieldName Name of the field which will be used in CQL statements
key Unique id of this CQL field declaration in the plugin
name A human readable name for the field
class The implementation of the CQL field, this should extend the 
com.atlassian.querylang.fields.BaseFieldHandler class

The ui-support declaration, allows the new field to appear in the UI:

Attribute Purpose
value-type Type of values which can be stored against this field (see supported UI value types in CQL Field Module)
default-operator CQL operator used in conjunction with the contents of the UI field when it's converted to a statement
i18n-key The i18n key in the i18n properties file for the text that will be displayed as a label for the field in the UI
data-uri REST endpoint which provides field values

For this example, we'll implement a status field, using the generic string UI value type backed by a data-uri which provides the user with a set of values to select from in the front-end.

Our data-uri points to a simple REST resource, responding with a set of status values.

<rest key="status-values" path="/status-field" version="none">
    <description> Provides status values to the front end to power CQL status field UI support</description>
</rest>

The response from the /status-field/status-values endpoint is:

{
  "suggestedResults":[],
  "searchResults":[
    {
      "id":"pending",
      "text":"Pending"
    },
    {
      "id":"review",
      "text":"Review"
    },
    {
      "id":"complete",
      "text":"Complete"
    }
  ]
}

Finally, we need to add the i18n field name to our i18n properties file:

cql.field.status=Status

Step 2. Add an EventListener and Extractor

This step is not specifically related to the process of adding fields to CQL, but is required for the field to function correctly. In the example repository, an EventListener has been setup that listens for page create events and sets the default 'status' of 'pending' as a content property on the page. Setting a content property to a piece of content could be done in a variety of ways, such as via a REST resource but we have used the EventListener in this case for simplicity. An Extractor class is then used which runs after the EventListener and page has been created, extracting the status information from a content property, then adding the information to the index and allowing it to searched. 

Below is an extractor implementation used in the example repository and adapted from the Extractor Module documentation.

package com.example.plugins.tutorial.confluence.impl;

import com.atlassian.bonnie.search.Extractor;
import com.atlassian.bonnie.Searchable;
import com.atlassian.plugin.spring.scanner.annotation.component.Scanned;
import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import com.atlassian.confluence.core.ContentEntityObject;
import com.atlassian.confluence.core.ContentPropertyManager;
import com.opensymphony.util.TextUtils;

import javax.inject.Inject;

@Scanned
public class StatusPropertyExtractor implements Extractor
{
    public static final String INDEXABLE_PROPERTY = "status";

    private ContentPropertyManager contentPropertyManager;

    @Inject
    public StatusPropertyExtractor(@ComponentImport ContentPropertyManager contentPropertyManager)
    {
        this.contentPropertyManager = contentPropertyManager;
    }

    public void addFields(Document document, StringBuffer defaultSearchableText, Searchable searchable)
    {
        if (searchable instanceof ContentEntityObject)
        {
            ContentEntityObject contentEntityObject = (ContentEntityObject) searchable;
            String value = contentPropertyManager.getStringProperty(contentEntityObject, INDEXABLE_PROPERTY);

            if (TextUtils.stringSet(value))
            {
                defaultSearchableText.append(value);
                document.add(new Field(INDEXABLE_PROPERTY, value, Field.Store.YES, Field.Index.NOT_ANALYZED));
            }
        }
    }

}

Step 3. Add a FieldHandler

In order to complete the addition of our new CQL field, we need to provide the implementation of the class which was referenced in our cql-query-field declaration. 

package com.example.plugins.tutorial.confluence.impl;

import com.atlassian.confluence.plugins.cql.spi.v2searchhelpers.V2SearchQueryWrapper;

import com.atlassian.confluence.search.v2.SearchQuery;
import com.atlassian.confluence.search.v2.query.TermQuery;
import com.atlassian.querylang.fields.BaseFieldHandler;
import com.atlassian.querylang.fields.EqualityFieldHandler;
import com.atlassian.querylang.fields.expressiondata.EqualityExpressionData;
import com.atlassian.querylang.fields.expressiondata.SetExpressionData;
import com.google.common.base.Function;

import javax.annotation.Nullable;

import static com.atlassian.confluence.plugins.cql.spi.v2searchhelpers.V2FieldHandlerHelper.joinSingleValueQueries;
import static com.atlassian.confluence.plugins.cql.spi.v2searchhelpers.V2FieldHandlerHelper.wrapV2Search;
import static com.atlassian.querylang.fields.expressiondata.EqualityExpressionData.Operator.EQUALS;
import static com.atlassian.querylang.fields.expressiondata.EqualityExpressionData.Operator.NOT_EQUALS;

import static com.atlassian.querylang.fields.expressiondata.SetExpressionData.Operator.IN;
import static com.atlassian.querylang.fields.expressiondata.SetExpressionData.Operator.NOT_IN;
import static com.google.common.collect.Sets.newHashSet;

public class StatusFieldHandler extends BaseFieldHandler implements EqualityFieldHandler<String, V2SearchQueryWrapper> {

    private static final String FIELD_NAME = "status";

    public StatusFieldHandler()
    {
        super(FIELD_NAME);
    }

    @Override
    public V2SearchQueryWrapper build(SetExpressionData setExpressionData, Iterable<String> values)
    {
        validateSupportedOp(setExpressionData.getOperator(), newHashSet(IN, NOT_IN));
        SearchQuery query = joinSingleValueQueries(values, new Function<String, TermQuery>(){
            @Override
            public TermQuery apply(@Nullable String value)
            {
                return createEqualityQuery(value);
            }
        });
        return wrapV2Search(query, setExpressionData);
    }

    @Override
    public V2SearchQueryWrapper build(EqualityExpressionData equalityExpressionData, String value)
    {
        validateSupportedOp(equalityExpressionData.getOperator(), newHashSet(EQUALS, NOT_EQUALS));
        return wrapV2Search(createEqualityQuery(value), equalityExpressionData);
    }

    private TermQuery createEqualityQuery(String value)
    {
        return new TermQuery(StatusPropertyExtractor.INDEXABLE_PROPERTY, value);
    }
}

We extend the BaseFieldHandler, which provides much of the default implementation required and is the class recommended as a base for all CQL FieldHandler implementations. The class must also implement at least one typed FieldHandler interface, EqualityFieldHandler in our case, which will allow for support of "=, !=, IN and NOT IN" CQL operators for querying the status of a page. See CQL Field Module for a full list of available field types.

The EqualityFieldHandler interface requires that we implement two build methods. One will support SetExpressions, "status IN ('pending', 'review')", and the other EqualityExpressions, "status = 'pending'".

These methods usually look quite similar, and we have provided a number of helper functions via the CQL SPI for performing common operations found in these build method implementations.

validateSupportedOp - is used to check whether the operator used in the CQL statement being executed is supported by our FieldHandler. Using the ExpressionData.getOperator method as a first argument and providing a set of the operations we have decided to support will perform this validation for us, throwing an exception with a meaningful error message to the user when a CQL statement with an unsupported operator has attempted to execute. Using this helper is recommended to allow existing FieldHandlers to continue working by providing the appropriate response in the event that we add new operators to CQL. 

wrapV2Search - Used to combine a V2SeachQuery with the ExpressionData object provided to the build method to return a V2SearchQueryWrapper object, expected as the return type of the the build method. This V2SearchQueryWrapper object contains all the necessary information to execute the CQL and handles negation based on the ExpressionData operator to retrieve the required data from the index.

joinSingleValueQueries - A helper method used in SetExpressionData build method implementations, allowing a single V2SearchQuery to be transformed into a single QueryObject which can then be passed to wrapV2Search. 

Step 4. Build, install and run the plugin

Follow these steps to build and install your plugin, so that you can test your code.  

  1. Make sure you have saved all your code changes to this point.
  2. Open a terminal window and navigate to the plugin root folder (where the pom.xml file is).
  3. Run the following command:

    atlas-run
    

    This command builds your plugin code, starts a Confluence instance, and installs your plugin. This may take several seconds. When the process is complete, you'll see many status lines on your screen concluding with something like:

    [INFO] Confluence started successfully in 71s at http://localhost:1990/confluence
    [INFO] Type CTRL-D to shutdown gracefully
    [INFO] Type CTRL-C to exit
    
  4. Open your browser and navigate to the local Confluence instance started by atlas-run.
    If you used the settings in the instructions, the default port is 1990. 
  5. Navigate to your local Confluence, enter http://localhost:1990/confluence/ in your browser.
  6. At the Confluence login, enter the username admin and password admin.
  7. Navigate to the Confluence search page (http://localhost:1990/confluence/dosearchsite.action) which is backed by CQL.
  8. Hit Add a filter. Your new field should appear in the list of CQL fields available to filter the search. 


Screenshot: Confluence Search page showing the newly added Status CQL field. 

 

Was this page helpful?

Have a question about this article?

See questions about this article

Powered by Confluence and Scroll Viewport