Tutorial - Custom fields in JIRA

Level of experience:

This is a beginner tutorial. Our tutorials are classified as 'beginner', 'intermediate' and 'advanced'. This one is at 'beginner' level, so you can follow it even if you have never developed a plugin before. 

Time estimate:

It should take you approximately 30 minutes to complete this tutorial.

On this page:

Overview of the tutorial

This tutorial shows you how to create a simple custom field that can store a currency value. To create this field, you build a JIRA plugin consisting of the following components:

  • Java classes encapsulating the plugin logic.
  • Resources for display of the plugin UI (Velocity templates).
  • A plugin descriptor (XML file) to enable the plugin module in the Atlassian application.

Each component is further discussed below.

About these Instructions

You can use any supported combination of OS and IDE to construct this plugin. These instructions were written using Eclipse Classic Version 3.7.1 on Linux Ubuntu 10. If you are using another combination, you should use the equivalent operations for your specific environment.

This tutorial was last tested with JIRA 6.0.2. 

Prerequisites

To complete this tutorial, you need to know the following: 

  • How to use an integrated development environment (IDE), such as Eclipse or IDEA.
  • The basics of Java development, including classes, interfaces, methods, how to use the compiler, and so on.

You should already have the latest version of the Atlassian Plugin SDK on your development system. If you don't have the SDK and are not already familiar with it, you should start with the Getting Started information.

Plugin Source

We encourage you to work through this tutorial. If you want to skip ahead or check your work when you have finished, you can find the plugin source code on Atlassian Bitbucket. Bitbucket serves a public Git repository containing the tutorial's code. To clone the repository, issue the following command:

git clone https://bitbucket.org/atlassian_tutorial/jira-custom-field

Alternatively, you can download the source as a ZIP archive by choosing download here: 

https://bitbucket.org/atlassian_tutorial/jira-custom-field

Step 1. Create the plugin project

In this step, you'll use two atlas- commands to generate stub code for your plugin and set up the stub code as an Eclipse project. The atlas- commands are part of the Atlassian Plugin SDK, and automate much of the work of plugin development for you.

  1. Open a terminal and navigate to your Eclipse workspace directory.
  2. Enter the following command to create a JIRA plugin skeleton:

    atlas-create-jira-plugin
    
  3. Choose 1 for JIRA 5 when asked which version of JIRA you want to create the plugin for. 
  4. As prompted, enter the following information to identify your plugin:

    group-id

    com.example.plugins.tutorial.jira

    artifact-id

    tutorial-jira-custom-field

    version

    1.0-SNAPSHOT

    package

    com.example.plugins.tutorial.jira.customfields

  5. Confirm your entries when prompted.
  6. Change to the tutorial-jira-custom-field directory created by the previous step.
  7. Run the command:

    atlas-mvn eclipse:eclipse
    
  8. Start Eclipse.
  9. Select File > Import.
    Eclipse starts the Import wizard.
  10. Filter for Existing Projects into Workspace (or expand the General folder tree).
  11. Press Next and enter the root directory of your workspace.
    Your Atlassian plugin folder should appear under Projects.
  12. Select your plugin and click Finish.
    Eclipse imports your project.

Step 2. Review and tweak the generated stub code

It is a good idea to familiarize yourself with the stub plugin code. In this section, we'll check a version value and tweak a generated stub class. Open your plugin project in Eclipse and follow along in the next sessions to tweak some code.

Add plugin metadata to the POM

Add some metadata about your plugin and your company or organization.

  1. Edit the pom.xml file in the root folder of your plugin.
  2. Add your company or organization name and your website to the <organization>element:

    <organization>
        <name>Example Company</name>
        <url>http://www.example.com/</url>
    </organization>
    
  3. Update the <description>element:

    <description>Provides a custom field to store money amounts.</description>
    
  4. Save the file.

Review the generated plugin descriptor

Your stub code contains a plugin descriptor file atlassian-plugin.xml. This is an XML file that identifies the plugin to the host application (JIRA) and defines the required plugin functionality. In your IDE, open the descriptor file which is located in your project under src/main/resources and you should see something like this:

<atlassian-plugin key="${project.groupId}.${project.artifactId}" name="${project.name}" plugins-version="2">
    <plugin-info>
        <description>${project.description}</description>
        <version>${project.version}</version>
        <vendor name="${project.organization.name}" url="${project.organization.url}" />
        <param name="plugin-icon">images/pluginIcon.png</param>
        <param name="plugin-logo">images/pluginLogo.png</param>
    </plugin-info>
    <resource type="i18n" name="i18n" location="tutorial-jira-custom-field"/>
    <web-resource key="tutorial-jira-custom-field-resources" name="tutorial-jira-custom-field Web Resources">
        <dependency>com.atlassian.auiplugin:ajs</dependency>
        <resource type="download" name="tutorial-jira-custom-field.css" location="/css/tutorial-jira-custom-field.css"/>
        <resource type="download" name="tutorial-jira-custom-field.js" location="/js/tutorial-jira-custom-field.js"/>
        <resource type="download" name="images/" location="/images"/>
        <context>tutorial-jira-custom-field</context>
    </web-resource>

