Manipulating Work Item Workflow States


I was interested in modifying the workflow state of a work item using the Java API. This is interesting for various types of automation on the server as well as on the client. If you ever wondered how to drive a work item through its workflow, you will find this post of interest. If you are just starting with extending Rational Team Concert, start reading this and the linked posts to get some guidance on how to set up your environment.

License and how to get started with the RTC API’S

As always, our lawyers reminded me to state that the code in this post is derived from examples from Jazz.net as well as the RTC SDK. The usage of code from that example source code is governed by this license. Therefore this code is governed by this license, which basically means you can use it for internal usage, but not sell. Please also remember, as stated in the disclaimer, that this code comes with the usual lack of promise or guarantee. Enjoy!

If you just get started with extending Rational Team Concert, or create API based automation, start with the post Learning To Fly: Getting Started with the RTC Java API’s and follow the linked resources.

You should be able to use the following code in this environment and get your own automation or extension working.

To keep it simple this example is, as many others in this blog, based on the Jazz Team Wiki entry on Programmatic Work Item Creation and the Plain Java Client Library Snippets. The example in this blog shows RTC Client and Common API.

Updates

*Update* The post A RTC WorkItem Command Line Version 2 contains downloadable code that performs most of the activities required for reading and modifying work items, their attributes, and all kinds of links. This includes reading and writing work item attribute of all kinds, including state changes. The interesting code can be found in the com.ibm.js.team.workitem.commandline.helper package in the class WorkItemHelper. All techniques described below are used there. You can familiarize yourself with the concepts in this post and then look into that project for how it is used.

*Update* I published the server API code in the post Resolve Parent If All Children Are Resolved Participant
*Update* changed the title to reflect we are talking about the workflow state. A work item has a history. Each historical version is also called state and is accessible through the API for example using ItemManager.fetchAllStateHandles(). This blog does talk about the work item state and not historical states of a work item.

Modifying the workflow state of a work item is interesting in various scenarios such as

  • Data migration tooling
  • Copying or moving work item data between project areas or repositories
  • Modifying the workflow state of a work item based on the state of related work items for example in Participants

Work Item Workflow

Initially I looked at the server API and wanted to figure out how to close a parent work item when all children were closed. I found some code in the SDK that actually does this and immediately recognized that there is a general challenge when trying to code this kind of automation. The challenge is that the server API does not allow to set the workflow state of a work item directly. It requires to provide a workflow action that changes the workflow state to the desired target state. If the work item is in a specific workflow state and there is no valid workflow action from that state into the desired target state, it would be necessary to find a path of several workflow actions from the current state to the target state.

I also looked at the Client Libraries and the client work item API. IWorkItem exposes a method setState2(Identifier value) which could be used to directly set the workflow state of a work item. However, this method is deprecated. The reason to deprecate it is most likely related to the fact that directly setting the workflow state of a work item does not respect the workflow. Essentially it would be possible to set a workflow state that is not even reachable from the current workflow state. Instead the method WorkItemWorkingCopy.setWorkflowAction(String action) is provided to allow to apply a workflow action to the work item.

This leaves you with the same challenge: find a path of workflow actions from the current workflow state of a work item to the desired state. If you ever lost in a computer game because your NPC followers staggered around an impossible trail and got stuck, you realize this is not a trivial challenge.

Resolving a Work Item

Lets look at a method that is based on code I discovered when looking into the SDK that tries to resolve a work item. That code shows all the issues described above.
It gets the IWorkflowInfo for the work item. It then checks if the work item is in the group of closed states.

If this is not the case, it tries to get the resolves workflow action defined in the workflow. If there is one defined it tries to find this action in the list of workflow actions that are defined for the current workflow state.

If there is no valid action the code does nothing. An alternative would be to stop the operation. If there is a valid action available the code saves the work item providing the workflow action to close the work item.

/**
 * Resolve using the resolve transition
 * 
 * @param workItem
 * @param monitor
 * @throws TeamRepositoryException
 */
