RTC Update Parent Duration Estimation and Effort Participant


I am helping users with questions in the forum  for quite some time now. One area where a lot of questions come up is around the API and how to extend Rational Team Concert. One very popular question, and really asked a lot recently, is how to update a parent or child work item when saving a work item. Since this comes up so often and I can’t find the example I believe I once found on the Jazz Wiki anymore, I wrote my own code and I intent to show it in this post.

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.

The example in this blog shows RTC Server and Common API.

Download

*Update* I published a slightly enhanced version of the code presented below in the post Resolve Parent If All Children Are Resolved Participant. You can download the code here and it contains this example as well.

Solution Overview

The task is simple: when a work item gets saved, we want to update the estimates, correction and time spent on the parent work item based on the accumulated data of all its children.

Rational Team Concert supports this by creating a so called Participant. The Participant is one or more Eclipse plug-ins that are extending the extension point com.ibm.team.process.service.operationParticipants for, in this case, the operation ID com.ibm.team.workitem.operation.workItemSave. You can find a list of extension points and operation ID’s here in the Jazz Wiki. The Rational Team Concert 4.0 Extensions Workshop shows all the steps required to create a complete participant also sometimes called a follow up action. Please note that all code below is for a Server extension. Client extensions would use client libraries that have similar names.

The following picture shows the data on the work item that we are interested in.

Estimation and effort tracking data of a work item
Estimation and effort tracking data of a work item

*Update* A participant or follow up action works after the fact of saving. RTC also supports preconditions or Advisors. Start here if you are looking into doing something like this.

Lets look at the initial code. The explanation follows.

package com.ibm.js.team.workitem.extension.updateparent.participant;

import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.eclipse.core.runtime.IProgressMonitor;

import com.ibm.team.links.common.IItemReference;
import com.ibm.team.links.common.ILink;
import com.ibm.team.links.common.IReference;
import com.ibm.team.links.common.factory.IReferenceFactory;
import com.ibm.team.process.common.IProcessConfigurationElement;
import com.ibm.team.process.common.IProjectAreaHandle;
import com.ibm.team.process.common.advice.AdvisableOperation;
import com.ibm.team.process.common.advice.runtime.IOperationParticipant;
import com.ibm.team.process.common.advice.runtime.IParticipantInfoCollector;
import com.ibm.team.repository.common.TeamRepositoryException;
import com.ibm.team.repository.service.AbstractService;
import com.ibm.team.workitem.common.ISaveParameter;
import com.ibm.team.workitem.common.IWorkItemCommon;
import com.ibm.team.workitem.common.model.IAttribute;
import com.ibm.team.workitem.common.model.IWorkItem;
import com.ibm.team.workitem.common.model.IWorkItemHandle;
import com.ibm.team.workitem.common.model.IWorkItemReferences;
import com.ibm.team.workitem.common.model.WorkItemEndPoints;
import com.ibm.team.workitem.service.IWorkItemServer;

