Tutorial - Writing custom importer using JIRA importers plugin
This tutorial applies to JIRA 5.0.2 or later, with JIRA Importers Plugin 5.0.2 or later installed.
Level of experience:
This is an advanced tutorial. You should have completed at least one intermediate tutorial before working through this tutorial. See the list of tutorials in DAC.
It should take you approximately 2 hours to complete this tutorial.
On this page:
Overview of this tutorial
The Atlassian JIRA Importers (JIM) add-on enables administrators to import issues from external issue trackers into JIRA. The JIM add-on is bundled with JIRA by default, but you can supplement it with your own custom importers. This lets you create importers for proprietary issue tracking systems, say, or to import issues from any type of system in some custom way.
This tutorial shows you how to create a custom importer. We'll build a plugin that imports issues from a comma-separated values file into JIRA. It's a simple example, and one that mostly duplicates an importer already in JIM, but it does show you the basic steps involved in building your own importer.
To get the most out of this tutorial, you should know about:
- The basics of Java development, such as classes, interfaces, methods, and so on.
- How to create an Atlassian plugin project using the Atlassian Plugin SDK.
- How to use and administer JIRA.
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 code.
To clone the repository on your system, enter the following command:
Alternatively, you can download the source as a ZIP archive by choosing Downloads and then Branches here: https://bitbucket.org/atlassian_tutorial/tutorial-jira-simple-csv-importer
Step 1. Create the plugin project
In this step, you'll use the
atlas- command to generate stub code for your plugin. The
atlas commands are part of the Atlassian Plugin SDK, and automate much of the work of plugin development for you.
- If you haven't already set up the Atlassian Plugin SDK, do that now: Set up the Atlassian Plugin SDK and Build a Project.
- Open a terminal and navigate to your workspace directory.
Enter the following command to create a plugin skeleton:
- Choose 1 for JIRA 5 when asked which version of JIRA you want to create the plugin for.
As prompted, enter the following plugin settings and attributes:
- Confirm your entries when prompted.
The SDK creates your project home directory with initial project files, stub source code, and plugin resources.
Step 2. Modify the POM
It's a good idea to familiarize yourself with the project configuration file, known as the POM (Project Object Model definition file). Among other functions, the POM declares project dependencies and controls build settings. It also contains descriptive information for your plugin.
- Open the POM file (
pom.xml) for editing. You can find it in the root folder of your project.
Add your company or organization name and website to the
Next, add the following dependency and version property to your POM:
Here we're adding the JIRA Importers Plugin as a dependency. Since we've set the scope of this dependency to
provided, the plugin will rely upon the JIM package that comes with JIRA at runtime. Using a different scope would likely cause classloader issues related to duplicate classes available on the plugin classpath. The JIM version we specified, 6.0.11, matches the one that comes with JIRA 6.0.4 by default.
If you are using a version of JIRA that does not have JIM 5.0.2 or above included (JIRA versions 5.2 or earlier), you must also set the
pluginsproperty in your
pom.xml. This directs the SDK to install the specified version of JIM when starting JIRA.
- Save your changes.
Step 3. Add the external system importer module
As of JIM version 5.0, plugin developers can create importers by declaring an
external-system-importer plugin module in the plugin descriptor.
Let's add one to our plugin descriptor now:
- Open the plugin descriptor,
atlassian-plugin.xml, for editing. The plugin descriptor file is located in the
src/main/resourcesdirectory of your project home.
external-system-importerdeclaration as a child to the
The attributes of the element are:
keyis the plugin key for this importer.
i18n-name-keyis an optional i18n key for the module name.
i18n-description-keyis an i18n description string for this importer.
i18n-supported-versions-keyis an optional i18n message to indicate the versions of JIRA that this importer supports.
logo-module-keyis the name of the web resource containing the graphics that you want to use for this importer. This value is made up of the fully qualified plugin key for this plugin (the group ID and artifact ID you entered when creating the project), plus the name attribute from the web resource module (which we're adding in the next step).
logo-fileis the name of the logo file in the web resource element defined by
classis the fully qualified class name that implements interface
AbstractImporterController. For our importer, the class will be
weightis a numeric value that determines the position of this importer in the list of external importers, with lower numbered importers appearing earlier in the list. Built-in importers have weight values of 1 to 50. Feel free to experiment with the placement of the importer on the page by modifying this number. For example, making it 0 puts it second in the list, since there's already an importer with a weight of 0.
web-resourceelement that identifies the graphic to be used as the icon for our custom importer in the External Importers page
Import a few external components to our plugin by adding these
This imports two components from JIM:
JiraDataImporteris responsible for handling the end-to-end import process.
UsageTrackingServiceallows usage tracking of your importer. For this tutorial, we will rely upon the standard JIM tracking service. However, you can implement your own tracking just by implementing the interface
Step 4. Set up a few UI resources
While in the resources directory, let's take care of a few more resources for the importer. Each importer appears on the External Importers page with an icon of about 133x64 pixels in size.
Let's add one for our importer and add UI strings:
- Download the following image and put it in your
Add the following properties to the
The description and version fields appear on the external importers page. The other fields will appear as labels for the individual steps of the import wizard.
Step 5. Implement the AbstractImporterController class
Now create the importer controller class for our plugin. This class serves as the orchestrator for the activities of our importer.
Create the importer controller class at
com.example.plugins.tutorial.jira.csvimport.SimpleCsvImporterController. Give it the following code:
Notice that the import controller implements the abstract class
AbstractImporterController. Included among the base methods we're overriding, we have:
AbstractConfigBeaninstance, along with a few other things we need. The config bean in this case is
SimpleCsvConfigBean, which is responsible for handling the configuration process for us. It's also responsible for storing the
importProcessBeanin the session (using the method
storeImportProcessBeanInSession()). In our case we also create
SimpleCsvClient, a helper class to encapsulate all logic connected with our external system. We must place it here, since we cannot tie custom objects to
ImportProcessBean; instead we'll use
SimpleCsvConfigBeanto transfer this object for us. Note that storing
importProcessBeanin session means that only one importer instance may be created for each logged in user.
createDataBean()gets help from a private method,
getConfigBeanFromSession()to return the configuration data bean the controls the import process for our custom importer.
getSteps()returns a list of action names that forms the wizard steps for this importer. Here we have:
SimpleCsvSetupPageprovides our custom setup page that reads the file name supplied by the user.
ImporterProjectMappingsPageis a reused project mapping page that allows the user to select or create a project into which to import the issues.
ImporterCustomFieldsPageis a reused custom field mapping page that allows the user to map custom fields from the external issue tracker to JIRA custom fields.
ImporterFieldMappingsPageis a reused field value mapping page that allows the user to choose how fields from the external system are mapped to JIRA issues.
ImporterValueMappingsPageis a reused field value mapping page that allows the user to select how particular field values are mapped to JIRA issues.
ImporterLinksPageis a reused link mapping page that allows the user to map links from external system to JIRA link types.
Note that JIM adds a final step for every importer automatically, so we didn't need to include it here. It shows the results of the import event. Also notice the .
class.getSimpleName()method call. We can call the method in this manner because, by convention, the action name is the same as the class name that implements the action.
isUsingConfigBeanreturns false, which indicates that this importer will not allow customers to save configuration of the import process.
As you may surmise from our controller code, we have a few more classes to implement. Let's keep going.
Step 6. Create the SimpleCsvConfigBean class
Now create the
com.example.plugins.tutorial.jira.csvimport.SimpleCsvConfigBean with the following code:
Our class extends
AbstractConfigBean2, which is compatible with the configuration bean provided in the JIM setup pages. We must implement four abstract methods to make this configuration work:
getExternalProjectNamesreturns a list of project names in the external system. These values appear on the project mappings page, where the user can choose an external project to map to projects in JIRA. Keep in mind that for our case, external system is simply the CSV file to be imported.
getCustomFieldsreturns a list of custom fields defined in the external system. You can use factory methods in
ExternalCustomFieldto create different types of custom fields. These custom fields appear on the custom field page, where users can choose how they'll be imported into JIRA.
getLinkNamesFromDbreturns link names from the external system. The Importer Links page shows these links, and allows user to map them to JIRA link types.
initializeValueMappingHelperinitializes the protected field
ValueMappingDefinitionsFactory, which is responsible for creating mapping definitions. Mapping definitions associate values from the external system to issue field values in JIRA. For most cases, the importer would need to account for mappings for the Status, Issue Type, Resolution, and Priority values. But you may map any value from an issue, as listed in
com.atlassian.jira.issue.IssueFieldConstants. We'll create the Priority
Step 7. Create the PriorityValueMappingDefinition class
Now create the class responsible for mapping priority values from the source system to JIRA. It tells the importer which external system field is mapped to which JIRA issue field. It also defines available values in JIRA and external system for the field. Moreover it can supply default mapping between JIRA and the external system.
Create the class
com.example.plugins.tutorial.jira.csvimport.mapping.PriorityValueMappingDefinition with the following code:
The class implements these methods:
getJiraFieldIdreturns values from
IssueFieldConstants, which determines which JIRA field this mapping relates to.
getTargetValuesreturns values from JIRA for this custom field. We use
constantsManagerto obtain these values.
canBeBlankif true, this field is optional in the UI.
canBeCustomif true, the user can create new values in the Importer Value Mappings page. Use this option only if values for this JIRA field can be created dynamically.
canBeImportedAsIsreturns true if the user does not have to provide a value for this field. In this case, the original value can be imported from the source system "as is" (that is, without further processing).
getExternalFieldIdis the name of this field in the external system.
getDescriptionis an optional description displayed to the user in the Importer Value Mappings page.
getDistinctValuesreturns only distinct values from the external sytem.
getDefaultValuesreturns the default mappings between values in the external system and in JIRA.
trueif the user is required to map this field.
Step 8. Create the SimpleCsvDataBean class
The data bean class handles the task of transforming the imported data.
Create the class
com.example.plugins.tutorial.jira.csvimport.SimpleCsvDataBean with the following code:
The class extends
AbstractDataBean<SimpleCsvConfigBean>, a helper class that combines our config bean and allows using user mapped values for projects and issues. We must implement these methods to make our data bean work:
getRequiredUsersreturns the user accounts that are effectively used in the JIRA project targeted for the import. In our case we just return all users, as we use all of them.
getAllUsersreturns the users from the external system.
getAllProjectsreturns all projects from the external system. In our case we return one project. Notice that we use
) and) to get values defined by the user for the project named "project", as specified on the Importer Project Mappings page.
getIssuesIteratorreturns an iterator with all issues in the external project. In this method importer is responsible for mapping custom fields and values to ones selected by the user.
Notice the following points:
, from.getPriority())to get the priority value that the user selected. The "priority" string is the name of the external system field defined in our
PriorityValueMappingDefinitionclass. Because we decided that the user can use each value "as is," we must check whether the user has provided a value for this mapped value. If not, we must use the original value from the external system.
)to get the user-selected name for custom field. We use the same id "cf" that was defined in
getLinksreturns all links between issues in the external system. The
)call gets the name of the link that the user has selected, passed as the "link" parameter, and which is defined in
getTotalIssuesreturns all issues for the selected projects.
getUnusedUsersGroupreturns JIRA group that will be created for users that are not active.
cleanUpis called after the import has finished. You can clean up used resources here.
getIssueKeyRegexcan return a regular expression that matches on external system issue keys. Matched expressions in summary, comments, and description fields are replaced by the assigned JIRA issue key. For example, ("case: 2842") will be rewritten to JIRA references ("JRA-2848").
getVersionsreturns all versions for the given project. In our example we do not handle versions, so we just return an empty collection.
getComponentsreturns all components for given project. In our example we do not handle components, so we just return an empty collection.
Step 9. Define the CSV file text field as a webwork action
The first page of the import page sequence is the setup page. It gets the path to the CSV file that contains the data we need to import. For simplicity, we'll require the user to specify the file by path on the local file system, leaving file upload outside the scope of this tutorial.
Our setup page is composed by several parts: the class that implements the setup page, a Velocity template, and the descriptor module that ties them together. We'll start with the class:
Create the class
com.example.plugins.tutorial.jira.csvimport.web.SimpleCsvSetupPagewith the following content
Notice a few things:
@RequiresXsrfCheckannotation for the
doExecutemethod is important! If not present, the user does not need to provide admin credentials to change JIRA state, which would create a serious security hole in the JIRA instance. Be sure to add it to your own importer implementations.
- To validate input,
doValidation()simply attempts to instantiate
SimpleCsvClientto see whether the provided file path is valid.
Back in the descriptor file,
atlassian-plugin.xml, add the following webwork action module:
The module references the setup page class you just added, and the Velocity template that will display our form. It's important for the
aliasattribute value to be the name of the class that implements the action. Notice also that we reuse page resources from JIRA:
restartneeded.vm, from JIM plugin, displays information indicating to the user that the import process needs to be restarted. This appears if the user tries to omit steps in the setup wizard, or for any reason the HTTP session ends.
securitybreach.jsp, from JIRA core, displays information about unauthorized usage.
Create the Velocity template for our setup page by adding the following code to a file named
in a new directory,
This is the page that lets the user enter the path to the CSV file they want to import. In it, we use JIM-provided templates to format our page:
import-header.vmadds a standard header for all JIRA importers.
textfield.vmrenders simple HTML input with type text and parameters provided in the
standardSetupFooter.vmputs Next and Back buttons on the page, along with the standard footer.
Step 10. Create the SimpleCsvClient class and data model
Finally, let's create a class that simulates our external issue tracker and models the data in our external issue tracking system. That data will be in CSV files and consist of six fields separated by a comma. The fields are
com.example.plugins.tutorial.jira.csvimport.SimpleCsvClient to simulate our external issue tracker:
SimpleCsvClientis capable of returning a list of issues containing six values:
Now create the issue class,
Create data for our external system. Add the following data to a file named
test.csv in the
The same file is checked into the tutorial repository on Bitbucket. The filename and location (
test/resources/test.csv) isn't so important for now; users of the importer can specify the file anywhere in the setup page. But it is where the integration test that we'll build later expects the test data to be, so putting it there now saves a step for later.
We're ready to start JIRA and see what we've got so far.
Step 11. Start JIRA and try out the importer
Follow these steps to install and test the importer plugin:
- In a terminal window, navigate to the plugin root folder (where the
pom.xmlfile is) and run
atlas-debugif you might want to launch the debugger in your IDE).
JIRA takes a few minutes to download and start up.
- When JIRA finishes starting up, open the JIRA home page in a browser. JIRA prints the URL to use in the console output.
- Log in with the default user and password combination, admin/admin.
- Create a new project based on the Software Development Template. You'll need this because it has the issue states that our plugin depends upon (such as the open state).
From the JIRA header, click Projects > Import External Project.
- Find your importer in the list. As you may recognize, it's the one named Simple CSV Importer:
- Click on the importer to launch the wizard. You should see the first step in the wizard, the setup page you created:
In the CSV File path field, enter the path to the file and click Next. For example:
- Continue working through the wizard. In step 2, be sure to map the imported issues to the software development project you created.
- Finally, click the Begin Import button to finish the import.
When done, you should get a message similar to this one: 0 projects and 4 issues imported successfully! If you navigate to the project, you should see four new issues in your project.
Let's extend the importer a bit by allowing users to save the configuration they've used to perform an import. They can then use the same configuration to quickly perform more imports.
Step 12. Expand the plugin to support configuration files
So far you have a basic importer that can import simple CSV files. Let's extend it a bit to provide the ability to save configuration settings to a file. The administrator can then reuse the configuration on subsequent imports. This comes in handy when you need to import many projects with the same import settings.
Add component import to
atlassian-plugin.xml. This will import ConfigFileHandler which will help us handle serialization and desalinization of importer configuration.
- Extend the
SimpleCsvSetupPageclass as follows:
Add a constructor parameter and field for
This allows us to use
configFileHandlerto validate the configuration file provided by the user.
Add this validation code as the last line of the
Expand the following code block to see what the entire class should now look like:
- Now extend the
SimpleCsvImporterControllerclass as follows:
First add constructor parameter and field for
Populate the configuration file in
SimpleCsvImporterController, as follows:
- Finally remove the
isUsingConfigurationmethod. By default (i.e., if not overridden), this method returns
true, which is what we want to do now that we support saving the configuration.
Extend your setup page Velocity template,
csvSetupPage.vm, to allow users to provide a configuration file for the import settings. To do this simply add this parse directive to your
csvSetupPage.vmfile before the
Step 13. Try it again
Reload the plugin in JIRA and try your custom importer again. Now you should see a new option on the first page of the wizard, a checkbox for Use an existing configuration file:
We don't have a configuration file yet, so proceed through the wizard manually, as before.
And after completing import process you should see something like this:
Notice the new sentence in the What now? information. The save the configuration link lets you save the configuration as a file and reuse it to perform the next import. Give it a shot!
Step 14. Create integration tests
To round out the picture, let's add a couple integration tests for our plugin. Writing unit tests is beyond the scope of this tutorial. Instead, we'll describe how to write an integration test that uses web driver and JIRA page objects. You will learn how to write your own page objects and how to use them to simulate user behavior to test the simple CSV importer.
You'll notice that the SDK generated some test code for us. Those tests are intended to serve as the starting point for tests. We'll use the
test directory it created for the following additional tests.
pom.xmlwith some dependencies required for the tests:
Also in the POM, add a
pluginsproperty to include the JIRA testkit plugin:
This means that when the SDK starts, JIRA will include the JIRA testkit plugin, enabling us to change the state of JIRA using the backdoor plugin.
Create the page object that represents
SImpleCsvSetupPage. Under the test directory (
src/test/) create the class
com.example.plugins.jira.csvimport.po.SimpleCsvSetupPage, with the following contents:
Still in the tests directory, create a test class
Note that the package name for this or any integration test class must start with
it. The SDK run environment looks for integration tests there. Notice the following about our test class:
First, we use the
setUpmethod to create the backdoor client used to restore JIRA to an empty state.
The first of our two tests checks whether our importer is added to the External Import page.
The second test goes through our Simple CSV Importer wizard and uses the backdoor to make sure that four issues have been created.
The test depends on your CSV file being in a particular location. If you don't already have your test data at the expected location, put the following test data at
- You also need to create a directory,
test/xml, for the tests to use. It can be empty.
- Now you're ready to run the test! In the console, enter the command
This starts JIRA, opens a Firefox browser, and runs the tests. For details of the results, inspect the log files at the location indicated in the command terminal output.