Skip to end of metadata
Go to start of metadata

Starting with version 2.3.7-J7 of Project Configurator, it is possible to use the app services from a Groovy script in Script Runner for Jira (SR4J) or from any Java program (another app, for example...) running within the Jira instance.

Project Configurator must be installed and licensed in your instance.

Once your code has been compiled, if you want to test it, it is necessary to have Project Configurator (version 2.3.7-J7 or higher) installed and licensed in your development instance. 

Project Configurator jar files can be downloaded from the Marketplace.

For development purposes, it is possible to license Project Configurator with some of the timebomb licenses for app developers.

API Javadocs

No matter if you are driving Project Configurator from a Groovy script or a Java class, you will need the Javadoc for understanding what classes, methods, etc, are available. The javadoc jars for each version of the API are published at Adaptavist External Maven repository, at this URL:

https://nexus.adaptavist.com/content/repositories/external/com/awnaba/projectconfigurator/operations-api/

Using Project Configurator from a Groovy script in ScriptRunner for Jira

In order to use the services of Project Configurator you need to get an instance of the interface that provides the required operation:

  • ProjectConfigExporter, ProjectConfigImporter, ProjectCompleteImporter or ProjectCompleteExporter

In the examples below, those instances have been retrieved using a call to ComponentAccessor.getOSGiComponentInstanceOfType(...)

Project Configurator is able to run most operations in two modes: asynchronous and synchronous. 

  • Methods that run in synchronous mode are explicitly marked in their name (like exportSynchronous(...) ).
  • On the other hand, methods that run asynchronously do not have any special name (like export(...) ).

Using Project Configurator from Java

Using Project Configurator from Java code (i.e. another app) has some extra requirements relative to usage from a Groovy script in SR4J. These specific requirements are described below.

It is also useful looking at the previous section with the Groovy script examples, as those examples have been created with a coding style that is highly compatible with Java.

Add Dependency in the pom.xml

First of all, you need to declare in your pom.xml file that your code depends on this API. This only requires adding this dependency to the xml:

pom.xml

<dependency>
    <groupId>com.awnaba.projectconfigurator</groupId>
    <artifactId>operations-api</artifactId>
    <version>1.3.0</version>
    <scope>provided</scope>
</dependency>

Do not use versions of the API older than 1.3.0. Those earlier versions were intended only for internal use and will not work with the examples in this page.

The jar artifact for the API is available from the same Maven repository as the javadocs. If you want to access that Maven repository add these lines to your pom.xml:
pom.xml

<repository>
    <id>adaptavist-external</id>
    <url>https://nexus.adaptavist.com/content/repositories/external</url>
    <snapshots>
        <enabled>false</enabled>
    </snapshots>
    <releases>
        <enabled>true</enabled>
        <checksumPolicy>fail</checksumPolicy>
    </releases>
</repository>

Access API Components From Your Java Code

As mentioned for the Groovy scripts, you will need to get the component objects that implement the API. This can be done in a variety of ways.

Declare the imported components in atlassian-plugin.xml

If your app is not using the Atlassian Spring Scanner, then you must declare in its atlassian-plugin.xml file that it will import the components you need to run exports, imports or other operation. For example, if you want to export configurations or obtain the graph of dependencies for configuration objects, you would need to add this line:
atlassian-plugin.xml

<component-import key="projectConfigExporter" interface="com.awnaba.projectconfigurator.operationsapi.ProjectConfigExporter"/>

Using Atlassian Spring Scanner

In this case you can inject the required dependencies to the API objects without the need to add an explicit component import declaration in atlassian-plugin.xml. Using the correct annotations will be enough:
YourClass.java

@Scanned
public class SamplePluginAction extends JiraWebActionSupport {
 
....
     
