Documentation for JIRA 4.3. Documentation for other versions of JIRA is available too.

Summary

As of JIRA 3.13, you can now import single projects from an XML backup. The import includes the projects issues and all related issue data.

In order to import custom field data the CustomFieldType class that defines the custom field must implement a new interface, ProjectImportableCustomField. All the existing JIRA system custom fields and the custom fields included in supported JIRA plugins have been modified to implement this interface.

As of July 18th, 2008, the following plugins have been modified to support the new interface:

You only need to be concerned about this document if you are using a custom field that may have been developed before 3.13, or without taking into account the ProjectImportableCustomField interface AND you would like this custom fields data to be imported when performing a project import.

Making a field project importable

ProjectImportableCustomField

The ProjectImportableCustomField interface is an optional interface that can be implemented when creating a JIRA CustomFieldType. If this interface is not implemented then any custom fields of this type will be ignored when performing a project import. This will in no way stop the import from proceeding but it will stop the custom field values from being included in the projects issue data.

The interface looks like this:

public interface ProjectImportableCustomField
{
    ProjectCustomFieldImporter getProjectImporter();
}

ProjectCustomFieldImporter

As you can see implementing the interface commits you to creating an instance of a ProjectCustomFieldImporter. The ProjectCustomFieldImporter is the class that does the actual work for a custom field when being imported.

A ProjectCustomFieldImporter has two jobs:

  1. Validate whether or not the custom field values are relevant for the running instance of JIRA.
  2. Transform the custom field values (if required) so that the values will be valid in the running instance of JIRA.

The ProjectCustomFieldImporter has two methods, canMapImportValue and getMappedImportValue:

MessageSet canMapImportValue(ProjectImportMapper projectImportMapper, ExternalCustomFieldValue customFieldValue, FieldConfig fieldConfig, final I18nHelper i18n);

MappedCustomFieldValue getMappedImportValue(ProjectImportMapper projectImportMapper, ExternalCustomFieldValue customFieldValue, FieldConfig fieldConfig);

canMapImportValue

The project import will run through all the custom field values that are relevant to a custom field and, if the custom field type is project importable, will first invoke the canMapImportValue. At this time the custom field needs to decide if there is a problem with the provided custom field value.

The ProjectCustomFieldImporter implementation can communicate two levels of messages, warnings and errors. An error will make it so the project import can not continue. The error message will be shown on the pre-import summary screen associated with the custom field. Error messages should be descriptive to the extent that a user can, hopefully, correct the error so that the import will eventually proceed. A warning will NOT cause the project import to stop. A warning message is used to alert the user to some aspect of the data that you may want them to know BEFORE they decide if they want to perform the import or not. The warning message will be shown on the pre-import screen.

Both warning and error messages are added to a MessageSet. One characteristic of the MessageSet is that if you add 2 or more of the exact same textual errors, they will only be reported as a single errors. For example:

Your select custom field in your running instance of JIRA does not have an option configured for the value "test option 1".

In your backup data you have 10 references to the custom field value "test option 1"

canMapImportValue will be called 10 times with this value. Each time you add an error message of "Field 'My Select Field' can not import value 'test option 1', the field does not have this option."

In the pre-import UI, this message will be shown one time under a sub-heading of the custom fields name.

If you add an error message to the message set when the canMapImportValue method is called then the getMappedImportValue will never be called.

If you are a custom field that is storing system values (e.g. a group custom field). You can also get the system to perform validation for you by alerting the system that your "system" value is required. This is done through the mappers that are passed in via the ProjectImportMapper.

Each individual mapper has a flagValueAsRequired method. Calling this records the fact that the value is a system value that must be present for the import to proceed. If the value is not present then the project import will report an error on the pre-import summary screen under the sub-heading of the system field (e.g. group).

For example if you were a group custom field:

projectImportMapper.getGroupMapper().flagValueAsRequired(groupname);

getMappedImportValue

This method is called once the actual import is being performed.

The main objective of this method is to "transform" the existing custom field value in any way that the ProjectCustomFieldImporter implementation deems necessary.

