Are you interested in using Project Configurator to migrate workflows that contain conditions, validators or post-functions defined by a third-party app? Let's discover how easily this can be achieved.

Is the workflow app already supported?:

Navigate to the list of supported workflow plugins. This list includes the most popular Jira apps for workflows and there are new additions to it from time to time. If your third-party app is already supported, then you do not need to create a new extension and can start moving workflows that use this app immediately. If the app is not supported, follow the next steps with the help of an example, based on the app Workflow Essentials for Jira.

Example source code

The complete source code for this example is available at https://bitbucket.org/Adaptavist/example-workflow-extension/src/master/.

Step 1: Create the workflow feature and export its XML descriptor

Install the third-party app in the Jira instance you will use to develop and test the extension, then create a workflow that has the conditions, validators, or post-functions (collectively, workflow functions) that you want to support with Project Configurator. In this guide, we focus on:

Add them to a workflow:

Workflow screen - Post functions

Depending on the complexity of the chosen workflow function, it may be a good idea to create more than one instance of each to cover cases when the function can be configured in different ways. In the case of the Date Compare condition, you can see that it can work both with system or custom fields, and that it can handle a time expression or the current date and time. It is therefore better to create two instances of this condition to cover those cases.

Workflow screen - conditions

Once you have the workflow with the desired functions, export it from Jira as XML.

Export workflow screen

Open the XML file with your preferred editor and navigate to the section of the XML where the functions are defined.

Step 2: Implement interface HookPointCollection

Unless your extension already has another implementation of com.awnaba.projectconfigurator.extensionpoints.common.HookPointCollection, to which you can add the extensions for these workflow functions, create a new instance of this interface:

ImplementingHookPointCollection

@Profile("pc4j-extensions") @Component
public class WES4JExtensionModule implements HookPointCollection {

}

JAVA

Step 3: Implement the workflow extensions

a) DateCompareCondition

Analyze

Examine the workflow functions in the exported XML file. For example, start with the Date Compare condition. Looking in the XML, you will find the two occurrences of this condition:

First occurrence of the "Date Compare condition"

<condition type="class">
	<arg name="EVALUATION_EXPRESSION">+7d</arg>
	<arg name="OPERATOR">></arg>
	<arg name="FIELD_ID">duedate</arg>
	<arg name="SELECT_DATE_COMPARE_OPTION">VARIABLE_EXPRESSION</arg>
	<arg name="class.name">de.codecentric.jira.condition.DateComparisonCondition</arg>
</condition

JAVA

Second occurrence of the "Date Compare condition"

<condition type="class">
	<arg name="EVALUATION_EXPRESSION"></arg>
	<arg name="OPERATOR">></arg>
	<arg name="FIELD_ID">10101</arg>
	<arg name="SELECT_DATE_COMPARE_OPTION">CURRENT_DATETIME</arg>
	<arg name="class.name">de.codecentric.jira.condition.DateComparisonCondition</arg>
</condition>

JAVA

Looking at this part of the workflow descriptor, you see that the argument "FIELD_ID" can contain either the name of a system field ("duedate") or the ID of a custom field ("10101"), so it refers to another object in Jira. Then you have to create for this argument an implementation of the com.awnaba.projectconfigurator.extensionpoints.workflow.WorkflowHookPoint interface. Create a method with this return type in the class created in step 2.

Implementing HookPointCollection

@Profile("pc4j-extensions") 
@Component
public class WES4JExtensionModule implements HookPointCollection { 

	public WorkflowHookPoint getDateCompareConditionHookPoint() {
	}
}

JAVA

Why are references to other objects important? (1 of 2)

To start with, it occurs very frequently that a reference will need to be changed for a successful migration. In the above example, whenever a custom field is referenced, it will likely have a different ID at a different Jira instance, so this argument has to be rewritten with the ID of the equivalent custom field at the destination instance.

The other possibility is that this arg refers to a system field, like "duedate". A system object is completely transparent from the point of view of moving this workflow to another instance, as we expect all Jira instances to have that system field. This means you do not have to consider whether the system field exists or not, or if it has to be created before the workflow.