public class UpdateParentDuration extends AbstractService implements
IOperationParticipant {

	// The attribute ID's hard coded. TODO: make this configurable
	private static final String WORKITEM_ATTRIBUTE_CORRECTEDESTIMATE = "correctedEstimate";
	private static final String WORKITEM_ATTRIBUTE_TIMESPENT = "timeSpent";

	// Services we need
	private IWorkItemServer workItemServer;
	private IWorkItemCommon wiCommon;

	@Override
	public void run(AdvisableOperation operation,
		IProcessConfigurationElement participantConfig,
		IParticipantInfoCollector collector, IProgressMonitor monitor)  throws TeamRepositoryException {

		// First check that the operation was a 'save' and get he operation data.
		Object data= operation.getOperationData();
		if (!(data instanceof ISaveParameter))
			return;

		// Check that this was a save operation on a work item
		ISaveParameter saveParameter= (ISaveParameter) data;
		if (!(saveParameter.getNewState() instanceof IWorkItem))
			return;

		/**
		 *  remove comment from the code below to prevent the code from recursive updates
		 */
		// if (saveParameter.getAdditionalSaveParameters().contains(IExtensionsDefinitions.UPDATE_PARENT_DURATION_EXTENSION_ID)) { return; }

		// Check to see if the work item has a 'Parent'
		IWorkItemHandle parentHandle = findParentHandle(saveParameter, monitor);
		if (parentHandle == null)
			return;

		// Get the required service interfaces
		workItemServer = getService(IWorkItemServer.class);
		wiCommon = getService(IWorkItemCommon.class);
		// Roll the child estimates up into the parent estimate
		updateParent(parentHandle, monitor);
	}

What the code does is essentially, get the parameters and check if it is responsible for this operation. If this is the case it checks if a parent exists, retrieves it, and then tries to update the parent from its children. It tries to decide as fast as possible if it has to run. The reason is that it would block a user interface operation longer than necessary if it does too much work. It would be possible to add additional checks. For example it would make sense if the save has changes to the attributes we are interested in.

Avoid Recursions

The checks contain code that is commented out to be able to avoid recursive calls of the participant. The details are described in the section Save The Parent.

What is missing now is the code to find the parent. This is done in the following operation:

/**
 * Find the parent of this work item
 * @param saveParameter
 * @param monitor
 * @return a work item handle of the parent or null if a parent does not exist.
 */
private IWorkItemHandle findParentHandle(ISaveParameter saveParameter, IProgressMonitor monitor) {

	// Check to see if the references contain a 'Parent' link
	List references = saveParameter.getNewReferences().getReferences(WorkItemEndPoints.PARENT_WORK_ITEM);
	if (references.isEmpty())
		return null;

	// Traverse the list of references (there should only be 1 parent) and
	// ensure the reference is to a work item then return a handle to that work item
	for (IReference reference: references)
	if (reference.isItemReference() && ((IItemReference) reference).getReferencedItem() instanceof IWorkItemHandle)
		return (IWorkItemHandle)((IItemReference) reference).getReferencedItem();
	return null;
}

The  code basically gets the parent references of the new state of the work item that is being saved and returns it if one exists.

The last part we are missing is the most complex one. We want to read the children of the parent we found and update the parent with the accumulated value of the estimation and effort data. This is done with the code below:

/**
 * Update the parent from the estimation data of its children.
 *
 * @param parentHandle
 * @param monitor
 * @throws TeamRepositoryException
 */
private void updateParent(IWorkItemHandle parentHandle, IProgressMonitor monitor) throws TeamRepositoryException
{
	// Get the full state of the parent work item so we can edit it
	IWorkItem parent = (IWorkItem)workItemServer.getAuditableCommon().resolveAuditable(parentHandle,IWorkItem.FULL_PROFILE,monitor).getWorkingCopy();
	IAttribute timeSpentAttribute = wiCommon.findAttribute(parent.getProjectArea(), WORKITEM_ATTRIBUTE_TIMESPENT, monitor);
	IAttribute correctedEstimateAttribute = wiCommon.findAttribute(parent.getProjectArea(), WORKITEM_ATTRIBUTE_CORRECTEDESTIMATE, monitor);

	long duration = 0; // Estimate
	long correctedEstimate = 0; // Corrected estimate
	long timeSpent = 0; // TimeSpent

	// get all the references
	IWorkItemReferences references = workItemServer.resolveWorkItemReferences(parentHandle, monitor);
	// narrow down to the children
	List listChildReferences = references.getReferences(WorkItemEndPoints.CHILD_WORK_ITEMS);

	IReference parentEndpoint = IReferenceFactory.INSTANCE.createReferenceToItem(parentHandle);
	for (Iterator iterator = listChildReferences.iterator(); iterator.hasNext();) {
		IReference iReference = (IReference) iterator.next();
		ILink link = iReference.getLink();
		if (link.getOtherEndpointDescriptor(parentEndpoint) == WorkItemEndPoints.CHILD_WORK_ITEMS) {
			IWorkItem child = (IWorkItem) workItemServer.getAuditableCommon().resolveAuditable( 
				(IWorkItemHandle)link.getOtherRef(parentEndpoint).resolve(), WorkItem.FULL_PROFILE, monitor);
			long childDuration = child.getDuration();
			timeSpent+=getDuration(child,timeSpentAttribute,monitor);
			correctedEstimate+=getDuration(child,correctedEstimateAttribute,monitor);
			if(childDuration>0)
				duration += childDuration;
		}
	}

	// We want to modify the parent, so get a working copy.
	parent = (IWorkItem)parent.getWorkingCopy();
	// Set the duration on the parent to be the total of child durations
	parent.setDuration(duration);
	// Set the corrected estimation
	parent.setValue(correctedEstimateAttribute, correctedEstimate);
	// Set the time spent/remaining
	parent.setValue(timeSpentAttribute, timeSpent);

	// Save the work item with an information that could be used to prevent recursive ascent.
	Set additionalParams = new HashSet();
	additionalParams.add(IExtensionsDefinitions.UPDATE_PARENT_DURATION_EXTENSION_ID);
	workItemServer.saveWorkItem3(parent, null, null,additionalParams);
	return;
}

This code does a lot. First it gets the full state of the parent. We need the parent as a work item to be able to get the attributes we are interested in and we need the latest state so that the work item can be edited at all.

Then the code looks up the attributes for Time Spent/Time Remaining and the Corrected Estimate, using the attribute ID’s.

The code then iterates the references of the parent to find the child work items. For each child it looks up the values of the attributes we are interested in and adds the data up, if there is a value. The value -1 indicates the attribute is uninitialized.

The last steps are to get a working copy of the parent work item so that it can be modified. Then the calculated values are set.

Save The Parent

Finally the work item is saved.

The saveWorkItem3() operation takes an additional parameter, a set of strings. This can be used to detect that a subsequent trigger of the participant was caused by this save operation. The following code inserted into the run() operation would allow to prevent this from happening, e.g. to prevent that the parent’s save causes another roll up.

Communication Between Operations to Avoid Recursions

The code updates the parent work item. This will cause a workitem save operation and also trigger the associated advisors and follow up actions including this one. The saving of the parent will cause this participant to run and update its parent and so forth.

There are cases, where this is OK, like in this case. But there are other cases where this can cause issues like loops and the like. Loops or endless recursions can cause  your server to crash, so you need to prevent this from happening.

This is what the code below can be used for. This code looks at additional parameters – basically strings. If some expected string is present the operation finishes.  The additional parameter is provided when saving the work item already in the code above.

/**
 *  remove comment from the code below to prevent the code from recursive updates
 */
if (saveParameter.getAdditionalSaveParameters().contains(IExtensionsDefinitions.UPDATE_PARENT_DURATION_EXTENSION_ID)) {
	return; 
}

There is still some code missing, that gets the value of the attributes for Time Spent and the Corrected Estimate. If there is no data we return 0 so that we don’t break anything.

private long getDuration(IWorkItem child, IAttribute attribute, IProgressMonitor monitor) throws TeamRepositoryException {
	long duration = 0;
	if(attribute!=null && child.hasAttribute(attribute)){
		Long tempDuration = (Long)child.getValue(attribute);
		if(tempDuration!=null && tempDuration.longValue()>0)
			return tempDuration.longValue();
	}
	return duration;
}

Now the participant’s code is finished. You would have to create the plugin and a component as described in the Rational Team Concert 4.0 Extensions Workshop to deploy it. The plugin.xml would look similar to the code below, please note the prerequisites that you have to enter manually. All services you want to use need to be listed here. There is also a reference to a component com.ibm.js.team.workitem.extension.component that is defined in its own plug-in.

Extension plugin.xml
Extension plugin.xml

* Update* I was leaving out the IExtensionsDefinitions. It just defines the ID of the extension

package com.ibm.js.team.workitem.extension.updateparent.participant;

public class IExtensionsDefinitions {
	/**
	 * The extension id is used to identify the operation participant to Jazz.
	 * It is also included in instantiations of the participant in process
	 * definitions.
	 */

	public static final String UPDATE_PARENT_DURATION_EXTENSION_ID = "com.ibm.js.team.workitem.extension.updateparentduration";
}

Now we have the most important code for the plugin. You should be able to get it working. Please remember that there is few error handling at this point. You might want to enhance this.