If, for some reason, you do not want the value to be stored then you can return a MappedCustomFieldValue with a null value.

Otherwise you should fill the MappedCustomFieldValue with the value you want stored in the running JIRA instance.

It should almost always be the case that if this method is called (i.e. canMapImportValue did not generate an error) that a MappedCustomFieldValue can be created.

ProjectImportMapper

Many custom fields rely on system information. The ProjectImportMapper gives you access to system values (statues, priorities, project roles, etc.) that the Importer has already mapped and validated. This allows you to find the ID of a system value in the running JIRA instance corresponding to the "old" ID (usually your custom field value).

Custom fields are therefore provided with a ProjectImportMapper when the canMapImportValue and getMappedImportValue methods are called. This mapper contains individual mappers for different system fields. The mapper is filled with information about the values from the backup XML data and mapped values in the running JIRA instance. These mapped values can be useful in deciding if a custom field type can map a value and what that mapping would be.

Sometimes you may want to not perform a validation check yourself, instead you want to communicate to the Project Import that the system value is "in use" or "required" and then the Project Import can validate if that system value is correct or not.

One example of where you want to let the Project Importer do validation is Users and the UserCFType.

The project importer will try to create any users that are required but do not exist in the running JIRA instance. Therefore, we want to let the system know about any users that a custom field may reference.

The UserCustomFieldImporter looks like this:

public MessageSet canMapImportValue(final ProjectImportMapper projectImportMapper, final ExternalCustomFieldValue customFieldValue, final FieldConfig fieldConfig, final I18nHelper i18n)
{
    String username = customFieldValue.getValue();
    // ignore empty username including null and empty String.
    if (username != null && username.length() > 0)
    {
        // Flag the username as required
        projectImportMapper.getUserMapper().flagValueAsRequired(username);
        // We don't check the Mapper directly if the username can be mapped, because Users can sometimes be automatically imported
        // during the Project Import.
    }
    return null;
}

public MappedCustomFieldValue getMappedImportValue(final ProjectImportMapper projectImportMapper, final ExternalCustomFieldValue customFieldValue, final FieldConfig fieldConfig)
{
    // We don't actually map Users, we just use the same username.
    return new MappedCustomFieldValue(customFieldValue.getValue());
}

Notice the call to projectImportMapper.getUserMapper().flagValueAsRequired(username). If you want to alert a mapper that a field value is required for the import to continue you can call this method on the provided mapper.

Most of the time, you are not storing system field values so, you should just add an error message yourself. Users are a very special case since the importer will create them if they are missing. Do not raise an error if you are a user custom field, instead flag the value as required in the UserMapper and the import will handle the creation/validation of the user for you.

One example of just using a mapper and doing the validation yourself is Project information and the ProjectCFType.

For example:

The Project custom field stores the id of a project within JIRA. When importing a project from a backup there is no guarantee that the custom fields value will make any sense in the running JIRA instance.

In the project custom fields ProjectCustomFieldImporter implementation of canMapImportValue we lookup the projectMapper and see if there is a mapped value for the custom field value (old project id):

projectImportMapper.getProjectMapper()
final String mappedId = projectMapper.getMappedId(valueAsIntString);

The ProjectImport has already done the trouble to validate and map the values found in the system mappers so the custom fields can use this information to decide if their values are valid. In the case of the project custom field if there is not a mapped ID we choose to add a warning to the message set to let the users know that the unmapped values will be dropped on import.

Not all ProjectCustomFieldImporter's will need to use the ProjectImportMapper. It may be the case that the custom field has no need of system information.

NoTransformationCustomFieldImporter

The majority of CustomFieldType's store values that have no dependency on the running JIRA instances configuration (e.g. text custom fields, date custom fields, number custom fields, etc.).

In these cases we just want to pass the existing value strait through the importer.

We have created an implementation of ProjectCustomFieldImporter that does exactly this, its called NoTransformationCustomFieldImporter. If you custom field simply wants its existing value imported into the new system then you should use this implementation.

Example: Select custom field ProjectCustomFieldImporter