There are no other references to other entities in Jira, so when you implement support for the "FIELD_ID" argument, you are finished with this condition. The default action for PC when a workflow is migrated is to transfer it to the other instance as it is; for all other elements in the condition that do not reference entities in Jira, you do not need to do anything.

Define location

In order to create the WorkflowHookPoint, you have to provide information about the location within the workflow descriptor of the string that this extension is dealing with. In this case, the location of this string may be described as "the text node under an 'arg' element that has an attribute called 'name', equal to 'FIELD_ID' under a 'function' element that has another 'arg' with attribute 'name' equal to 'class.name' that is equal to 'de.codecentric.jira.condition.DateComparisonCondition'. This description must be provided as an XPath v1 expression that identifies this location:

XPath expression

//condition[arg[@name='class.name' and (text()='de.codecentric.jira.condition.DateComparisonCondition')]]/arg[@name='FIELD_ID']/text()
JAVA

Don't know XPath? No problem!

If you are not familiar with XPath, don't worry that you will have to learn yet another arcane piece of technology. As you will see, all the XPath expressions that you need to cover all your workflows are essentially the same, just with different values for the 'class.name' or 'name' attributes for the args, and then applying them for condition, validator, or function elements.

Wait until you see another example!

Define the content of the reference

You must specify the content of the string. We know the string contains either the internal name of a system field or the numeric ID of a custom field. As discussed before, when a system field is present, we can simply ignore it, as it is not necessary to do anything with it in the migration.

The content of the string handled by this extension point is specified by the method, ReferenceProcessor<String> getReferenceProcessor() in interface HookPoint. A ReferenceProcessor is the object that is able to interpret and handle the content of a reference.

Except for very specific cases, you do not need to create a ReferenceProcessor yourself, as there are two services, SimpleReferenceProcessorFactory and ReferenceProcessorComposer, that create these objects. The former is used to create a ReferenceProcessor from a set of built-in ones, both for Jira "out of the box" objects or objects defined in extensions to PC (see section 7 of this guide). The ReferenceProcessorComposer can be used when you want to compose a more complex ReferenceProcessor based on another already available ReferenceProcessor. 

There are two possible alternatives for the content of the reference in this case:

  • A numeric custom field ID
  • A system field identifier. No special action required, the default behaviour of copying the descriptor "as is" will be enough.

Looking at the Javadoc for SimpleReferenceProcessorFactory, you will find it has a method ReferenceProcessor<String> fromOption(ReferenceOption option) that creates one from a set of predefined ReferenceProcessor depending on the value of an enum passed to that method. Among the possible values of ReferenceOption you will find these two:

  • CUSTOM_FIELD_ID that handles a numeric custom field ID (this would cover the case of custom fields).
  • VOID_TRANSLATOR, that does nothing; it treats the content as if it were not a reference. This would serve for the case of system fields.

Finally, it is necessary to specify that these references should be treated in one of these ways, depending on whether they are about custom or system fields. In the Javadoc for ReferenceProcessorComposer you will find method:

<W> ReferenceProcessor<W> choiceOf(List<ReferenceProcessor<W>> processors, Function<W, Integer> selector)

This method can be used to build a ReferenceProcessor that is able to pick one among a list of ReferenceProcessor at runtime, depending on the reference values. 


Note

Many workflow plugins reference fields in a slightly different way to the one seen here, using the ID string provided by Field.getId(). This produces the same strings shown here, with the only difference being that a custom field would be identified by "custom field_10101" instead of "10101". For those cases, there is already a built-in translator that handles the overall case, both for system and custom fields: simpleReferenceProcessorFactory.fromOption(FIELD_STRING_ID).

Assembling everything the result would be something like this:

Implementing WorkflowHookPoint

public WorkflowHookPoint getDateCompareConditionHookPoint() {
	ReferenceProcessor processor = referenceProcessorComposer.choiceOf(
 		Arrays.asList(
			simpleReferenceProcessorFactory.fromOption(VOID_TRANSLATOR),
 			simpleReferenceProcessorFactory.fromOption(CUSTOM_FIELD_ID)
		),
 		fieldId -> isSystemCustomFieldId(fieldId) ? 0 : 1 );
 
	return new WorkflowHookPointImpl(
		processor,
		"//condition[arg[@name='class.name' and(text()='de.codecentric.jira.condition.DateComparisonCondition')]]/arg[@name='FIELD_ID']/text()"
 	);
}

