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.
*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.
* 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.
Great post, I hear this type of request all of the time from Jazz users.
Yes, it is popular. Stay tuned for the participant that updates the state of a work item.
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 Jorge,
it only defines the ID of the extension. I added the code.
HI Ralph Just Wondering , how to access the Old and New State of the Workitem Workflow ?
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.
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 .
Nevertheless, I am convinced that needs to be done in the process API and not in the API I usually use for accessing the work items.
Pingback: Resolve Parent If All Children Are Resolved Participant | rsjazz
Hi Ralph, nice post! This approach works as well to operationAdvisor, or just for operationParticipants?
Hi Joao, in general the code should work for advisors as well as participants.
However, an advisor is something that should run upfront of a save, check conditions and prevent saving if conditions don’t meet requirements.
A participant does work after that save is checked by the advisors. So I would not update any work items in an advisor.
Also see: https://rsjazz.wordpress.com/2012/12/14/do-not-modify-the-triggering-element-in-an-operationadvisor/ . Having said that, the code does not update the triggering element.
I would still consider the purpose of advisors and participants and avoid doing this in advisors.
Thanks so much 🙂
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.
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.
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.
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
Things like this happen. Great you found it.
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) ?
https://rsjazz.wordpress.com/2013/01/02/working-with-work-item-attributes/ explains the basics