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.

25 thoughts on “RTC Update Parent Duration Estimation and Effort Participant

  1. Hi Ralph, nice post!! Only one comment to try to follow the example:
    – Where is imported interface IExtensionsDefinitions? I’m having compile errors 😦

    Thanks

    • Hi Anil,

      the Extening Team Concert workshop mentioned in the post shows that on page 64:


      IWorkItem newState = (IWorkItem) saveParameter.getNewState();
      Identifier newStateId = newState.getState2();
      Identifier oldStateId = null;
      IWorkItem oldState = (IWorkItem) saveParameter.getOldState();
      if (oldState != null) // New work item check.
          oldStateId = oldState.getState2();
          if ((newStateId != null) && !(newStateId.equals(oldStateId))) {
      .
      .
      .

      • Thanks Ralph . Meanwhile though iam able to set value to Custom Attribute , not able to identify proper method to Set the Custom attribute as Read Only . Any suggestions please .

      • So far I have not looked into this. I did peek into the API and com.ibm.team.workitem.ide.ui.internal.aspecteditor.type.AddEditAttributeDialog is involved. However, I couldn’t easily find where the data is used to create the attribute.

  2. Thanks for the update . if i understand your reply correctly , are you thing that Custom attribute to be newly added with “Read only” behaviour programatically? No . The custom attribute already created statically and only i have to set the behaviour – Read Only (on the attribute) programmatically .

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

    • Welcome, Joao.

      Also keep in mind that the interface of an advisor and the data returned is slightly different. You would at least have to change the related code parts.

  4. Hi Ralph,

    I have a question regarding this implementation.

    let`s say I have a three children to a workitem and its duration is the sum of all the three.

    what if I remove the Parent link in the children workitem? I mean when I try to get the Parent Handle it will be null.

    how can we handle this?

    Thanks
    Surender

    • What you can try is to compare the parent child references of the old state and the new state, to detect if a parent was removed. If so, the old state should provide you with the work item.

  5. Hello Ralph ,
    We are still getting the stale data exception , even after getting the working copy ,
    In the above said example : the following code to get a working copy
    // We want to modify the parent, so get a working copy.
    parent = (IWorkItem)parent.getWorkingCopy();

    But our code looks like :
    IWorkItemWorkingCopyManager manager= workItemClient.getWorkItemWorkingCopyManager();
    manager.connect(workItem, IWorkItem.FULL_PROFILE, this.monitor);
    IWorkItem workItemWorkCopy= null;
    WorkItemWorkingCopy copy= manager.getWorkingCopy(workItem);
    Will there be any difference .
    Here is the error we are getting :
    com.ibm.team.workitem.common.model.MultiStaleDataException: Stale Data
    at com.ibm.team.workitem.common.internal.util.Utils.checkSaveResult(Utils.java:265) atcom.ibm.team.workitem.client.internal.WorkItemWorkingCopyRegistry.saveWorkItems(WorkItemWorkingCopyRegistry.java:2330)
    ……Girish

    • You have to refetch the item. Eg.

      IAuditableCommon.resolveAuditable(workItemHandle, IWorkItem.FULL_PROFILE, monitor);

      This should make sure to have the latest work item data and you can change the attributes you want to then and save. Get the workingcopy for the newly fetched item.

  6. Hello Ralph,
    I have applied your method to modify a workitem from a rtc plugin server with the method “saveWorkitem3”. So I have added the requiredService IWorkitemServer.
    I have tested my development on my jetty server as explained in your website and there is no problem.
    I have built the plugin to deploy it on my RTC server … no problem … Note : I have done this operation many times this last months without problem.
    But when I restart my RTC server, I have this error on my plugin : “bundle could not be resolved” …
    What is the problem ? Have you got an idea ? It’s the first time that I use the requiredService IWorkitemServer, I have perhaps forgotten a configuration …
    Thanks for your help
    Regards
    Mathieu

    • The important part is what bundle could not be resolved. The message basically says your extension has a dependency to a bundle (plugin/feature) that can not be found.

      Remove the dependency and it should load. The issue does not come up in Jetty, because there you have the client and the server SDK and more. So you have likely added a dependency to a client API.

      • Ralph,
        You are right. I have found this night !!! I have included in the dependencies a library *.client
        There is no problem on jetty but it fails on the server
        Regards

  7. Hi Ralph

    If I want to update a custom attribute on my parent , I need to create a new function (or modify the getDuration).
    Where do I need to put the id of my custom attribute I want to update?
    f (attribute != null && workItem.hasAttribute(attribute)) {
    Long tempDuration = (Long) workItem.getValue(attribute);
    if (tempDuration != null && tempDuration.longValue() > 0)
    return tempDuration.longValue();
    }
    return duration;
    Is there on “workitem.getValue(attribute);? Like workitem.getValue(custom_attribute_id) ?

Leave a reply to João Bosco Jares Cancel reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.