Tutorial - Writing custom importer using JIRA importers plugin

Applicable:

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.

Time estimate:

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. 

About these Instructions

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

This tutorial was last tested with JIRA 6.0.4. 

Required knowledge

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.

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 code.

To clone the repository on your system, enter the following command:

git clone https://bitbucket.org/atlassian_tutorial/tutorial-jira-simple-csv-importer.git

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.

  1. If you haven't already set up the Atlassian Plugin SDK, do that now: Set up the Atlassian Plugin SDK and Build a Project.
  2. Open a terminal and navigate to your workspace directory.
  3. Enter the following command to create a plugin skeleton:
    atlas-create-jira-plugin

  4. Choose 1 for JIRA 5 when asked which version of JIRA you want to create the plugin for.
  5. As prompted, enter the following plugin settings and attributes:

    group-id

    com.example.plugins.tutorial.jira.csvimport

    artifact-id

    simple-csv-importer

    version

    1.0-SNAPSHOT

    package

    com.example.plugins.tutorial.jira.csvimport

  6. 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. 

  1. Open the POM file (pom.xml) for editing. You can find it in the root folder of your project. 
  2. Add your company or organization name and website to the organization element:

    <organization>
        <name>Example Company</name>
        <url>http://www.example.com/</url>
    </organization>
  3. Next, add the following dependency and version property to your POM: 

    <dependencies>
    ...
    	<dependency>
    		<groupId>com.atlassian.jira.plugins</groupId>
    		<artifactId>jira-importers-plugin</artifactId>
    		<version>${jim.version}</version>
    		<scope>provided</scope>
    	</dependency>
    ...
    </dependencies>
    ...
    <properties>
    ...
    	<jim.version>6.0.11</jim.version>
    ...
    </properties>

    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.

  4. 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 plugins property in your pom.xml. This directs the SDK to install the specified version of JIM when starting JIRA.

    <properties>
    ...
    	<plugins>com.atlassian.jira.plugins:jira-importers-plugin:${jim.version}</plugins>
    ...
    </properties>
  5. 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:

  1. Open the plugin descriptor, atlassian-plugin.xml, for editing. The plugin descriptor file is located in the src/main/resources directory of your project home.
  2. Add the external-system-importer declaration as a child to the atlassian-plugin element:

        <external-system-importer name="SimpleCSVImporter" key="SimpleCSVImporterKey"
            i18n-description-key="com.example.plugins.tutorial.jira.csvimport.description"
            i18n-supported-versions-key="com.example.plugins.tutorial.jira.csvimport.versions"
            logo-module-key="com.example.plugins.tutorial.jira.csvimport.simple-csv-importer:graphics"
            logo-file="simplecsv.png"
            class="com.example.plugins.tutorial.jira.csvimport.SimpleCsvImporterController"
            weight="20"/>
    

    The attributes of the element are:

    • key is the plugin key for this importer.
    • i18n-name-key is an optional i18n key for the module name. 
    • i18n-description-key is an i18n description string for this importer.
    • i18n-supported-versions-key is an optional i18n message to indicate the versions of JIRA that this importer supports. 
    • logo-module-key is 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-file is the name of the logo file in the web resource element defined by logo-module-key. 
    • class is the fully qualified class name that implements interface AbstractImporterController. For our importer, the class will be com.example.plugins.tutorial.jira.csvimport.CsvImporterController.  
    • weight is 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. 
  3. Add a web-resource element that identifies the graphic to be used as the icon for our custom importer in the External Importers page

    <web-resource key="graphics" name="Importer Graphics">
       <resource type="download" name="simplecsv.png"         
           location="images/simplecsv.png"/>
    </web-resource>
  4. Import a few external components to our plugin by adding these component-import statements:

    <component-import key="jiraDataImporter"
           interface="com.atlassian.jira.plugins.importer.imports.importer.JiraDataImporter"/>
    <component-import key="usageTrackingService"
           interface="com.atlassian.jira.plugins.importer.tracking.UsageTrackingService"/>

    This imports two components from JIM:

    • JiraDataImporter is responsible for handling the end-to-end import process.
    • UsageTrackingService allows 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 UsageTrackingService.

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: 

  1. Download the following image and put it in your src/main/resources/images directory:

  2. Add the following properties to the src/main/resources/simple-csv-importer.properties file.

    com.example.plugins.tutorial.jira.csvimport.description=Simple CSV Importer
    com.example.plugins.tutorial.jira.csvimport.versions=comma-separated value files
    com.example.plugins.tutorial.jira.csvimport.step.oauth=Authentication
    com.example.plugins.tutorial.jira.csvimport.step.csvSetup=Setup
    com.example.plugins.tutorial.jira.csvimport.step.projectMapping=Projects
    com.example.plugins.tutorial.jira.csvimport.step.customField=Custom Field
    com.example.plugins.tutorial.jira.csvimport.step.fieldMapping=Field mapping
    com.example.plugins.tutorial.jira.csvimport.step.valueMapping=Value mapping
    com.example.plugins.tutorial.jira.csvimport.step.links=Link

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:

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

