Writing Jira Plugin to add Custom Field


I was trying to develop a JIRA plugin to add custom field. In the mean time, I have gone through some of the useful sites and compiled the findings, which I want to share with you all.

1)Download the Atlassian SDK from Atlassian Home Page.
This contains apache maven as well.

2)Set your JAVA_HOME.

3)For eclipse users, go to the workspace directory and create a folder for the project that you are going to create for the custom plugin.

4)Open a command line and navigate inside the path of the folder created in (3).

5)Now you are going to create the skeleton for the custom plugin.I hope you have set the JAVA_HOME,M2_HOME(set to maven installed dir) properly.
From the command line trigger the command atals-create-jira-plugin.
 

You will be prompted to select the version of jira for which you want to create the plugin. Here I have selected 4(or earlier version).The skeleton of the plugin will be prepared for you and you need not worry about the structure of the project being created.

Upon prompting, you need to provide values for groupID,artifactID,version and package. This things can be provided as per your desire.
 

On opening the project into eclipse you will see the structure something like this :


6)To see the effect of this, you can trigger the command atlas-run from the directory where pom.xml file is located within the project just created.
Now the jira will run on port 2990 by default and you can simply login using credentials of admin/admin.
For the first time, the required libraries for JIRA  will be downloaded from the maven repository and it will take some time.If you already have repository somewhere locally then you can point to that location indicating that by the parameter localrepository in the pom.xml file of your project or of the maven( indicated by environment variable M2) that is being used.

7)Till now we haven't developed any plugin module.Its only the skeleton. So to develop the plugin module navigate to the project directory and trigger the command atlas-create-jira-plugin-module.
8)Among the provided option select Custom Field Type

9) Supply the following information as prompted:
  •     Enter New Classname:    MoneyCustomField (This is the CustomFieldType class.)
  •     Package Name:    com.example.plugins.tutorial.jira.customfields
  •     Choose N for Show Advanced Setup.
  •     Choose N for Add Another Plugin Module.
  •     Confirm your choices.
  •     Return to Eclipse and Refresh your project.
  •     Review the components added by the plugin module generator.
  •     atlassian-plugin.xml     The customfield-type module was added.
  •    MoneyCustomField     Added a source file for this class
  •    edit.vm,view.xm     WebWork Velocity templates added to the resources.
  •    atlassian-plugin.properties     File containing i18n key/value pairs.
10)If you have made any changes in the files and the thing is not reflected in the eclipse then you can trigger the command atlas-mvn eclipse:eclipse.

11)Now the most awaited part is the code writing part.

Open Eclipse and browse to the MoneyCustomField class. (The atlas-create-jira-plugin-module command you ran earlier generated this class.) Custom Fields can either store single values, or multiple values. We want to store a single value, so MoneyCustomField should extend the AbstractSingleFieldType class. This class provides much of the field's implementation for you.
Furthermore we want to 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 to so we will declare:
public class MoneyCustomField extends AbstractSingleFieldType<BigDecimal>
You will need to add the following imports:
import com.atlassian.jira.issue.customfields.impl.AbstractSingleFieldType;
import java.math.BigDecimal;
We won't override anything in getVelocityParameters(), so you can delete this and our field will use the default implementation from the superclass which is AbstractCustomFieldType. If you want to explore the class hierarchy, go to index of JIRA Javadocs and navigate to the your version's docs.
Now there are some abstract methods to implement from AbstractSingleFieldType
getStringFromSingularObject() - This methods turns a value in our Transport object (BigDecimal in our case) into text. Add the following method:
@Override
public String getStringFromSingularObject(final BigDecimal singularObject)
{
    if (singularObject == null)
        return null;
    else
        return singularObject.toString();
}
getSingularObjectFromString() - This 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. Add this 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.");
    }
}
getDatabaseType() - This tells 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;
}
getObjectFromDbValue() - 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().
@Override
protected BigDecimal getObjectFromDbValue(@NotNull final Object databaseValue)
        throws FieldValidationException
{
    return getSingularObjectFromString((String) databaseValue);
}
getDbValueFromObject() - 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);
}
Removing unused code, your class should look like this:

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