public void resolve(IWorkItem workItem, IProgressMonitor monitor)
		throws TeamRepositoryException {
	wiServer = getService(IWorkItemServer.class);
	IWorkItem wi = (IWorkItem) wiServer.getAuditableCommon()
		.resolveAuditable(workItem, IWorkItem.FULL_PROFILE, monitor)
		.getWorkingCopy();

	IWorkflowInfo workflowInfo = wiServer.findWorkflowInfo(wi, monitor);
	String actionId = null;
	if (workflowInfo != null) {
		// check if already closed
		if (workflowInfo.getStateGroup(wi.getState2()) == IWorkflowInfo.CLOSED_STATES) {
			// nothing to do.
			return;
		}
		if (workflowInfo.getStateGroup(wi.getState2()) != IWorkflowInfo.CLOSED_STATES) {
		// Get the resolve action
			Identifier resolveActionId = workflowInfo.getResolveActionId();
			// If there is a resolve action 
			if (resolveActionId != null) {
				Identifier availableActions[] = workflowInfo.getActionIds(wi.getState2());
				// Find the resolve action in the actions available for this workflow state
				for (int i = 0; i < availableActions.length; i++) {
					if (resolveActionId == availableActions[i]) {
						actionId = resolveActionId.getStringIdentifier();
						break;
					}
				}
			}
		}
		if (actionId == null) {
			// can't use an action from the current state
			// Do nothing or throw some error
			return;
		}
		// This can be used to filter it out for preconditions and follow up
		// Actions
		Set additionalParams = new HashSet();
		additionalParams.add(IExtensionsDefinitions.UPDATE_PARENT_STATE_EXTENSION_ID);
		// We found a valid action
		IStatus status = wiServer.saveWorkItem3(wi, null, actionId, additionalParams);
		if (!status.isOK()) {
			// TODO: Throw an exception or do nothing
		}
	}
}

The solution above would obviously only work for workflows where there is a resolving workflow action for each given workflow state. This is not always the case, not even in the out of the box workflows.

Workflow Considerations

Lets look at the example workflow below.

This workflow is set up in a way that the Done state can only be reached by entering the In Progress state first. That means an algorithm as described above won’t work for example in the New state.

To be able to get to the Done state an algorithm would have to find a series of actions from the given state to the Done state.

Another issue with the workflow above is the Dead State Verified. There is no way out of this workflow state and once it is set, it is impossible to get anywhere else.

I would consider to avoid dead states when designing workflows. One example of dead states in the out of the box RTC workflows is the Invalid state in the Scrum Process Templates User Story workflow. There are reasons for the chosen design, for example documentation of a decision.

However, dead states cause issues and I would consider to always have some reopen or similar workflow action to get out of a dead state.

Workflow Path Finder

I finally decided to create some code to solve the dilemma of finding paths for general workflows. The Code can be downloaded from DropBox.

The code provides the following classes:

The class PathElement stores a state transition described by a state and an action. It can be uses to store a source state and an outgoing action as well as to store an incoming action and the target state.

The class Path is used to create and store paths using an ArrayList of PathElements.

The class VisitTracker uses a HashMap to store objects that have already been visited while searching the path. The class is necessary to avoid loops like the Still working action in the Example workflow above. It prevents from going to the same workflow state or using the same action to a target state more than once.

The Class WorkflowPathFinder finally implements the search strategies to find a path.

In general the class provides two strategies.

Depth First Recursive Descent

Depth first recursive descent basically picks one action and tries to find a path to the target using this action. It recursively calls itself as long as there are available options depth first. If it can’t find a path it returns and the next upper recursion that has option will try the alternatives. It tracks actions and target states to avoid crashing in the recursion. It only takes one action to a specific state once.

  • public Path findPathToState_DF(Identifier currentState, String targetStateID) tries to find a path from a current state to the target state.
  • public Path findPathToAction_DF(Identifier currentState, Identifier targetActionId) tries to find a path from a current state to the target action.

The depth first algorithms are very easy to implement and reliable, the only issue is that, dependent on the order of the actions, a recursive descent approach does not necessarily find the shortest path.

Breadth First Recursive Descent