In the next step, you'll use the plugin module generator (another atlas- command) to generate the stub code for modules needed by the plugin.

Step 3. Add your plugin modules to the plugin descriptor

For this tutorial, you will need a Custom Field plugin module. You'll add this by using the atlas-create-jira-plugin-module command.

  1. Open a console and go to the plugin root folder (where the pom.xml is located).
  2. Run 

    atlas-create-jira-plugin-module
  3. Choose the option labeled Custom Field.
  4. As prompted, enter the module parameters:

    Enter New Classname

    MoneyCustomField (This is the CustomFieldType class.)

    Package Name

    com.example.plugins.tutorial.jira.customfields

  5. Choose N for Show Advanced Setup.
  6. Choose N for Add Another Plugin Module.

SDK adds the module declaration to the plugin descriptor and creates the source files for the module, including a class file and presentation templates. 

Step 4. Update your project and refresh your IDE

Eclipse is not automatically aware of changes made to the project outside of Eclipse, as we've just made with the SDK. Moreover, sometimes your project dependencies require an update. Let's refresh the project in Eclipse:

  1. Switch to a terminal window.
  2. Change directory to the project root.
    This is the directory with the pom.xml file.
  3. Update your Eclipse metadata with the changes to the plugin source code:

    atlas-mvn eclipse:eclipse
    
  4. Back in Eclipse, refresh the plugin project to pick up the changes.

Remember to do this update and refresh step each time you edit your pom.xml and whenever you modify your plugin source with an Atlassian command.

Step 5. Write the plugin code

You have already generated the stubs for your plugin modules. Now you will write some code that will make your plugin do something. Recall that this plugin will add a custom text field in a JIRA issue that stores a money value. To do this, you'll need to implement the CustomFieldType interface and create velocity templates for viewing and editing the field.

  1. Open Eclipse and browse to the MoneyCustomField class. The atlas-create-jira-plugin-module command you ran earlier generated this class.
  2. Let's start by adding some import statements. Add these to the ones the SDK added for you:

    import com.atlassian.jira.issue.customfields.impl.AbstractSingleFieldType;
    import java.math.BigDecimal; 
    import com.atlassian.jira.issue.customfields.persistence.PersistenceFieldType;
  3. Custom Fields can store single values or multiple values. In our case, we want to store a single value, so change the class declaration to extend the AbstractSingleFieldType class:

    public class MoneyCustomField extends AbstractSingleFieldType<BigDecimal>
    

    This class provides much of the field's implementation for you. Notice also that we'll use BigDecimal as our "transport object" (for dealing with a currency in Java). A transport object is just a plain old Java object (POJO). The object's type represents the custom field used.

  4. We won't override anything in getVelocityParameters(), so you can delete this. Our field will use the default implementation from the superclass which is AbstractCustomFieldType. If you want to explore the class hierarchy, access the docs appropriate for your version from the index of JIRA Javadocs.

  5. We'll implement a few abstract methods from AbstractSingleFieldType, first by adding the following the getStringFromSingularObject() method to your class.

    @Override
    public String getStringFromSingularObject(final BigDecimal singularObject)
    {
        if (singularObject == null)
            return null;
        else
            return singularObject.toString();
    }
    


    This method turns a value in our Transport object (BigDecimal, in our case) into text.

  6. Add the getSingularObjectFromString() method:

    @Override
    public BigDecimal getSingularObjectFromString(final String string) 
            throws FieldValidationException
    {
        if (string == null)
            return null;
        try
        {
            final BigDecimal decimal = new BigDecimal(string);
            // Check that we don't have too many decimal places
            if (decimal.scale() > 2)
            {
                throw new FieldValidationException(
                        "Maximum of 2 decimal places are allowed.");
            }
            return decimal.setScale(2);
        }
        catch (NumberFormatException ex)
        {
            throw new FieldValidationException("Not a valid number.");
        }
    }

    The method takes input from the user, validates it, and puts it into a a transport object. We want to validate that the user has entered a valid number, and that there are no more than two decimal places.

  7. Now add the getDatabaseType() method to tell JIRA what kind of database column to store the data in. You can choose text, long text, numeric, or date. We could use numeric, but we will use Text to keep it simple.

    @Override
    protected PersistenceFieldType getDatabaseType()
    {
        return PersistenceFieldType.TYPE_LIMITED_TEXT;
    }
    
  8. Add the getObjectFromDbValue() method:

    @Override
    protected BigDecimal getObjectFromDbValue(final Object databaseValue)
            throws FieldValidationException
    {
        return getSingularObjectFromString((String) databaseValue);
    }
    

    This takes a value from the DB and converts it to our transport object. The value parameter is declared as Object, but will be String, Double, or Date depending on the database type defined above. Because we chose FieldType TEXT, we will get a String and can reuse getSingularObjectFromString().

  9. Finally, add the getDbValueFromObject() method. It takes a value as our transport object and converts it to an Object suitable for storing in the DB. In our case we want to convert to String.

    @Override
    protected Object getDbValueFromObject(final BigDecimal customFieldObject)
    {
        return getStringFromSingularObject(customFieldObject);
    }
    

