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:
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.
Once you have the workflow with the desired functions, export it from Jira as XML.
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:
@Profile("pc4j-extensions") @Component public class WES4JExtensionModule implements HookPointCollection { }
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:
<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
<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>
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.
@Profile("pc4j-extensions") @Component public class WES4JExtensionModule implements HookPointCollection { public WorkflowHookPoint getDateCompareConditionHookPoint() { } }
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:
//condition[arg[@name='class.name' and (text()='de.codecentric.jira.condition.DateComparisonCondition')]]/arg[@name='FIELD_ID']/text()
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
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:
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); }
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:
... <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>
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()
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:
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()" ); }
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.