Breadth first recursive descent basically tries to find paths to the target using any available action and can be set to return the shortest path found. This algorithm has to find more paths and is therefore slower than the depth first search, but it would likely not return a path that runs through unnecessary transitions.

  • public Path findPathToState_BF(Identifier currentState, String targetStateID, boolean shortestDistance) tries to find a path from a current state to the target state.
  • public Path findPathToAction_BF(Identifier currentState, Identifier targetActionId, boolean shortestDistance) tries to find a path from a current state to the target action.

All algorithms have the issue, that they don’t understand the context and don’t necessarily return a path that a human would have used. By providing the desired target action instead of the target state you can restrict the path likely to be found a bit.

Usage Examples

The classes can be used with the client API as well as the Server API.

This example code shows how to run it on the client:

IWorkItemClient workItemClient = (IWorkItemClient) teamRepository.getClientLibrary(IWorkItemClient.class);
IWorkflowInfo workflowInfo = workItemClient.findWorkflowInfo(workItem,	monitor);
if (workflowInfo != null) {
	// check if already closed
	if (workflowInfo.getStateGroup(workItem.getState2()) != IWorkflowInfo.CLOSED_STATES) {
		Identifier resolveActionId = workflowInfo.getResolveActionId();
		if (resolveActionId != null) {
			WorkflowPathFinder tManager = new WorkflowPathFinder(
				workflowInfo, monitor);
			Path path = tManager.findPathToAction_BF(workItem.getState2(), resolveActionId, true);
			WorkItemSetWorkflowActionModification workflowOperation = new WorkItemSetWorkflowActionModification(null);
			ArrayList transitions = path.getElements();
			for (Iterator iterator = transitions.iterator(); iterator.hasNext();) {
				PathElement pathElement = (PathElement) iterator.next();
				workflowOperation.setfWorkFlowAtion(pathElement.getActionID());
				workflowOperation.run(workItem, monitor);
				System.out.println("Workflow Action for work item " + workItem.getId() + " : "+pathElement.getActionID());
			}
		}
	}
}

The WorkItemOperation used in the example looks as follows:

private static class WorkItemSetWorkflowActionModification extends WorkItemOperation {

	private String fWorkFlowAtion;

	public WorkItemSetWorkflowActionModification(String workFlowAtion) {
		super("Modifying Work Item State", IWorkItem.FULL_PROFILE);
		fWorkFlowAtion = workFlowAtion;
	}

	@Override
	protected void execute(WorkItemWorkingCopy workingCopy,
			IProgressMonitor monitor) throws TeamRepositoryException {
		workingCopy.setWorkflowAction(fWorkFlowAtion);
	}

	public void setfWorkFlowAtion(String fWorkFlowAtion) {
		this.fWorkFlowAtion = fWorkFlowAtion;
	}
}

You can download the Plain Java Client Library based tool here.

This example is based on the wiki entry on Programmatic Work Item Creation. The API used in the following example is client API.

On the Server the difference is to get wiServer = getService(IWorkItemServer.class); instead of IWorkItemClient. The following code does the save operation.

/**
 * Save a state transition
 * 
 * @param workItem
 * @param pathElement
 * @param workflowInfo
 * @param monitor
 * @throws TeamRepositoryException
 */
public void saveWorkItemStateTransition(IWorkItem workItem,
		PathElement pathElement, IWorkflowInfo workflowInfo,
		IProgressMonitor monitor) throws TeamRepositoryException {
	IWorkItem saveWi = (IWorkItem) wiServer.getAuditableCommon()
		.resolveAuditable(workItem, IWorkItem.FULL_PROFILE, monitor)
		.getWorkingCopy();

	// This can be used to filter it out for preconditions
	// and followup Actions
	Set additionalParams = new HashSet();
	additionalParams.add(IExtensionsDefinitions.UPDATE_PARENT_STATE_EXTENSION_ID);
	IStatus status = wiServer.saveWorkItem3(saveWi, null,
	pathElement.getActionID(), additionalParams);
	if (!status.isOK()) {
		// Do something
	}
}

As always, to keep it simple, there is few error handling. You might want to enhance this for later usage. I hope the code helps someone out there to save some time and get the work done.

Advertisements

About rsjazz

Hi, my name is Ralph. I work for IBM and help colleagues and customers with adopting the Jazz technologies.
This entry was posted in Jazz, RTC, RTC Extensibility and tagged , , , , , . Bookmark the permalink.