That's it for the MoneyCustomField class. Removing unused code, your class should look like this:

package com.example.plugins.tutorial.jira.customfields;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.atlassian.jira.issue.customfields.impl.AbstractSingleFieldType;
import com.atlassian.jira.issue.customfields.impl.FieldValidationException;
import com.atlassian.jira.issue.customfields.manager.GenericConfigManager;
import com.atlassian.jira.issue.customfields.persistence.CustomFieldValuePersister;
import com.atlassian.jira.issue.customfields.persistence.PersistenceFieldType;

import java.math.BigDecimal;

public class MoneyCustomField extends AbstractSingleFieldType<BigDecimal> {
    private static final Logger log = LoggerFactory.getLogger(MoneyCustomField.class);

    public MoneyCustomField(CustomFieldValuePersister customFieldValuePersister,
            GenericConfigManager genericConfigManager) {
        super(customFieldValuePersister, genericConfigManager);
    }

    @Override
    protected PersistenceFieldType getDatabaseType()
    {
        return PersistenceFieldType.TYPE_LIMITED_TEXT;
    }

    @Override
    protected Object getDbValueFromObject(final BigDecimal customFieldObject)
    {
        return getStringFromSingularObject(customFieldObject);
    }

    @Override
    protected BigDecimal getObjectFromDbValue(final Object databaseValue)
            throws FieldValidationException
    {
        return getSingularObjectFromString((String) databaseValue);
    }

    @Override
    public String getStringFromSingularObject(final BigDecimal singularObject)
    {
        if (singularObject == null)
            return "";
        // format
        return singularObject.toString();
    }

    @Override
    public BigDecimal getSingularObjectFromString(final String string)
            throws FieldValidationException
    {
        if (string == null)
            return null;
        try
        {
            final BigDecimal decimal = new BigDecimal(string);
            // Check that we don't have too many decimal places
            if (decimal.scale() > 2)
            {
                throw new FieldValidationException(
                        "Maximum of 2 decimal places are allowed.");
            }
            return decimal.setScale(2);
        }
        catch (NumberFormatException ex)
        {
            throw new FieldValidationException("Not a valid number.");
        }
    }
}

Notice the Logger instance we instantiate in the first line of the class. The SDK added this to our stub code. By default, Atlassian plugins use the Simple Logging Facade for Java (SLF4J) for its logging facility. We haven't shown how to use logging in our sample, but in general, you can add log messages in your code where appropriate for troubleshooting and debugging. You then set the desired logging level in the JIRA administration console. For a tutorial of a plugin that uses logging, see Tutorial - Writing JIRA event listeners with the atlassian-event library. Also see the Confluence logging guidelines. While targeted for Confluence, that page covers some general concepts related to logging as well. 

Step 6. Build and test your plugin

We're not finished yet, but let's try what we've created so far.

Start JIRA up with your plugin and create project

  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 JIRA instance, and installs your plugin in it. This may take several seconds or so, when the process completes you see many status lines on your screen concluding with something like the following lines:

    [INFO] jira started successfully in 71s at http://localhost:2990/jira
    [INFO] Type CTRL-D to shutdown gracefully
    [INFO] Type CTRL-C to exit
    
  4. Open your browser and navigate to the local JIRA instance started by atlas-run.
    If you followed the instructions, enter http://localhost:2990/jira in your browser.
  5. At the JIRA login, enter a username of admin and a password of admin.
    Since this is a new instance, JIRA prompts you to select a project type. 
  6. With Blank Project selected as the project type, choose Next.
    JIRA displays the Add a New Project dialog.
  7. Enter TUTORIAL for both the project name and the key.

    To keep our plugin code simple, we've hard-coded the project name into our plugin code. The JIRA UI will offer TUT as a suggested project key for a project named TUTORIAL. So be sure to type TUTORIAL for the project key as well, or the plugin will not work.

  8. Click Submit.
    JIRA displays the overview page for your new project.