import com.atlassian.jira.plugins.importer.imports.importer.ImportDataBean;
import com.atlassian.jira.plugins.importer.imports.importer.JiraDataImporter;
import com.atlassian.jira.plugins.importer.web.*;
import com.example.plugins.tutorial.jira.csvimport.web.SimpleCsvSetupPage;
import com.google.common.collect.Lists;
import java.util.List;

public class SimpleCsvImporterController extends AbstractImporterController {
    public static final String IMPORT_CONFIG_BEAN = "com.example.plugins.tutorial.jira.google.csvimport.config";
    public static final String IMPORT_ID = "com.example.plugins.tutorial.jira.google.csvimport.import";
    public SimpleCsvImporterController(JiraDataImporter importer) {
        super(importer, IMPORT_CONFIG_BEAN, IMPORT_ID);
    }
    @Override
    public boolean createImportProcessBean(AbstractSetupPage abstractSetupPage) {
        if (abstractSetupPage.invalidInput()) {
            return false;
        }
        final SimpleCsvSetupPage setupPage = (SimpleCsvSetupPage) abstractSetupPage;
        final SimpleCsvConfigBean configBean = new SimpleCsvConfigBean(new SimpleCsvClient(setupPage.getFilePath()));
        final ImportProcessBean importProcessBean = new ImportProcessBean();
        importProcessBean.setConfigBean(configBean);
        storeImportProcessBeanInSession(importProcessBean);
        return true;
    }
    @Override
    public ImportDataBean createDataBean() throws Exception {
        final SimpleCsvConfigBean configBean = getConfigBeanFromSession();
        return new SimpleCsvDataBean(configBean);
    }
    private SimpleCsvConfigBean getConfigBeanFromSession() {
        final ImportProcessBean importProcessBean = getImportProcessBeanFromSession();
        return importProcessBean != null ? (SimpleCsvConfigBean) importProcessBean.getConfigBean() : null;
    }
    // @Override no override annotation to remain compatible with JIRA 5
    public List<String> getStepNameKeys() {
        return Lists.newArrayList(
                "com.example.plugins.tutorial.jira.csvimport.step.csvSetup",
                "com.example.plugins.tutorial.jira.csvimport.step.projectMapping",
                "com.example.plugins.tutorial.jira.csvimport.step.customField",
                "com.example.plugins.tutorial.jira.csvimport.step.fieldMapping",
                "com.example.plugins.tutorial.jira.csvimport.step.valueMapping",
                "com.example.plugins.tutorial.jira.csvimport.step.links"
        );
    }
    @Override
    public List<String> getSteps() {
        return Lists.newArrayList(SimpleCsvSetupPage.class.getSimpleName(),
                ImporterProjectMappingsPage.class.getSimpleName(),
                ImporterCustomFieldsPage.class.getSimpleName(),
                ImporterFieldMappingsPage.class.getSimpleName(),
                ImporterValueMappingsPage.class.getSimpleName(),
                ImporterLinksPage.class.getSimpleName());
    }
    @Override
    public boolean isUsingConfiguration() {
        return false;
    }
}

Notice that the import controller implements the abstract class AbstractImporterController. Included among the base methods we're overriding, we have:

  • createImportProcessBean() instantiates an ImportProcessBean and AbstractConfigBean instance, 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 importProcessBean in 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 SimpleCsvConfigBean to transfer this object for us. Note that storing importProcessBean in 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: 
    • SimpleCsvSetupPage provides our custom setup page that reads the file name supplied by the user.

    • ImporterProjectMappingsPage is a reused project mapping page that allows the user to select or create a project into which to import the issues.
    • ImporterCustomFieldsPage is a reused custom field mapping page that allows the user to map custom fields from the external issue tracker to JIRA custom fields.
    • ImporterFieldMappingsPage is a reused field value mapping page that allows the user to choose how fields from the external system are mapped to JIRA issues.
    • ImporterValueMappingsPage is a reused field value mapping page that allows the user to select how particular field values are mapped to JIRA issues. 
    • ImporterLinksPage is 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.

  • isUsingConfigBean returns 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 SimpleCsvConfigBean class, com.example.plugins.tutorial.jira.csvimport.SimpleCsvConfigBean with the following code:

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

import com.atlassian.jira.plugins.importer.external.beans.ExternalCustomField;
import com.atlassian.jira.plugins.importer.imports.config.ValueMappingDefinition;
import com.atlassian.jira.plugins.importer.imports.config.ValueMappingDefinitionsFactory;
import com.atlassian.jira.plugins.importer.imports.config.ValueMappingHelper;
import com.atlassian.jira.plugins.importer.imports.config.ValueMappingHelperImpl;
import com.atlassian.jira.plugins.importer.imports.importer.AbstractConfigBean2;
import com.example.plugins.tutorial.jira.csvimport.mapping.PriorityValueMappingDefinition;
import com.google.common.collect.Lists;
import java.util.List;