    private ProjectConfigExporter projectConfigExporter;
...
    @Autowired
    public SamplePluginAction(@ComponentImport ProjectConfigExporter projectConfigExporter, ...) {
        ...
        this.projectConfigExporter = projectConfigExporter;
        ...
    }

This example has a simple approach to error handling.
Synchronous-Export-Example.groovy

// Required Imports
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.task.TaskProgressSink
import com.awnaba.projectconfigurator.operationsapi.ProjectConfigExporter
import com.onresolve.scriptrunner.runner.customisers.WithPlugin
  
// The required annotation to enable access to the functionality provided by the plugin within our script.
@WithPlugin("com.awnaba.projectconfigurator.projectconfigurator")
  
// Get an instance of the export method
ProjectConfigExporter exporter = ComponentAccessor.getOSGiComponentInstanceOfType(ProjectConfigExporter)
  
  
try {
// Define the project keys to be exported in the set below
    Set projectKeys = ["DEMO""DEMO1"]
  
// Define the options for how you want to configure the export.
    Map<String, String> exportOptions = [
            "filterCFMode"           "filterUnusedCFExtended"// Options: none, filterUnusedCFExtended, all
            "jiraFilterExportMode"   "none"// Options: none, global, projects, global-or-projects, shared, all
            "jiraDashboardExportMode""none"// Options: none, global, projects, global-or-projects, shared, all
            "agileBoardsExportMode"  "none" // Options: none, projects, all
    ]
  
// Run the export synchronously using the options specified above
    def exportFinalResult = exporter.exportSynchronous(exportOptions, projectKeys, TaskProgressSink.NULL_SINK).getFinalResult()
  
// Check if the export completed successfully and if so generate the XML file
  
// If the export failed notify the user
    if (exportFinalResult == null || exportFinalResult.getReturnCode() != ProjectConfigExporter.ReturnOpCode.SUCCESS) {
        return "The export did not complete successfully"     
    else {
        // If the export was successful write the XML out to a file and notify the user where the file is stored
        // Define the path and export filename to be used below
        File exportFile = new File("/tmp/export.xml")
        // Write the generated xml out to a configuration export XML file.
        exportFile.write(exportFinalResult.getExportedXML())
        // Return the location to the created export file
        return "Export file created at " + exportFile
    }
catch (Exception e) {
    return "An unexpected error occurred. Please check your atlassian-jira.log for more information" "<br/>" + e
}

Synchronous-Import-Example.groovy

// Required Imports
import com.onresolve.scriptrunner.runner.customisers.WithPlugin
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.task.TaskProgressSink
import com.awnaba.projectconfigurator.operationsapi.ProjectConfigImporter
import com.awnaba.projectconfigurator.operationsapi.ConfigOpFullProcessResult
import com.awnaba.projectconfigurator.operationsapi.ConfigImportResult
import com.awnaba.projectconfigurator.operationsapi.ConfigOpCallResult
  
// The required annotation to enable access to the functionality provided by the plugin within our script
@WithPlugin("com.awnaba.projectconfigurator.projectconfigurator")
    
// Get an instance of the import method
def importer = ComponentAccessor.getOSGiComponentInstanceOfType(ProjectConfigImporter.class)
    
// Specify the path to the export file
def exportFile = "/tmp/export.xml"
    
// Perform the import if the file is valid
try {
 // Extract the contents of the export file to a String that the import method can use
 def fileContents = new File(exportFile).text
    
 // The booleans below allow you to configure the import options you require and match the checkboxes displayed inside the user interface
    
// Set to true if you wish to apply the changes as by default only a simulated import is run.
 boolean applyChanges = false
    
// Set to true if you want to create any referenced projects as part of the import
 boolean CreateExtraProjects = false
    
// Set to true if you wish to use smart custom field contexts
 boolean smartCustomFieldContexts = false
    
// Set to true if you wish to try to publish drafs as part of the import
 boolean publishDrafts = false
    
// Set to true to continue if any errors occur related to Jira Issue Filters or Dashboards
 boolean continueOnErrorsWithDashboardsandFilters = false
    
// A list to specify any configuration objects which we do not wish to import.
 String[] doNotLoad = []
    
// Construct a new ConfigOpFullProcessResult object which will store the results of the configuration import
// Requires the following parameters of XML config, applyChanges,createExtraProjects,smartCFContexts,publishDrafts,continueOnDashboardFilterErrors,doNotLoadObjects as well as an instance of the TaskProgressSink object.
 def importResult = importer.importConfigurationSynchronously(fileContents.toString(), applyChanges, CreateExtraProjects, smartCustomFieldContexts, publishDrafts, continueOnErrorsWithDashboardsandFilters, doNotLoad, TaskProgressSink.NULL_SINK) as ConfigOpFullProcessResult<ProjectConfigImporter.ReturnCallCode, ? extends ConfigImportResult>
    
// Check if the import completed successfully and if so display the results
def callResult = importResult.getCallResult() as ConfigOpCallResult;
def callReturnCode = callResult.getReturnCode()
      
// If the import failed notify the user
// Possible return codes that can be checked = IMPORT_STARTED, NOT_LOGGED_IN, UNAUTHORIZED, UNLICENSED, ERROR_READING_CONFIG_FILE, IMPORT_ALREADY_RUNNING
 if (callReturnCode!=null && callReturnCode != ProjectConfigImporter.ReturnCallCode.IMPORT_STARTED) {
 return "The import did not launch succesfully. Launching failed with a return code of " + callReturnCode.toString()
        // If the import was successful display the results
 else {
 // get the results of the import
 ProjectConfigImporter.ReturnOpCode opCode = importResult.getFinalResult().getReturnCode()
 String message = opCode==ProjectConfigImporter.ReturnOpCode.SUCCESS ? importResult.getFinalResult().getSuccessMessage() : "Import failed"
 return opCode.name() + "<br/>" + message +  "<br/><br/>" + importResult.getFinalResult().getLoadTrace().replaceAll("\n""<br/>");
 }
    
// If an invalid file is found print an exception on screen
catch (FileNotFoundException e) {
 return "You must provide a valid file: " "<br/>" + e
}

Asynchronous-Export-Example.java

// Required Imports
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import com.awnaba.projectconfigurator.operationsapi.ProjectConfigExporter;
import com.onresolve.scriptrunner.runner.customisers.WithPlugin;
import com.atlassian.jira.component.ComponentAccessor;
import com.awnaba.projectconfigurator.operationsapi.ConfigOpCallResult;
import com.awnaba.projectconfigurator.operationsapi.TaskHelper;
import com.atlassian.jira.web.bean.TaskDescriptorBean;
import  com.awnaba.projectconfigurator.operationsapi.ConfigExportResult;
 
 // The required annotation to enable access to the functionality provided by the plugin within our script.
@WithPlugin("com.awnaba.projectconfigurator.projectconfigurator")
 
// Define our Asynchronous Exporter Class
public class AsynchronusExporter {
 
    // Define our export method
    public String export() {
 
        // Get an instance of the export method
        ProjectConfigExporter exporter = ComponentAccessor.getOSGiComponentInstanceOfType(ProjectConfigExporter);
 
        // Define a set to store the keys of all of the projects that we wish to export
        Set projectKeys = new HashSet();
 
        // Add the project keys that we want to export to our projectKeys set
        projectKeys.add("DEMO");
        projectKeys.add("DEMO1");
 
        // Define a map to store all of the export options that we require
        Map<String, String> exportOptions = new HashMap<String, String>();
 
        // Add values to our exportOptions map
        exportOptions.put("filterCFMode""filterUnusedCFExtended"); // Options: none, filterUnusedCFExtended, all
        exportOptions.put("jiraFilterExportMode""none");
        // Options: none, global, projects, global-or-projects, shared, all
        exportOptions.put("jiraDashboardExportMode""none");
        // Options: none, global, projects, global-or-projects, shared, all
        exportOptions.put("agileBoardsExportMode""none"); // Options: none, projects, all
 
        // Try to generate the export safely throwing an exception if generating the export fails.
        try {
            // Call the asynchronous export method passing in the export options map and the project keys set as paramaters
            ConfigOpCallResult exportResult = exporter.export(exportOptions, projectKeys) as ConfigOpCallResult<ProjectConfigExporter.ReturnCallCode, ? extends ConfigExportResult>;
             
            // Check if the export completed successfully and if so generate the XML file
            // If the export failed notify the user
            // Possible return codes that can be checked = EXPORT_STARTED, NOT_LOGGED_IN, UNAUTHORIZED, UNLICENSED, NO_PROJECTS, EXPORT_ALREADY_RUNNING
            if (exportResult.returnCode.toString() != "EXPORT_STARTED") {
                return "The export did not start successfully with a return code of " + exportResult.getReturnCode().toString();
                // If the export was successful write the XML out to a file and notify the user where the file is stored
            else {
                // Define a new file writer object to write out the generated XML.
                FileWriter fw;
                // Define the path and export filename to be used below
                String exportFile = "/tmp/export.xml";
                // Get the task ID of of the export from the Config Op Call Result object
                Long taskId = exportResult.getTaskId();
                //Get a new instance of the task helper class
                TaskHelper taskHelper = ComponentAccessor.getOSGiComponentInstanceOfType(TaskHelper.class);
 
                // Get the task descriptor object for the current task provided by the task helper class
                TaskDescriptorBean<?> taskDescriptor = taskHelper.getTaskState(taskId);
 
                // Wait until the export task is finished
                while (taskDescriptor.isFinished() == false) {
                    // Wait 0.5 seconds before getting a new instance of the taskDescriptor
                    sleep(500);
                    // Get a new instance of the task descriptor
                    taskDescriptor = taskHelper.getTaskState(taskId);
                }
 
                // Check if the export has completed and if so write out the xml file
                if (taskDescriptor.isFinished() == true) {
                    // Define the new file to write out the generated XML to.
                    fw = new FileWriter(new File(exportFile));
                    // Write the generated xml out to a configuration export XML file.
                    fw.write(taskHelper.getResult(taskDescriptor).getExportedXML().toString());
                    // Close the file writer once writing is complete to avoid memory overflow errors.
                    fw.close();
                    //Return the location to the created export file
                    return "Export file created at " + exportFile;
                }
            }
            // Throw an exception if the XML export file cannot be generated
        catch (IOException e) {
            return "An unexpected error occurred. Please check your atlassian-jira.log for more information" "<br/>" + e;
        }
    }
}
// Call the export method from the script console
new AsynchronusExporter().export();

Asynchronous-Import-Example.java

// Required Imports
import com.onresolve.scriptrunner.runner.customisers.WithPlugin;
import com.atlassian.jira.component.ComponentAccessor;
import com.awnaba.projectconfigurator.operationsapi.ProjectConfigImporter;
import com.awnaba.projectconfigurator.operationsapi.ConfigOpCallResult;
import com.awnaba.projectconfigurator.operationsapi.ConfigImportResult;
import com.awnaba.projectconfigurator.operationsapi.TaskHelper;
import com.atlassian.jira.web.bean.TaskDescriptorBean;
import java.nio.charset.StandardCharsets;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
 
 
// The required annotation to enable access to the functionality provided by the plugin within our script.
@WithPlugin("com.awnaba.projectconfigurator.projectconfigurator")
 
// Define our Asynchronous Importer Class
public class AsynchronusImporter {
 
    // Define our import method
    // Note - import is a reserved keyword meaning we cannot call the import method this
    public String configImport() {
 
        // Get an instance of the import method
        def importer = ComponentAccessor.getOSGiComponentInstanceOfType(ProjectConfigImporter.class);
 
        // Specify the path to the export file
        def exportFile = "/tmp/export.xml";
 
        // Perform the import if the file is valid
        try {
 
            // Define a new input stream for reading in the file
            InputStream inputStream = new FileInputStream(exportFile);
 
            // Define a new buffered reader to read in each line of the file
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
 
            // Define a variable to store the current line in the file
            String currentLine = bufferedReader.readLine();
 
            // Define a new string builder which will build the xml string
            StringBuilder stringBuilder = new StringBuilder();
 
            // Loop through the file and read each line into a sting
            while (currentLine != null) {
                stringBuilder.append(currentLine).append("\n");
                currentLine = bufferedReader.readLine();
            }
 
            // Generate the string containing the contents of the XML file
            String xmlString = stringBuilder.toString();
 
            // The booleans below allow you to configure the import options you require and match the checkboxes displayed inside the user interface
 
            // Set to true if you wish to apply the changes as by default only a simulated import is run.
            boolean applyChanges = false;
 
            // Set to true if you want to create any referenced projects as part of the import
            boolean CreateExtraProjects = false;
 
            // Set to true if you wish to use smart custom field contexts
            boolean smartCustomFieldContexts = false;
 
            // Set to true if you wish to try to publish drafs as part of the import
            boolean publishDrafts = false;
 
            // Set to true to continue if any errors occur related to Jira Issue Filters or Dashboards
            boolean continueOnErrorsWithDashboardsandFilters = false;
 
            // A list to specify any configuration objects which we do not wish to import.
            String[] doNotLoad = [];
 
            // Construct a new ConfigOpCalResult object which will store the results of the configuration import
            // Requires the following parameters of XML config, applyChanges,createExtraProjects,smartCFContexts,publishDrafts,continueOnDashboardFilterErrors,doNotLoadObjects as well as an instance of the TaskProgressSink object.
            def importResult = importer.importConfiguration(xmlString, applyChanges, CreateExtraProjects, smartCustomFieldContexts, publishDrafts, continueOnErrorsWithDashboardsandFilters, doNotLoad) as ConfigOpCallResult<ProjectConfigImporter.ReturnCallCode, ? extends ConfigImportResult>;
 
            // Check if the import started successfully
            if (!importResult.returnCode.toString().equals("IMPORT_STARTED")) {
                return "The import did not launch successfully with a return code of " + importResult.getReturnCode().toString();
                // If the import launched successfully display the import result on screen
            else {
                // Get the task ID of of the import from the Config Op Call Result object
                Long taskId = importResult.getTaskId();
 
                //Get a new instance of the task helper class
                TaskHelper taskHelper = ComponentAccessor.getOSGiComponentInstanceOfType(TaskHelper.class);
 
                // Get the task descriptor object for the current task provided by the task helper class
                TaskDescriptorBean<?> taskDescriptor = taskHelper.getTaskState(taskId);
 
                // Wait until the import task is finished
                while (taskDescriptor.isFinished() == false) {
                    // Wait 0.5 seconds before getting a new instance of the taskDescriptor
                    sleep(500);
                    // Get a new instance of the task descriptor
                    taskDescriptor = taskHelper.getTaskState(taskId);
                }
 
                // Check if the import has completed and if so display the results of the impoprt
                if (taskDescriptor.isFinished() == true) {
 
                    // Get the final import result after it has completed
                    def importFinalResult = taskHelper.getResult(taskDescriptor) as ConfigImportResult;
 
                    // Get the return code from the import
                    def returnCode = importResult.returnCode.toString();
 
                    // Get the import trace to display
                    String message = importFinalResult.getSuccessMessage();
 
                    // If the import failed notify the user
                    // Possible return codes that can be checked = ERROR_DURING_IMPORT, SUCCESS
                    if (returnCode.toString().equals("ERROR_DURING_IMPORT")) {
                        return "The import did not complete successfully." "<br/><br/>" + message;
                        // If the import was successful display the results
                    }
                    // Return the result of the import on screen
                    return message + "<br/><br/>" + importFinalResult.getLoadTrace().replaceAll("\n""<br/>");
                }
 
            }
        }
        // If an invalid file is found print an exception on screen
        catch (IOException e) {
            return "You must provide a valid file: " "<br/>" + e;
        }
    }
}
 
// Call the import method from the script console
new AsynchronusImporter().configImport();


  • No labels