Add your custom field to the project configuration

From the overview page of your new project: 

  1. Choose the Administration tab.
  2. Scroll down to the Fields section and click Default Field Configuration.
  3. Click Custom Fields from the left menu.
  4. Click Add Custom Field
    The system displays the Create Custom Field: Choose the Field Type step.
  5. Select Advanced and then the Money Custom Field
    This is the custom field you just created.
  6. Click Next.
    The system displays the Create Custom Field - Details (Step 2 of 2) step.
  7. Set the fields as follows:

    Field Name

    Expense

    Description

    Accepts a money amount

    Choose applicable issue types

    Any issue type

    Choose applicable context

    Global context

    Notice the Choose Search Template section. Since we haven't implemented any custom search templates, we don't have any to choose from here. 

  8. Click Finish.
  9. Select Default Screen and click Update.
    This ensures the field appears on the default screen. 

Test the new field

Now we can see our new field in action by creating an issue:

  1. Choose Create Issue from the menu bar.
  2. Make sure the project is the TUTORIAL project and click Next.
  3. In the Create Issue form, scroll to the bottom of the page and notice the placeholder text, edit.vm.
    The plugin worked, which is good, but it's not usable yet, which is not so good. We need to customize our template code to allow for user input to the field.
  4. Click Cancel, but keep JIRA running and your browser window open for now. We'll get back to it in a minute.

Step 7. Edit the Velocity templates

Plugins can expose interface in JIRA through Velocity templates. The SDK gave us two templates, one for viewing the field value and for another for editing it.

There are two additional templates you can create: column-view, which lets you customize the field value rendering in the issue navigator (JIRA uses the view.vm rendering otherwise), and xml, which renders your field in the XML and RSS output for the issue.

See Tutorial - Creating a custom field type for more information.

Customize the default templates as follows:

  1. In a new console, navigate to this directory: src/main/resources/templates/customfields/money-custom-field

  2. Open the view.vm file for editing and replace its contents with the following:

    #if ($value)
        $$value
    #end
    

    The #if clause simply checks for null values in the value variable. If its value is null, nothing gets shown in the template rendering. Otherwise, $value in the second line is replaced by the value of our transport object in the rendered template. The additional "$" symbol is actually a literal dollar sign. You can change this to another currency symbol if desired (e.g., €$value).

  3. In the same directory, replace the contents of the edit.vm file with the following:

    #customControlHeader ($action $customField.id $customField.name $fieldLayoutItem.required $displayParameters $auiparams)
    <input class="text" id="$customField.id" name="$customField.id" type="text" value="$textutils.htmlEncode($!value)" />
    #customControlFooter ($action $customField.id $fieldLayoutItem.fieldDescription $displayParameters $auiparams)
    

    This is the same code found in the edit-basictext.vm template in the JIRA core code. This means that this field will look the same as many other text fields in JIRA that use the "basic text" input template. The #customControlHeader and #customControlFooter are Velocity macros defined in the macros.vm file built into JIRA. It checks certain conditions and adds standard UI text around the field, including a label, description, and validation error messages. The interesting bit is the input element. It's the HTML form element that exposes the text field for entering a value for our custom field.

    For more about the context parameters you can use in your Velocity templates, see Custom field Velocity context unwrapped.

Step 8. Do a live reload and try again

You can test your changes without restarting JIRA using live reload.

  1. Back in the JIRA browser window, expand the developer toolbar by clicking the expander arrow at the bottom left of the page.
  2. In the expanded toolbar, click the icon for Live reload noncompiled code:

    Give JIRA a few moments, but note that you can continue to the next step while the progress indicator is still revolving.
  3. Now create an issue as you did before.
    This time, our Expense field appears at the bottom of the form.
  4. Enter a value in Expense field.
  5. Press Create.

Experiment some more with this field. You can try entering values that are invalid. Try entering something like "2" or "2.5" and see how it gets rendered after you save. You could also not enter anything in the field. Edit some of the issues you create. Notice that if you don't enter value in the field, when you edit the issue later the field is hidden.

Extra credit

You can keep going by experimenting with different field types.

Also, try extending the plugin by adding a custom field searcher. You can use the Atlassian Plugin SDK to generate the initial code for your custom field searcher, using atlas-create-jira-plugin-module and choosing the Custom Field Searcher option as the module to create. From there, you can choose the custom field searcher class to extend, such as text searcher or exact text searcher.

For more about custom field searchers, see custom field searcher.

Also, check out these other resources on custom fields:

Congratulations, that's it

Have a chocolate!

Was this page helpful?

Have a question about this article?

See questions about this article

Powered by Confluence and Scroll Viewport