21 Responses to Manipulating Work Item Workflow States

  1. Pingback: Resolve Parent If All Children Are Resolved Participant | rsjazz

  2. Jayee says:

    Now I have to update status of a work item.
    From the article here.
    IWorkItem saveWi = (IWorkItem) wiServer.getAuditableCommon();

    Where is wiServer from?

    And additionalParams.add(IExtensionsDefinitions.UPDATE_PARENT_STATE_EXTENSION_ID);

    error : IExtensionsDefinitions is not recognized. I used RTC V4 client Java API.

    I also tried other following options:

    IWorkItem workItem …..;

    IWorkItemHandle handle = (IWorkItemHandle) workItem.getItemHandle();

    IWorkItemWorkingCopyManager wcm = workItemClient
    .getWorkItemWorkingCopyManager();
    wcm.connect(handle, IWorkItem.FULL_PROFILE, null);

    WorkItemWorkingCopy wc = wcm.getWorkingCopy(handle);

    IWorkItem copiedWorkItem = wc.getWorkItem();

    IAttribute statusAttribute= workItemClient.findAttribute(projectArea, IWorkItem.STATE_PROPERTY, monitor);

    IWorkflowInfo workflowInfo = workItemClient.findWorkflowInfo(copiedWorkItem, monitor);
    String currentStatus = workflowInfo.getStateName(copiedWorkItem.getState2()); // I have gotten the current status of the work item.

    Now I want to change the status but failed via either of the following two ways.
    Way 1:
    wc.setWorkflowAction(“In Progress”);
    Way 2:
    Identifier [] allStatus = workflowInfo.getAllStateIds();
    copiedWorkItem.setState2(allStatus[2]);

    Seems I used the right API but the status was not changed on RTC.

    • rsjazz says:

      Hi Jayee, there are essentially two API parts, one for the server and one for the client. The code on the top is from a server extension. The section “Usage Examples” talks about how to use it for a Plain Java Client Library application. The code in class WorkItemSetWorkflowActionModification is what you want on the client. Please be aware that it does not call the workitemSave, it uses a workitem workingcopy to set the workflow action. The save is done by the WorkItemOperation.

      You can tell the difference between the code used by how the client library or the service is retreived. getClientLibrary() is client code getService() is server code.

      The additional parameters on the server can be used to communicate between server extensions, to avoid recursive operations etc. You don’t have to do that on the server. Please note, there is a link “You can download the Plain Java Client Library based tool here” that gives you the path finder for the client.

  3. Guido Schneider says:

    Hi Ralph,
    great article. Thank you. We are using some part of your code in our JaccCommandlineInterface.
    Now we started to implement a new Workitem-Importer tool, because the one in Eclipse is not fullfilling our needs. During this we run into the issue you mentioned above:

    “IWorkItem exposes a method setState2(Identifier value) which could be used to directly set the workflow state of a work item. However, this method is deprecated. The reason to deprecate it is most likely related to the fact that directly setting the workflow state of a work item does not respect the workflow. Essentially it would be possible to set a workflow state that is not even reachable from the current workflow state. Instead the method WorkItemWorkingCopy.setWorkflowAction(String action) is provided to allow to apply a workflow action to the work item.”

    I agree completely with this reason, but not in case of IMPORT action in your mentioned migration szenario. During an import it MUST be possible to set ANY state. Also triggers, behaviours etc. should not be fired.
    The fact that the Eclipse Importer allows today (V.4.0.4) to set the state directly means for me, either they use a deprecated function and we will run in troubles in the future, or there exists an option/function
    to set the state directly we do not know.

    In ClearQuest Importer this was possible by a built-in action “IMPORT” which was only possible to be fired by an ADMIN.

    May you ask your development if there is a possibility or what they have in mind in the future?

    regards
    Guido

    • rsjazz says:

      Hi Guido,

      I will try. For now, please use the available method, even if it is deprecated. By the way, this would also be needed for things like project move.

  4. Thomas says:

    Hello,

    the method WorkItemWorkingCopy.setWorkflowAction(String action) seems not to give any information on failure back nor throwing any exception if the action-String is not a valid transition.

    How can i recognize a failed status transition?

    • rsjazz says:

      I would assume that the actual save will give you that information.

      • Thomas says:

        Thank you very much!
        save() returns an IDetailed-Object which contains a status and a message.

      • rsjazz says:

        RTC processes all that data and assesses only with the save, if the save can be finished. At that time advisors and other code is also run. E.g. the state could be OK, but you have no permission to save in that state. This is a common pattern in RTC.

  5. Voja says:

    Hi Ralph,
    great article. I have tried to inegrate this in my app and I have a small problem. On workflowOperation.run(workItem, monitor); i get an error:

    ERROR ThreadCheck:124 – Long-running operations prohibited on this thread
    java.lang.IllegalStateException: Long-running operations prohibited on this thread

    Could you please tell me how to fix this?

    • rsjazz says:

      Hi Voja, sorry for the delay, I was traveling.

      I am not completely sure why you get that. I can only imagine that your automation runs in a context that does not allow long running operations. Some of the operations are fast and won’t block. Some are long running (see JavaDoc of the methods). Try to find out which one might block and try to find if there is another one that does not.

      • rsjazz says:

        Voja,

        a colleague mentioned recently that RTC throws that kind of error if you run a long running operation in the UI thread. You have to create a job/worker thread that runs the long running operation. I have no code currently. I would suggest to look into the SDK to find such a job.

  6. Carlos Ordoyo says:

    Hi Ralph,

    thank you very much for your article. I have used workitemcopy (client API) in my program, and work item state changes OK…but I have an issue that I’m not sure can be resolved… To see this change (in the RTC work item view), it must be refresh (F5 or pressing refresh button). Is there any way for automatic refresh in client side?

    Thanks in advanced.

    Saludos / Regards.

    • rsjazz says:

      In general no. This is as some other user changes the state. The editor will detect it eventually.

      If you where running in the eclipse client, I assume there would be a way to tell the editor to refresh, but in general, there is not.

      PS: I never use workingcopy in client API. I use WorkItemOperations.

  7. Girish Chandra P says:

    Hello Ralph,
    /**********************************************************************
    @Deprecated
    void setState2(Identifier value)
    Deprecated.
    Do not use this method. The correct way is to set the workflow action on save.
    /*************************************************************
    We are using JAVA API : 6.0.1 Version and We just need to know can we use the above function I am asking this question as I could see this is deprecated).
    We have lot of in between states to jump , so it will be make us easy if we use this function.
    Need your suggestions on this.
    Thanks
    Girish

    • rsjazz says:

      I will not be able to suggest anything here. The method is deprecated for several years already. The reason for it being that using it also means not using a legal action with operational behavior. As with anything else in the API using it is on your own risk. Using deprecated methods can cause issues if the method is actually removed in a future version. It is basically the risk you have to take. In https://rsjazz.wordpress.com/2012/11/27/resolve-parent-if-all-children-are-resolved-participant/ I show how you could use legal actions to get to a state (if there is a path to it). This requires multiple save operations if multiple state changes are needed if there is no direct connection of the states. This is also not ideal, but it uses no deprecated code.

    • rsjazz says:

      Please also note that the deprecated message actually tells you not to use it and why. This is more serious than just the deprecation. I am however aware that the method is very convenient for some automation use. To avoid it, see my other answer.

  8. Vikrant Kamble says:

    Hi Ralph,
    I am getting error “IExtensionsDefinitions cannot be resolved to a variable”. I am doing it using server API. Could you please let me associated plugin for this api to add into dependencies.

  9. Salvatore says:

    Hi Ralph, I’m new about RTC automation, I’m working on it for few months.
    I’m trying to change the status of a workitem (from “new” to “assigned”). I tried to integrate your snippet in my project but I found some problems in this part :
    Identifier resolveActionId = workflowInfo.getResolveActionId();

    in my case resolveActionId is always null, so the state doesn’t change.
    I also tried your entire class downloaded from dropbox (ModifyWorkItemStateOperation), but I have the same issue.
    Do you have any suggest?
    Thanks

    • rsjazz says:

      Check if the workflow actually has a resolve action configured.

      You want to search and provide a state in your case, maybe use the workflow path finder from that post.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s