public class SimpleCsvConfigBean extends AbstractConfigBean2 {
    private SimpleCsvClient csvClient;
    public SimpleCsvConfigBean(SimpleCsvClient csvClient) {
        this.csvClient = csvClient;
    }
    @Override
    public List<String> getExternalProjectNames() {
        return Lists.newArrayList("project");
    }
    @Override
    public List<ExternalCustomField> getCustomFields() {
        return Lists.newArrayList(ExternalCustomField.createText("cf", "custom field"));
    }
    @Override
    public List<String> getLinkNamesFromDb() {
        return Lists.newArrayList("link");
    }
    @Override
    public ValueMappingHelper initializeValueMappingHelper() {
        final ValueMappingDefinitionsFactory mappingDefinitionFactory = new ValueMappingDefinitionsFactory() {
            public List<ValueMappingDefinition> createMappingDefinitions(ValueMappingHelper valueMappingHelper) {
                final List<ValueMappingDefinition> mappings = Lists.newArrayList();
                mappings.add(new PriorityValueMappingDefinition(getCsvClient(), getConstantsManager()));
                return mappings;
            }
        };
       return new ValueMappingHelperImpl(getWorkflowSchemeManager(),
                getWorkflowManager(), mappingDefinitionFactory, getConstantsManager());
    }
    public SimpleCsvClient getCsvClient() {
        return csvClient;
    }
}

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:

  • getExternalProjectNames returns 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.
  • getCustomFields returns a list of custom fields defined in the external system. You can use factory methods in ExternalCustomField to 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.
  • getLinkNamesFromDb returns link names from the external system. The Importer Links page shows these links, and allows user to map them to JIRA link types.
  • initializeValueMappingHelper initializes the protected field valueMappingHelper. ValueMappingHelper contains 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 PriorityValueMappingDefinition class next.

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:

package com.example.plugins.tutorial.jira.csvimport.mapping;

import com.atlassian.jira.config.ConstantsManager;
import com.atlassian.jira.issue.IssueConstant;
import com.atlassian.jira.issue.IssueFieldConstants;
import com.atlassian.jira.plugins.importer.imports.config.ValueMappingDefinition;
import com.atlassian.jira.plugins.importer.imports.config.ValueMappingEntry;
import com.example.plugins.tutorial.jira.csvimport.Issue;
import com.example.plugins.tutorial.jira.csvimport.SimpleCsvClient;
import com.google.common.base.Function;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Set;

public class PriorityValueMappingDefinition implements ValueMappingDefinition {
    private final SimpleCsvClient simpleCsvClient;
    private final ConstantsManager constantsManager;
    public PriorityValueMappingDefinition(SimpleCsvClient simpleCsvClient, ConstantsManager constantsManager) {
        this.simpleCsvClient = simpleCsvClient;
        this.constantsManager = constantsManager;
    }
    @Override
    public String getJiraFieldId() {
        return IssueFieldConstants.PRIORITY;
    }
    @Override
    public Collection<ValueMappingEntry> getTargetValues() {
        return new ArrayList<ValueMappingEntry>(Collections2.transform(constantsManager.getPriorityObjects(),
                new Function<IssueConstant, ValueMappingEntry>() {
                    public ValueMappingEntry apply(IssueConstant from) {
                        return new ValueMappingEntry(from.getName(), from.getId());
                    }
                }));
    }
    @Override
    public boolean canBeBlank() {
        return false;
    }
    @Override
    public boolean canBeCustom() {
        return true;
    }
    @Override
    public boolean canBeImportedAsIs() {
        return true;
    }
    @Override
    public String getExternalFieldId() {
        return "priority";
    }
    @Override
    public String getDescription() {
        return null;
    }
    @Override
    public Set<String> getDistinctValues() {
        return Sets.newHashSet(Iterables.transform(simpleCsvClient.getInternalIssues(),
                new Function<Issue, String>() {
                    @Override
                    public String apply(Issue from) {
                        return from.getPriority();
                    }
                }));
    }
    @Override
    public Collection<ValueMappingEntry> getDefaultValues() {
        return new ImmutableList.Builder<ValueMappingEntry>().add(
                new ValueMappingEntry("Low", IssueFieldConstants.TRIVIAL_PRIORITY_ID),
                new ValueMappingEntry("Normal", IssueFieldConstants.MINOR_PRIORITY_ID),
                new ValueMappingEntry("High", IssueFieldConstants.MAJOR_PRIORITY_ID),
                new ValueMappingEntry("Urgent", IssueFieldConstants.CRITICAL_PRIORITY_ID),
                new ValueMappingEntry("Immediate", IssueFieldConstants.BLOCKER_PRIORITY_ID)
        ).build();
    }
    @Override
    public boolean isMandatory() {
        return false;
    }
}