private boolean isSystemCustomFieldId(String fieldId) {
 Field field = fieldManager.getField(fieldId);
	return (field != null) && !fieldManager.isCustomField(field);
}
JAVA

Congratulations! You have created your first extension point for workflows. With it, any instance of the Date Compare Condition from Workflow Essentials for Jira can now be migrated in an easy and safe way to another Jira instance.

Code Notes

 WorkflowHookPointImpl is a convenience class that facilitates creating implementations of WorkflowHookPoint.

This class also has a builder that can be used like this:

...

return new WorkflowHookPointImpl.Builder().

               withReferenceProcessor( processor).

               withXPath("//condition[arg[@name='class.name' and(text()='de.codecentric.jira.condition.DateComparisonCondition')]]/arg[@name='FIELD_ID']/text()").

               build();

...

b) Assign specific user post-function

Let's look at this post-function, which will be the second extension to implement.

Analyze

This is an occurrence of this post-function inside a workflow descriptor:

"User in group validator” in the workflow descriptor

...
		<function type="class">
			<arg name="full.module.key">de.codecentric.jira.wesset-assignee-to-specific-user-function</arg>
			<arg name="USERNAME_VALUE_FIELD">jsmith</arg>
			<arg name="class.name">de.codecentric.jira.postfunction.SetAssigneeToSpecificUserPostFunction</arg>
		</function>

JAVA

You can see that the only reference to other entities in Jira is a username.

Why are references to other objects important? (2 of 2)

In this case, your first thoughts may be:

"Wait a minute, I expect the username to be the same at the source and target instances, so it is not necessary to change anything here. Moreover, as Project Configurator's default behaviour is to migrate anything in the workflow descriptor as it is, I do not need to create an extension point for this post-function, right?"

It is absolutely true that nothing needs to be changed and that if you do not create an extension point for this post-function, the workflow will be migrated correctly in most cases. However, imagine user jsmith exists at the source instance but not at the target. In this case, the workflow will be migrated with a formally valid descriptor, but this validator would be referencing a user that does not exist. This is somewhat inconsistent, and it could mean that this post-function does not work as expected. It will likely fail when this transition takes place. If you instead create an extension point for this post-function, then Project Configurator will help the Jira admin to manage this situation:

• When a user displays PC's Object Dependencies Report it will show that this user is referenced in that workflow.

• The user will be exported whenever this workflow is exported.

• When importing this configuration, Project Configurator will ensure that the user is created before the workflow or report the problem otherwise.

To achieve these benefits, you need to create an implementation of WorkflowHookPoint.

This is similar to what we did with the Date Compare condition, so let's dive in.

Define location

As in any workflow extension, you have to specify the location of the reference string within the descriptor as an XPath:

//function[arg[@name='class.name' and (text()='de.codecentric.jira.postfunction.SetAssigneeToSpecificUserPostFunction')]]/arg[@name='USERNAME_VALUE_FIELD']/text()
CODE

Notice how the structure of this XPath location is similar to the one used for the previous condition.

Define the content of the reference

In this case, the reference consists of a single username. As in the previous extension, if you look at the Javadoc for SimpleReferenceProcessorFactory you will notice there is an option to retrieve a built-in ReferenceProcessor that handles plain user names.

Combining both things in the extension code, you should add the following method to WES4JExtensionModule class:

Implementing another WorkflowHookPoint

public WorkflowHookPoint getAssignSpecificUserHookPoint() {

	return new WorkflowHookPointImpl( 
		simpleReferenceProcessorFactory.fromOption(USERNAME), 
		"//function[arg[@name='class.name' and
(text()='de.codecentric.jira.postfunction.SetAssigneeToSpecificUserPostFunction')]]/arg[@name='USERNAME_VALUE_FIELD']/text()"
	);
}
JAVA

Now test it!

Your second workflow extension is completed now. Congratulations again!

Refer to the tips in section 8 for ideas on how to test this extension.