The SelectCFType in JIRA stores a string which relates to a configured custom field option in JIRA. A select custom field that references a value that does not have a corresponding option in the custom field configurations will not be shown on the JIRA view issue screen.

THEREFORE, when importing select custom field values you DO NOT want to import the value if there is not a corresponding option for that select custom field.

The code for the select custom field ProjectCustomFieldImporter looks like this:

public class SelectCustomFieldImporter implements ProjectCustomFieldImporter
{
    private final OptionsManager optionsManager;

    public SelectCustomFieldImporter(OptionsManager optionsManager)
    {
        this.optionsManager = optionsManager;
    }

    public MessageSet canMapImportValue(final ProjectImportMapper projectImportMapper, final ExternalCustomFieldValue customFieldValue, final FieldConfig fieldConfig, final I18nHelper i18n)
    {
        final String value = customFieldValue.getValue();
        // Get this custom field's "valid" options and see if the value is one of them
        final Options options = optionsManager.getOptions(fieldConfig);
        if (options.getOptionForValue(value, null) == null)
        {
            // If an option does not exist for the value we are looking at then log an error and stop the import
            MessageSet messageSet = new MessageSetImpl();
            messageSet.addErrorMessage(i18n.getText("admin.errors.project.import.custom.field.option.does.not.exist", fieldConfig.getCustomField().getName(), value));
            return messageSet;
        }
        return null;
    }

    public MappedCustomFieldValue getMappedImportValue(final ProjectImportMapper projectImportMapper, final ExternalCustomFieldValue customFieldValue, final FieldConfig fieldConfig)
    {
        // Since this method will never be called without a successful call to canMap we can safely just pass the
        // value back out.
        return new MappedCustomFieldValue(customFieldValue.getValue());
    }
}

Take careful notice of how in the canMapImportValue method we use the provided FieldConfig (this provides the "context" that the custom field is being used in) to find the configured Options for this custom field.

We next try to find the option that corresponds to the string custom field value we are provided. If we do not find this option in the running JIRA instance we will add an internationalized error message (please note the I18nHelper provided is the I18nHelper configured for your custom field, so if you want to provide an I18n'ed message you can do so via normal methods in your plugin descriptor).

Notice too that once getMappedImportValue is called all we have to do is pass back the existing value since we can be sure that the canMapImportValue has already succeeded.

Now we need to wire our ProjectCustomFieldImporter with our CustomFieldType:

public class SelectCFType extends TextCFType implements MultipleSettableCustomFieldType, MultipleCustomFieldType,
                                                        SortableCustomField, GroupSelectorField, ProjectImportableCustomField
{

    private final SelectConverter selectConverter;
    private final OptionsManager optionsManager;
    private final ProjectCustomFieldImporter projectCustomFieldImporter;

    private static final Logger log = Logger.getLogger(SelectCFType.class);

    public SelectCFType(CustomFieldValuePersister customFieldValuePersister, StringConverter stringConverter,
                        SelectConverter selectConverter, OptionsManager optionsManager, GenericConfigManager genericConfigManager)
    {
        super(customFieldValuePersister, stringConverter, genericConfigManager);
        this.selectConverter = selectConverter;
        this.optionsManager = optionsManager;
        this.projectCustomFieldImporter = new SelectCustomFieldImporter(this.optionsManager);
    }

    public ProjectCustomFieldImporter getProjectImporter()
    {
        return this.projectCustomFieldImporter;
    }

Existing ProjectCustomFieldImporter Implementations

These implementations have been created and are used by various JIRA system custom fields:

  • NoTransformationCustomFieldImporter
  • SelectCustomFieldImporter
  • CascadingSelectCustomFieldImporter
  • GroupCustomFieldImporter
  • ProjectPickerCustomFieldImporter
  • UserCustomFieldImporter
  • VersionCustomFieldImporter

Conclusion

If you want your custom field to participate in project imports you will need to modify your existing custom field code, hopefully this document has provided enough information to help you do this.

If your custom field type is extending a JIRA system custom field type you may already inherit an implementation of ProjectImportableCustomField.

Please make sure that this is the right implementation for your custom field type.