The class implements these methods:

  • getJiraFieldId returns values from IssueFieldConstants, which determines which JIRA field this mapping relates to.
  • getTargetValues returns values from JIRA for this custom field. We use constantsManager to obtain these values.
  • canBeBlank if true, this field is optional in the UI. 
  • canBeCustom if 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.
  • canBeImportedAsIs returns 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). 
  • getExternalFieldId is the name of this field in the external system.
  • getDescription is an optional description displayed to the user in the Importer Value Mappings page.
  • getDistinctValues returns only distinct values from the external sytem.
  • getDefaultValues returns the default mappings between values in the external system and in JIRA.
  • isMandatory should return true if 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:

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

import com.atlassian.jira.issue.IssueFieldConstants;
import com.atlassian.jira.plugins.importer.external.CustomFieldConstants;
import com.atlassian.jira.plugins.importer.external.beans.*;
import com.atlassian.jira.plugins.importer.imports.config.ValueMappingHelper;
import com.atlassian.jira.plugins.importer.imports.importer.AbstractDataBean;
import com.atlassian.jira.plugins.importer.imports.importer.ImportLogger;
import com.google.common.base.Function;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.apache.commons.lang.StringUtils;
import java.util.*;

public class SimpleCsvDataBean extends AbstractDataBean<SimpleCsvConfigBean> {
    private final SimpleCsvClient csvClient;
    private final SimpleCsvConfigBean configBean;
    private final ValueMappingHelper valueMappingHelper;
    public SimpleCsvDataBean(SimpleCsvConfigBean configBean) {
        super(configBean);
        this.configBean = configBean;
        this.csvClient = configBean.getCsvClient();
        this.valueMappingHelper = configBean.getValueMappingHelper();
    }
    @Override
    public Set<ExternalUser> getRequiredUsers(Collection<ExternalProject> projects, ImportLogger importLogger) {
        return getAllUsers(importLogger);
    }
    @Override
    public Set<ExternalUser> getAllUsers(ImportLogger log) {
        return Sets.newHashSet(Iterables.transform(csvClient.getInternalIssues(), new Function<Issue, ExternalUser>() {
            @Override
            public ExternalUser apply(Issue from) {
                return new ExternalUser(from.getAssignee(), from.getAssignee());
            }
        }));
    }
    @Override
    public Set<ExternalProject> getAllProjects(ImportLogger log) {
        final ExternalProject project = new ExternalProject(
                configBean.getProjectName("project"),
                configBean.getProjectKey("project"));
        project.setExternalName("project");
        return Sets.newHashSet(project);
    }
    @Override
    public Iterator<ExternalIssue> getIssuesIterator(ExternalProject externalProject, ImportLogger importLogger) {
        return Iterables.transform(csvClient.getInternalIssues(), new Function<Issue, ExternalIssue>() {
            @Override
            public ExternalIssue apply(Issue from) {
                final ExternalIssue externalIssue = new ExternalIssue();
                final String priorityMappedValue = valueMappingHelper.getValueMapping("priority", from.getPriority());
                externalIssue.setPriority(StringUtils.isBlank(priorityMappedValue) ? from.getPriority() : priorityMappedValue);
                externalIssue.setExternalId(from.getIssueId());
                externalIssue.setSummary(from.getSummary());
                externalIssue.setAssignee(from.getAssignee());
                externalIssue.setExternalCustomFieldValues(Lists.newArrayList(
                        new ExternalCustomFieldValue(configBean.getFieldMapping("cf"), CustomFieldConstants.TEXT_FIELD_TYPE,
                                CustomFieldConstants.TEXT_FIELD_SEARCHER, from.getCustomField())));
                externalIssue.setStatus(String.valueOf(IssueFieldConstants.OPEN_STATUS_ID));
                externalIssue.setIssueType(IssueFieldConstants.BUG_TYPE);
                return externalIssue;
            }
        }).iterator();
    }
    @Override
    public Collection<ExternalLink> getLinks(ImportLogger log) {
        final List<ExternalLink> externalLinks = Lists.newArrayList();
        final String linkName = configBean.getLinkMapping("link");
        for (Issue issue : csvClient.getInternalIssues()) {
            if (StringUtils.isNotBlank(issue.getLinkedIssueId())) {
                externalLinks.add(new ExternalLink(linkName, issue.getIssueId(), issue.getLinkedIssueId()));
            }
        }
        return externalLinks;
    }
    @Override
    public long getTotalIssues(Set<ExternalProject> selectedProjects, ImportLogger log) {
        return csvClient.getInternalIssues().size();
    }
    @Override
    public String getUnusedUsersGroup() {
        return "simple_csv_import_unused";
    }
    @Override
    public void cleanUp() {
    }
    @Override
    public String getIssueKeyRegex() {
        return null;
    }
    @Override
    public Collection<ExternalVersion> getVersions(ExternalProject externalProject, ImportLogger importLogger) {
        return Collections.emptyList();
    }
    @Override
    public Collection<ExternalComponent> getComponents(ExternalProject externalProject, ImportLogger importLogger) {
        return Collections.emptyList();
    }
}

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:

  • getRequiredUsers returns 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.
  • getAllUsers returns the users from the external system.
  • getAllProjects returns all projects from the external system. In our case we return one project. Notice that we use configBean.getProjectName("project") and configBean.getProjectKey("project") to get values defined by the user for the project named "project", as specified on the Importer Project Mappings page.

  • getIssuesIterator returns 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:
    • We use valueMappingHelper.getValueMapping("priority", 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 PriorityValueMappingDefinition class. 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.

    • We use configBean.getFieldMapping("cf") to get the user-selected name for custom field. We use the same id "cf" that was defined in SimpleCsvConfigBean#getCustomFields. 

  • getLinks returns all links between issues in the external system. The configBean.getLinkMapping("link") call gets the name of the link that the user has selected, passed as the "link" parameter, and which is defined in SimpleCsvConfigBean#getLinkNamesFromDb.  

  • getTotalIssues returns all issues for the selected projects.
  • getUnusedUsersGroup returns JIRA group that will be created for users that are not active.
  • cleanUp is called after the import has finished. You can clean up used resources here.
  • getIssueKeyRegex can 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").
  • getVersions returns all versions for the given project. In our example we do not handle versions, so we just return an empty collection.
  • getComponents returns 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:

  1. Create the class com.example.plugins.tutorial.jira.csvimport.web.SimpleCsvSetupPage with the following content

    package com.example.plugins.tutorial.jira.csvimport.web;
    
    import com.atlassian.jira.plugins.importer.extensions.ImporterController;
    import com.atlassian.jira.plugins.importer.tracking.UsageTrackingService;
    import com.atlassian.jira.plugins.importer.web.AbstractSetupPage;
    import com.atlassian.jira.plugins.importer.web.ConfigFileHandler;
    import com.atlassian.jira.security.xsrf.RequiresXsrfCheck;
    import com.atlassian.plugin.PluginAccessor;
    import com.atlassian.plugin.web.WebInterfaceManager;
    import com.example.plugins.tutorial.jira.csvimport.SimpleCsvClient;
    
    public class SimpleCsvSetupPage extends AbstractSetupPage {
        private String filePath;
        public SimpleCsvSetupPage(UsageTrackingService usageTrackingService, WebInterfaceManager webInterfaceManager, PluginAccessor pluginAccessor) {
            super(usageTrackingService, webInterfaceManager, pluginAccessor);
        }
        @Override
        public String doDefault() throws Exception {
            if (!isAdministrator()) {
                return "denied";
            }
            final ImporterController controller = getController();
            if (controller == null) {
                return RESTART_NEEDED;
            }
            return INPUT;
        }
        @Override
        @RequiresXsrfCheck
        protected String doExecute() throws Exception {
            final ImporterController controller = getController();
            if (controller == null) {
                return RESTART_NEEDED;
            }
            if (!isPreviousClicked() && !controller.createImportProcessBean(this)) {
                return INPUT;
            }
            return super.doExecute();
        }
        @Override
        protected void doValidation() {
            if (isPreviousClicked()) {
                return;
            }
            try {
                new SimpleCsvClient(filePath);
            } catch (RuntimeException e) {
                addError("filePath", e.getMessage());
            }
            super.doValidation();
        }
        public String getFilePath() {
            return filePath;
        }
        public void setFilePath(String filePath) {
            this.filePath = filePath;
        }
    }

    Notice a few things:

    • The @RequiresXsrfCheck annotation for the doExecute method 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 SimpleCsvClient to see whether the provided file path is valid.
  2. Back in the descriptor file, atlassian-plugin.xml, add the following webwork action module:

    <webwork1 key="actions" name="Actions">
        <actions>
            <action name="com.example.plugins.tutorial.jira.csvimport.web.SimpleCsvSetupPage" alias="SimpleCsvSetupPage">
                <view name="input">templates/csvSetupPage.vm</view>
                <view name="denied">/secure/views/securitybreach.jsp</view>
                <view name="restartimporterneeded">/templates/admin/views/restartneeded.vm</view>
            </action>
        </actions>
    </webwork1>

    The module references the setup page class you just added, and the Velocity template that will display our form. It's important for the alias attribute 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.
  3. Create the Velocity template for our setup page by adding the following code to a file named csvSetupPage.vm in a new directory, src/main/resources/templates:

    #parse('/templates/admin/views/common/import-header.vm')
    #set ($auiparams = 
    	$map.build('name', 'filePath', 'label', "CSV File path", 'size', 60, 'value', $action.filePath, 'required', true))
    #parse("/templates/standard/textfield.vm")
    
    #parse("/templates/admin/views/common/standardSetupFooter.vm")
    
    

    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.vm adds a standard header for all JIRA importers.
    • textfield.vm renders simple HTML input with type text and parameters provided in the auiparams variable.

    • standardSetupFooter.vm puts 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 issueId, summary, priority, assignee, customField, and inkedIssueId.