import java.math.BigDecimal;

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;

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

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

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

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

   
    public String getStringFromSingularObject(final BigDecimal singularObject)
    {
        if (singularObject == null)
            return "";
        // format
        return singularObject.toString();
    }
    @Override
    public String getStringFromSingularObject(Object 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.");
        }
    }
   
}

Edit the Velocity Templates.

First lets setup the view template. This is called view.vm and lives under the src/main/resources/templates/customfields/money-custom-field
We want it to look like this:
#if ($value)
    $$value
#end
The #if clause checks for null values.
the "$value" will substitute in the value of our transport object, and the other $ in front is actually a literal dollar sign - you could change this to whatever suits the currency in your country if you want.
Next lets add the edit template.
Open edit.vm and set it to:
#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 pretty much a copy/paste from edit-basictext.vm in JIRA core code.
The #customControlHeader and #customControlFooter are boilerplate that add in the label, description, any validation error messages etc.
The interesting bit is the <input> tag that creates a textfield for the user to enter the value.

12)Build and Test Your Plugin
At this point, you haven't actually written any Java code. You can however run JIRA and see your plugin in action. In this step, you will build the plugin create a project, add a custom field and use it.

13)Build the Plugin and Create a New 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:
4.  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
  1. 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.
  2. At the JIRA login, enter a username of admin and a password of admin.
  3. Click the Administration link in the dashboards's right hand corner.
    The browser opens the Administration page.
  4. Click Create your first project.
    The system displays the Add a New Project dialog.
  5. Enter TUTORIAL for both the project and key value.
  6. Click Add.
    The system displays the page for your new project.
14)Add Your Custom Field
You should already be on the Project page for administrating the TUTORIAL project:
  1. Click the Issues tab.
  2. Click the Fields tab.
  3. Click Default Field Configuration.
  4. Click Custom Fields.
  5. Click Add Custom Field
    The system displays the Create Custom Field: Choose the Field Type step.
  6. Select the Money Custom Field
    Note the description reflects it comes from the plugin you just created.
  7. Click Next.
    The systme displays the Create Custom Field - Details (Step 2 of 2) step.
  8. Set the fields as follows:
Field Name
Expense
Description
Accepts a money amount
Choose Search Template
Use the default.
Choose applicable issue types
Any issue type
Choose applicable context
Global context. Apply to all issues in JIRA.
  1. Click Finish
    • Now select Default Screen and click Update.
      This ensures the field appears on the default screen. The system returns you to the
      Custom Fields page.
  2. Press Exit Administration.
    The system returns you to the System Dashboard.
15)Test with the New Field

From the System Dashboard, do the following to test the new field:
  1. Choose Issues > Create Issue from the menu bar.
    The system displays the Create Issue dialog.
  2. Work your way though the dialog entering the values you see fit.
    The last field on the dialog is the Expense field you created.
  3. Enter a value in Expense field.
  4. 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.

After running jira at "http://localhost:2990/jira" and adding the custom field of type that we just created against an issue, the issue detail appears something like this:


Happy Coding!

References:

5 comments:

  1. can post a code for select list.

    ReplyDelete
  2. can post a code for select list plugin.

    ReplyDelete
  3. Need to explore com.atlassian.jira.issue.customfields.impl package for the select list.

    ReplyDelete
  4. can you answer for this ?
    https://answers.atlassian.com/questions/181826/how-to-develop-custom-field-plugin-having-multiple-input-fields-as-like-cascade-text-input-custom-field

    ReplyDelete
  5. If you are able to populate one custom field then it wont be a problem to populate multiple fields. Regarding the calculation of sum of the contents of the three custom fields,it should be possible. Though I haven't tried it, if you are able to write your custom field then it should not be an issue.

    Thanks.

    ReplyDelete