Create com.example.plugins.tutorial.jira.csvimport.SimpleCsvClient to simulate our external issue tracker:

SimpleCsvClient
package com.example.plugins.tutorial.jira.csvimport;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import org.apache.commons.io.IOUtils;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
 * we assume data is in constant format
 * issueId,summary, priority, assignee, customField,linkedIssueId
 */
public class SimpleCsvClient {
    private List<Issue> internalIssues;
    public List<Issue> getInternalIssues() {
        return internalIssues;
    }
    public SimpleCsvClient(String filePath) {
        try {
            final FileInputStream input = new FileInputStream(filePath);
            try {
                final List<String> content = IOUtils.readLines(input);
                internalIssues = Lists.newArrayListWithExpectedSize(content.size());
                final Splitter splitter = Splitter.on(",").trimResults();
                for (int i = 0, contentSize = content.size(); i < contentSize; i++) {
                    final String issueLine = content.get(i);
                    final ArrayList<String> values = Lists.newArrayList(splitter.split(issueLine));
                    if (values.size() != 6) {
                        throw new RuntimeException("Invalid line " + (i + 1) + " " + issueLine + " should contain six values");
                    }
                    internalIssues.add(new Issue(values));
                }
            } finally {
                input.close();
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

Not much of interest here. The main point to know for now is that  SimpleCsvClient is capable of returning a list of issues containing six values:  issueIdsummarypriorityassigneecustomFieldlinkedIssueId.

Now create the issue class, com.example.plugins.tutorial.jira.csvimport.Issue:

package com.example.plugins.tutorial.jira.csvimport;
import java.util.List;
/**
 * Helper class to convert issues from csv values
 */
public class Issue {
    private final String issueId;
    private final String summary;
    private final String priority;
    private final String assignee;
    private final String customField;
    private final String linkedIssueId;
    public Issue(List<String> values) {
        issueId = values.get(0);
        summary = values.get(1);
        priority = values.get(2);
        assignee = values.get(3);
        customField = values.get(4);
        linkedIssueId = values.get(5);
    }
    public String getIssueId() {
        return issueId;
    }
    public String getSummary() {
        return summary;
    }
    public String getPriority() {
        return priority;
    }
    public String getAssignee() {
        return assignee;
    }
    public String getCustomField() {
        return customField;
    }
    public String getLinkedIssueId() {
        return linkedIssueId;
    }
}

Create data for our external system. Add the following data to a file named test.csv in the test/resources folder:

1,summary 1,Low,user1,value1,2
2,summary 2,High,user1,value2,3
3,summary 3,Low,user2,value1,
4,summary 4,Unknown,user3,value3,

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:

  1. In a terminal window, navigate to the plugin root folder (where the pom.xml file is) and run atlas-run (or atlas-debug if you might want to launch the debugger in your IDE).
    JIRA takes a few minutes to download and start up.
  2. When JIRA finishes starting up, open the JIRA home page in a browser. JIRA prints the URL to use in the console output.
  3. Log in with the default user and password combination, admin/admin.
  4. 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). 
  5. From the JIRA header, click Projects > Import External Project.

  6. Find your importer in the list. As you may recognize, it's the one named Simple CSV Importer:

  7. Click on the importer to launch the wizard. You should see the first step in the wizard, the setup page you created:


  8. In the CSV File path field, enter the path to the file and click Next. For example:
    //home/atlas/atlassian/tutorial-jira-simple-csv-importer/src/test/resources/test.csv

  9. Continue working through the wizard. In step 2, be sure to map the imported issues to the software development project you created.
  10. 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.

At this point you can leave JIRA running and use FastDev or the atlas-cli and pi commands to reinstall your plugin without having to restart JIRA.

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.

  1. Add component import to atlassian-plugin.xml. This will import ConfigFileHandler which will help us handle serialization and desalinization of importer configuration.

      <component-import key="configFileHandler"
                          interface="com.atlassian.jira.plugins.importer.web.ConfigFileHandler"/>
  2. Extend the SimpleCsvSetupPage class as follows: 
    1. Add a constructor parameter and field for ConfigFileHandler:

      ...
      private final ConfigFileHandler configFileHandler;
      public SimpleCsvSetupPage(UsageTrackingService usageTrackingService, WebInterfaceManager webInterfaceManager, 
      							PluginAccessor pluginAccessor, ConfigFileHandler configFileHandler) {
      	super(usageTrackingService, webInterfaceManager, pluginAccessor);
      	this.configFileHandler = configFileHandler;
      }
      ...

      This allows us to use configFileHandler to validate the configuration file provided by the user.

    2. Add this validation code as the last line of the doValidation method of SimpleCsvSetupPage:

      ...
      protected void doValidation() {
      ...
      	configFileHandler.verifyConfigFileParam(this);
      }

      Expand the following code block to see what the entire class should now look like:

      package com.example.plugins.tutorial.jira.csvimport.web;
      import com.atlassian.jira.plugins.importer.extensions.ImporterController;
      import com.atlassian.jira.plugins.importer.tracking.UsageTrackingService;
      import com.atlassian.jira.plugins.importer.web.AbstractSetupPage;
      import com.atlassian.jira.plugins.importer.web.ConfigFileHandler;
      import com.atlassian.jira.security.xsrf.RequiresXsrfCheck;
      import com.atlassian.plugin.PluginAccessor;
      import com.atlassian.plugin.web.WebInterfaceManager;
      import com.example.plugins.tutorial.jira.csvimport.SimpleCsvClient;
      public class SimpleCsvSetupPage extends AbstractSetupPage {
          private String filePath;
          private final ConfigFileHandler configFileHandler;
          public SimpleCsvSetupPage(UsageTrackingService usageTrackingService, WebInterfaceManager webInterfaceManager, PluginAccessor pluginAccessor, ConfigFileHandler configFileHandler) {
              super(usageTrackingService, webInterfaceManager, pluginAccessor);
              this.configFileHandler = configFileHandler;
          }
          @Override
          public String doDefault() throws Exception {
              if (!isAdministrator()) {
                  return "denied";
              }
              final ImporterController controller = getController();
              if (controller == null) {
                  return RESTART_NEEDED;
              }
              return INPUT;
          }
          @Override
          @RequiresXsrfCheck
          protected String doExecute() throws Exception {
              final ImporterController controller = getController();
              if (controller == null) {
                  return RESTART_NEEDED;
              }
              if (!isPreviousClicked() && !controller.createImportProcessBean(this)) {
                  return INPUT;
              }
              return super.doExecute();
          }
          @Override
          protected void doValidation() {
              if (isPreviousClicked()) {
                  return;
              }
              try {
                  new SimpleCsvClient(filePath);
              } catch (RuntimeException e) {
                  addError("filePath", e.getMessage());
              }
              super.doValidation();
              configFileHandler.verifyConfigFileParam(this);
          }
          public String getFilePath() {
              return filePath;
          }
          public void setFilePath(String filePath) {
              this.filePath = filePath;
          }
      }
  3. Now extend the SimpleCsvImporterController class as follows: 
    1. First add constructor parameter and field for ConfigFileHandler.

      ...
      private final ConfigFileHandler configFileHandler;
      public SimpleCsvImporterController(JiraDataImporter importer, ConfigFileHandler configFileHandler) {
      	super(importer, IMPORT_CONFIG_BEAN, IMPORT_ID);
      	this.configFileHandler = configFileHandler;
      }
      ...
    2. Populate the configuration file in createImportProcessBean method of SimpleCsvImporterController, as follows:

      public boolean createImportProcessBean(AbstractSetupPage abstractSetupPage) {
         ...
         final SimpleCsvConfigBean configBean = new SimpleCsvConfigBean(new SimpleCsvClient(setupPage.getFilePath()));
              final ImportProcessBean importProcessBean = new ImportProcessBean();
              if (!configFileHandler.populateFromConfigFile(setupPage, configBean)) {
                  return false;
              }
              importProcessBean.setConfigBean(configBean);
      ...
      }
    3. Finally remove the isUsingConfiguration method. 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.
  4. 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.vm file before the standardSetupFooter.vm directive:

    #parse('/templates/admin/views/common/configFile.vm')

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. 

  1. Update your pom.xml with some dependencies required for the tests:

    <dependency>
    	<groupId>com.atlassian.jira</groupId>
        <artifactId>atlassian-jira-pageobjects</artifactId>
        <version>${jira.version}</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>com.atlassian.jira.plugins</groupId>
        <artifactId>jira-importers-plugin</artifactId>
        <version>${jim.version}</version>
        <classifier>tests</classifier>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>com.atlassian.jira.tests</groupId>
        <artifactId>jira-testkit-client</artifactId>
        <version>${testkit.version}</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.codehaus.jackson</groupId>
        <artifactId>jackson-core-asl</artifactId>
        <version>1.9.1</version>
    </dependency>
    <dependency>
        <groupId>org.codehaus.jackson</groupId>
        <artifactId>jackson-xc</artifactId>
        <version>1.9.1</version>
    </dependency>
    <dependency>
        <groupId>org.codehaus.jackson</groupId>
        <artifactId>jackson-jaxrs</artifactId>
        <version>1.9.1</version>
    </dependency>
    <dependency>
       <groupId>org.codehaus.jackson</groupId>
       <artifactId>jackson-mapper-asl</artifactId>
       <version>1.9.1</version>
    </dependency>
  2. Also in the POM, add a plugins property to include the JIRA testkit plugin:

    <properties>
    	...
    	<testkit.version>5.0-m13</testkit.version>
    	<plugins>com.atlassian.jira.plugins:jira-importers-plugin:${jim.version},com.atlassian.jira.tests:jira-testkit-plugin:${testkit.version}</plugins>
    	</properties>

    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.

  3. 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:

    package com.example.plugins.jira.csvimport.po;
     
    import com.atlassian.jira.plugins.importer.po.common.AbstractImporterWizardPage;
    import com.atlassian.jira.plugins.importer.po.common.ImporterProjectsMappingsPage;
    import org.junit.Assert;
    import org.openqa.selenium.WebElement;
    import org.openqa.selenium.support.FindBy;
    
     
    public class SimpleCsvSetupPage extends AbstractImporterWizardPage {
    	
    	@FindBy(id = "filePath")
    	WebElement filePath;
    	
    	public SimpleCsvSetupPage setFilePath(String filePath) {
    		this.filePath.sendKeys(filePath);
    		return this;
    	}
    
    	@Override
        public String getUrl() {
            return "/secure/admin/views/SimpleCsvSetupPage!default.jspa?externalSystem=" +
                    "com.example.plugins.tutorial.jira.simple-csv-importer:SimpleCSVImporterKey";
        }
    	public ImporterProjectsMappingsPage next() {
    		Assert.assertTrue(nextButton.isEnabled());
    		nextButton.click();
    		return pageBinder.bind(ImporterProjectsMappingsPage.class);
    	}
    }
  4. Still in the tests directory, create a test class it.com.example.plugins.jira.csvimport.TestSimpleCsvImporter.

    package it.com.example.plugins.jira.csvimport;
     
    import com.atlassian.jira.pageobjects.JiraTestedProduct;
    import com.atlassian.jira.pageobjects.config.EnvironmentBasedProductInstance;
    import com.atlassian.jira.plugins.importer.po.ExternalImportPage;
    import com.atlassian.jira.plugins.importer.po.common.ImporterLinksPage;
    import com.atlassian.jira.plugins.importer.po.common.ImporterProjectsMappingsPage;
    import com.atlassian.jira.testkit.client.Backdoor;
    import com.atlassian.jira.testkit.client.restclient.SearchRequest;
    import com.atlassian.jira.testkit.client.restclient.SearchResult;
    import com.atlassian.jira.testkit.client.util.TestKitLocalEnvironmentData;
    import com.example.plugins.jira.csvimport.po.SimpleCsvSetupPage;
    import org.junit.Before;
    import org.junit.Test;
    import static org.junit.Assert.*;
    import static org.junit.matchers.JUnitMatchers.hasItem;
    
    public class TestSimpleCsvImporter {
    
    	private JiraTestedProduct jira;
    	private Backdoor backdoor;
    	
    	@Before
    	public void setUp() {
    		backdoor = new Backdoor(new TestKitLocalEnvironmentData());
    		backdoor.restoreBlankInstance();
    		jira = new JiraTestedProduct(null, new EnvironmentBasedProductInstance());
    	}
    
    	
    	@Test
    	public void testSimpleCsvImporterAttachedToConfigPage() {
    		assertThat(jira.gotoLoginPage().loginAsSysAdmin(ExternalImportPage.class).getImportersOrder(),
    				hasItem("SimpleCSVImporter"));
    	}
    
    	
    	@Test
    	public void testSimpleCsvImporterWizard() {
    		backdoor.issueLinking().enable();
    		backdoor.issueLinking().createIssueLinkType("Related", "related", "related to");
    		final SimpleCsvSetupPage setupPage = jira.gotoLoginPage().loginAsSysAdmin(SimpleCsvSetupPage.class);
    		final String file = getClass().getResource("/test.csv").getFile();
    		final ImporterProjectsMappingsPage projectsPage = setupPage.setFilePath(file).next();
    		projectsPage.createProject("project", "JIRA Project", "PRJ");
    		final ImporterLinksPage linksPage = projectsPage.next().next().next().next();
    		linksPage.setSelect("link", "Related");
    		assertTrue(linksPage.next().waitUntilFinished().isSuccess());
    		final SearchResult search = backdoor.search().getSearch(new SearchRequest().jql(""));
    		assertEquals((Integer) 4, search.total);
    	}
    }
    

    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 setUp method 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.

  5. 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 test/resources/test.csv:

    1,summary 1,Low,user1,value1,2
    2,summary 2,High,user1,value2,3
    3,summary 3,Low,user2,value1,
    4,summary 4,Unknown,user3,value3,
  6. You also need to create a directory, test/xml, for the tests to use. It can be empty.
  7. Now you're ready to run the test! In the console, enter the command atlas-integration-test.

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. 

Was this page helpful?

Have a question about this article?

See questions about this article

Powered by Confluence and Scroll Viewport