New Version and Now What? Experiences With SDK and Plain Java API across Tool Versions


There is a new version of RTC – what does that mean in the context of my automation Plain Java Client Libraries tools and my Extensions based on the SDK? This very valid question was recently asked on Jazz.net at least partially in this question.

I looked into it as far as I was able to and answered it briefly in the forum. I think it is worth adding a blog post as well.

Please be aware, as always, this is my personal experience and not a statement of IBM. Please also remember, as stated in the disclaimer, that this comes with the usual lack of promise or guarantee.

Just Starting With Extending RTC?

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.

Java APIs

As described in this post, RTC has at least the following Java APIs:

  1. Plain Java Client Libraries – for stand alone Java client applications
  2. The RTC client side SDK – for Eclipse client and other eclipse based extensions
  3. The RTC server side SDK – for server side extensions

It is somewhat important to distinguish between the different API’s because there are different dependencies on versions in extensions using the SDK and Plain Java Client Library based Java Tools.

Versions in Extension vs. in the Plain Java Client Libraries

A Plain Java Client Library tool just requires the classpath to include a version of the Plain Java Client Libraries. It just calls the classes available and does not check for any version information.

The Extensions plug-in manifest can specify dependencies to features, plug ins and other objects available in the SDK. These dependencies optionally can include a specification for the version number required by the extension. It is possible to specify no version, a minimal version, a range and a specific version of the dependent object to be required for an extension.

Plug In Version Dependencies

Plug In Version Dependencies

If you have a dependency specified this way and the dependency can not be satisfied by the available version, your plugin will not load.

Client vs. Server

It is also necessary to distinguish between client extensions and server extensions.

A client extension runs standalone or in a client and can potentially run in any version of the client. The libraries used – Plain Java Client Libraries or client SDK installed that come with the client – have a version.When the client connects to the server the first thing the server checks is, if the clients version is compatible.

The RTC Development Team attempts to provide N-1 compatibility from client to server. This means, the RTC Server should be compatible with clients with lower minor version numbers and 1 Major version number. This is handled in the server as mentioned above. Looking at RTC 4.x it should be possible to access the 4.x server with clients of smaller 4.x versions down to a 3.x client (N-1).

The server checks the clients version and rejects a connection if the versions are known to be incompatible. N+1 is not tested and rated as incompatible. So it is not possible to connect a RTC 4.0.3 Eclipse client with a RTC 4.0.1 server. The server won’t accept the connection as displayed below. Please note, this is also true for a Plain Java Client Library client.

4.0.3 Client Incompatible to 4.0.1 Server

4.0.3 Client Incompatible to 4.0.1 Server

Client/Server Compatibility Summary

Please see the Infocenter for the most current client/server compatibility data. The information is also provided in this TechNote.

Running Plain Java applications inherits the N-1 compatibility that is attempted, if it was achievable.

I ran a work item creation Plain Java Client Libraries app from within 3.0.1.1 against a RTC 4.0.3 server with no problem. Same with 4.0.3 clients against 5.0.x servers and 5.0.x clients against 6.0.x servers.

If the server version is more than 2 major versions ahead of the used Plain Java Client Libraries, the client will fail with a version incompatibility error. The same happens if the server version is smaller than the plain java client libraries. Provide a correct and compatible version of the Plain Java Client Libraries.

An RTC client SDK Extension based on the SDK  deployed in a client inherit the N-1 compatibility, if it was achieved.

If you have an extension in the client deployed (for example the RTC 3.0 Eclipse client), the client should be able to connect to servers with higher version numbers  as explained above. So the client extension should run – provided it does not depend on a server side extension that is not deployed in that server.

Extensions, Version Compatibility and Upgrade

What is missing is what happens to my extensions that have been created with earlier versions of the client or server SDK.

I have upgraded my extensions from 3.x to 4.x over the years. My experience so far is that it always was a smooth transition. I have seen very little changes in the API, some few deprecated classes. The only major changes I have seen are new API’s and in RTC 3.x permission aware read access. It is necessary to think about unavailable read access in RTC 3.x or higher. In RTC 2.0 there was no read access restrictions and you could always read any data. Make sure your RTC extension does not run in an environment that violates the assumptions made when it was created.

To upgrade, I usually set up a new development workspace (I am now working with 4.0.1 and looking into going to 4.0.3 see https://rsjazz.wordpress.com/2013/02/28/setting-up-rational-team-concert-for-api-development/ ). I usually load or import my extensions created with older SDK versions and make sure they do compile, debug or test them quickly. I would create the update site from this version and deploy it.

Avoiding Version Dependencies in Extensions

However, it is not always necessary to do this. If you make sure there is no version conflict in the required plug-in dependencies, a plug in created in an earlier version should run in a newer version, unless there are breaking API changes such as classes or packages renamed or made unavailable.

I just built the Build On State Change Participant from the RTC Extensions Workshop with 3.0.1.1 and deployed it in 4.0.3 client and server. It worked as expected in both the client as well as the server without rebuild. The dependencies in the Example don’t specify a version number they require and therefore the deployment is not rejected.

However, since there can be API changes in newer versions, it is a good practice to test your extensions with a newer SDK anyway.

Summary

RTC supports N-1 compatibility between clients and servers, but not N+1 compatibility. If you have client extension or Plain Java Client Libraries based automation, the versions used should be equal or less than the server versions used.

Eclipse Extensions based on the SDK can be made somewhat version independent as long as there are no API changes that break the interface contracts.

As always I hope this information is useful to others and helps them with deploying their RTC or CLM solution.

Advertisements

Maintaining User Photos With the Plain Java Client Libraries


There is an interesting question on Jazz.net asking if it is possible to download and re-size the personalized user images in Jazz using the Java or Rest API. This post shows the Java API to do that. 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.

Why is this of interest?

Users can upload a personalized photo to their account in Jazz, to support easier collaboration. The images are loaded and displayed in plans. Today Jazz does not rescale the images that are uploaded, it just stores them. So the size of the photo uploaded has an impact on the UI in plans, especially in the Web UI. Related, this work item asks for the feature to crop and optimized photos on upload. In the meantime it would be nice if it would be possible to automate downloading, rescaling and uploading of the images. The Java API allows to do just that.

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 API.

Download

You can download the code from here.

The code contains only one class ManageContributorPhoto is the main class that needs to be called

Please consider to test this in a test environment until it works as desired. Please also be aware that the code is only barely tested and you might want to change how it works for your environment

How to use the tool

You need to import the sources into Eclipse and provide the plain java client libraries as described in this article, in order to be able to run it. In this case a sample launch comes with the code. Alternatively you can compile the example code and provide the plain Java Client Libraries in the classpath.

The code has been developed using the SDK to be able to browse the API better and is also configured as a plug-in. If you don’t have your Client set up with an SDK you can remove the file plugin.xml and the folder META-INF and all its content.

Call ManageContributorPhoto.main with parameters repositoryURI AdminUserId AdminPassword where is down or up for download of the images or upload of modified ones. You can add another parameter pointing to a folder that should be used to store the images. For example “C:/temp/images/” or “/tmp/images”. Make sure to provide the trailing slash and make sure the folder exists.

For example call it with the following parameters to download the images into “C:/temp/images””

"https://clm.example.com:9443/jts" user password down "C:/temp/images/"

To upload the images after modification use

"https://clm.example.com:9443/jts" user password up "C:/temp/images/"

The Example ships with two launches as presented above that you should be able to use.

Please be aware that the context root jts is not a typo. You want to change the users in JTS, which promotes the changes to all the other applications. Please check that the uid’s have been changed in the registered applications after running the tool.

The tool downloads the available images for all contributors in the repository and stores them in a file created from the user name,the user ID and an extension ‘.jpg’ as shown in the image below. For uploading it expects the same format. It will not upload or change anything if no image is available.

UserPictures

Contributor Details

As already published, for instance in the post Changing the Jazz User ID Using the RTC Plain Java Client Libraries the information about the user is kept in a Contributor object. However, the photo image is not directly stored in this object. Instead there is an object implementing the IContributorDetails interface that has holds additional information to the contributor. These details also contain the reference to the picture or image we are interested in.

The data is accessible using this code:

IContributorDetailsHandle detailsHandle = contributor.getDetails();

The handle needs to be resolved or fetched to get at the data as usual. We will look at the details soon.

The Method run

The main code is done in the method run, which extracts the parameters, connects to the team repository and iterates all contributors of the repository. It calls the code to download or upload the image for each contributor based on the direction parameter provided.

Downloading Files

If the direction parameter down is provided the method downloadPhoto() is called that deals with the details. The code looks as follows.

/**
 * Download a photo (if exists) from a contributor to a file. The image file is
 * made available in the images folder (by default the execution folder). 
 * The Filename is composed from the base location user name, ID and the extension '.jpg'.
 * 
 * @param teamRepository
 *            must not be null
 * @param contributor
 *            must not be null
 * @param monitor
 * @throws TeamRepositoryException
 * @throws IOException 
 */
public static void downloadPhoto(ITeamRepository teamRepository,
		IContributor contributor, IProgressMonitor monitor)
			throws TeamRepositoryException, IOException {
	System.out.print(" Download ");
	// Get the contributor details to be able to access the photo
	IContributorDetails details = getDetails(teamRepository, contributor, monitor);
	if (details != null) {
		IContent photo = details.getPhoto();
		if (photo != null) {
			try {
				downloadSaveContent(teamRepository, photo, getFileName(contributor), monitor);
			} catch (IOException e) {
				System.out.println("\nException "+ e.getMessage());
				e.printStackTrace();
				throw e;
			}
		}
	}
}

The code tries to access the details of a contributor, gets the photo, if available, and tries to download the photo to a given file name constructed from the contributor information. The methods getDetails() and getFileName() are used by the upload and download code to get the details and construct the file name. The code of these methods looks as below.

/**
 * Create a file name string from a root path and 
 * the contributor information
 * 
 * @param contributor must not be null
 * @return
 */
public static String getFileName(IContributor contributor) {
	return imageRoot + contributor.getName() + "_" + contributor.getItemId() + ".jpg";
}

/**
 * Try to get the contributor details
 * 
 * @param teamRepository
 *            must not be null
 * @param contributor 
 *            must not be null
 * @param monitor
 * 
 * @return the resolved details if found or null
 * 
 * @throws TeamRepositoryException
 */
public static IContributorDetails getDetails(
		ITeamRepository teamRepository, IContributor contributor,
		IProgressMonitor monitor) throws TeamRepositoryException {
	// Try to get the contributor details handle
	IContributorDetailsHandle detailsHandle = contributor.getDetails();
	if (detailsHandle == null) {
		System.out.print("Details null");
		return null;
	}
	// Fetch the details if the handle is not null
	IContributorDetails details = (IContributorDetails) teamRepository
			.itemManager().fetchCompleteItem(detailsHandle,
					IItemManager.DEFAULT, null);
	return details;
}

The method downloadSaveContent below, is used to download the image and store it in a local file. The way it is set up the whole operation fails if the file can’t be saved.

/**
 * Download content to a given file.
 * 
 * @param teamRepository
 *            must not be null
 * @param content
 *            must not be null
 * @param filename
 *            must not be null
 * @param monitor
 * 
 * @return
 * @throws TeamRepositoryException
 * @throws IOException 
 */
public static void downloadSaveContent(ITeamRepository teamRepository,
		IContent content, String filename, IProgressMonitor monitor)
		throws TeamRepositoryException, IOException {
	File save = new File(filename);

	OutputStream out;
	try {
		out = new FileOutputStream(save);
		try {
			teamRepository.contentManager().retrieveContent(content, out, monitor);
			System.out.print(" " + filename + " download succeeded..."
					+ " Type: " + content.getContentType() + " Encoding: "
					+ content.getCharacterEncoding() + " LineDelemiter: "
					+ content.getLineDelimiter());
		} finally {
			out.close();
		}
	} catch (FileNotFoundException e) {
		System.out.print("File: " + filename
				+ "filename could not be created...");
		throw e;
	}
}

This is all the code needed for downloading. Please be aware that the download uses the file type and encoding set for the content when downloading.

Uploading an Image File

If the direction parameter up is provided the method uploadPhoto() is called that deals with the details of uploading an image to the details. The code looks as follows.

/**
 * Uploads a photo from an image file to the contributor. 
 * The image file is expected to be available in the images folder (by 
 * default the execution folder). 
 * The Filename is composed from the base location user name, ID and the extension '.jpg'.
 * 
 * @param teamRepository
 *            must not be null
 * @param contributor
 *            must not be null
 * @param monitor
 * 
 * @throws TeamRepositoryException
 * @throws IOException 
 */
public static void uploadPhoto(ITeamRepository teamRepository,
		IContributor contributor, IProgressMonitor monitor)
		throws TeamRepositoryException, IOException {

	System.out.print(" Upload ");
	// Try to upload the image as content.
	IContent newPhoto = uploadContent(teamRepository,
			getFileName(contributor), monitor);
	if (newPhoto != null) { // If the image was uploaded and content created
		// Get the details for the user. Create the details in case they are missing
		IContributorDetails details = getDetails(teamRepository,contributor, monitor);
		if (details == null) { 
			details = (IContributorDetails) IContributorDetails.ITEM_TYPE.createItem(teamRepository);
		}
		// Set the photo
		IContributorDetails detailsWorkingCopy = (IContributorDetails) details.getWorkingCopy();
		detailsWorkingCopy.setPhoto(newPhoto);
		// Set the details
		IContributor contributorWorkingCopy = (IContributor) contributor.getWorkingCopy();
		teamRepository.contributorManager().setContributorDetails(
				contributorWorkingCopy, detailsWorkingCopy, monitor);
	}
}

The code first tries to upload the content from the file then it tries to get the details and, if there was an upload either uses the existing contributor details or creates new details if needed.
It gets a working copy of the details, sets the photo content to it. Last but not least, it gets a working copy for the contributor and sets the contributor details that have been modified.

The code of the method uploadContent() to upload the image and to create the content with the image is shown below.

/**
 * Uploads an image to the repository and creates a new content.
 * 
 * @param teamRepository
 *            must not be null
 * @param filename
 *            must not be null
 * @param monitor
 * 
 * @return the content if created, null otherwise
 * 
 * @throws TeamRepositoryException
 * @throws IOException 
 */
public static IContent uploadContent(ITeamRepository teamRepository,
		String filename, IProgressMonitor monitor)
		throws TeamRepositoryException, IOException {
	IContent newContent = null;
	try{
		File uploadFile = new File(filename);
		FileInputStream fin = new FileInputStream(uploadFile);
		try {
			// Store the content with a meaningful content type, encoding and line delimiter.
			newContent = teamRepository.contentManager().storeContent(
					"application/octet-stream", IContent.ENCODING_UTF_8,
					LineDelimiter.LINE_DELIMITER_NONE, fin, null, monitor);
			System.out.print("Uploaded: " + filename);
		} finally {
			fin.close();
		}
	} catch (FileNotFoundException e) {
		System.out.print("File: " + filename
				+ " Filename could not be found...");
		// If there is no image file, we don't upload one.
	}
	return newContent;
}

Other than for the download, if the file for the image is not found, this code continues and just does not change the details.
The code provides content type and encoding similar to what was downloaded.

Summary

The code above allows to easily first download the images, then run some tool to scale them down and finally upload all the images for all users (that have an image) or even only for the images changed. I hope it is worth reading and helps some administrator out there to save some time.

A Create Approval Work Item Save Participant


This post is about a Participant that creates an approval when saving a work item. I was interested in posting this, because I was interested on how to get at project member and role information in a server extension. I had already helped a partner with a similar effort in the past, where the approver information was supposed to be read in an external system. Back then I couldn’t find how to access the project area information and to find the roles.

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 post shows RTC Server and Common API.

Download

You can download the code here. The API code in this post is Server and Common API.

Solution Overview

The code provides you with several classes. The interesting one is com.ibm.js.team.workitem.createapproval.participant.CreateApprovalParticipant. This class implements the participant that creates an approval if a workitem changes into the state "com.ibm.team.workitem.common.model.IState:com.ibm.team.apt.story.tested".

In case this state change is detected, the participant runs the following code to get approvers by their role Product Owner.

/**
 * @param workItem
 * @param collector
 * @param role
 * @param monitor
 * @throws TeamRepositoryException
 */
private void createApprovalByRole(IWorkItem workItem,
		IParticipantInfoCollector collector, String role, IProgressMonitor monitor) throws TeamRepositoryException {

	IContributorHandle[] approvers=findApproversByRole(workItem, "Product Owner", monitor);
	createApproval(workItem, collector, approvers, monitor);
}

The method called to find the approvers looks like the following code. The code gets the process area that governs the work item. and tries to get contributors with matching roles.

If there are no contributors that could be found with a matching role, it tries the same with the project area. The contributors are returned to create the approval.

Please note, this strategy could be changed into recursively start at the project area an find the enclosed team area hierarchy and then try all team areas in the hierarchy from the one that owns the work item up to the project area. This is left as a good example for the you to implement.

/**
 * Finds Approvers by role. Looks in the process area that owns the work item first, 
 * then looks at the project area if it was not already looking at it.
 * 
 * @param newState
 * @param roleName
 * @param monitor
 * @return
 * @throws TeamRepositoryException
 */
private IContributorHandle[] findApproversByRole(IWorkItem newState,
		String roleName, IProgressMonitor monitor) throws TeamRepositoryException {
	IProcessAreaHandle processAreaHandle = fWorkItemServer.findProcessArea(
		newState, monitor);
	IProcessArea processArea = (IProcessArea) fAuditableCommon.resolveAuditable(processAreaHandle,
		ItemProfile.createFullProfile(IProcessArea.ITEM_TYPE), monitor);
	IContributorHandle[] contributors = findContributorByRole(processArea, roleName, monitor);

	if(contributors.length==0){
		IProjectAreaHandle projectAreaHandle = processArea.getProjectArea();
		if(!projectAreaHandle.getItemId().equals(processAreaHandle.getItemId())){
			IProcessArea projectArea = (IProcessArea) fAuditableCommon.resolveAuditable(projectAreaHandle,
				ItemProfile.createFullProfile(IProcessArea.ITEM_TYPE), monitor);
			return findContributorByRole(projectArea, roleName, monitor);
		}
	}
	return contributors;
}

The code to find the approvers by role gets the members of the process area, then gets the contributors with the role name provided and returns the result. The code can be seen below.

/**
 * Find contributors by role on a process area.
 * 
 * @param processArea
 * @param roleName
 * @param monitor
 * @return
 * @throws TeamRepositoryException
 */
public IContributorHandle[] findContributorByRole(
		IProcessArea processArea, String roleName,
		IProgressMonitor monitor) throws TeamRepositoryException {
	fProcessServerService = getService(IProcessServerService.class);
	IContributorHandle[] members = processArea.getMembers();
	IContributorHandle[] matchingContributors = fProcessServerService
		.getContributorsWithRole(members, processArea,
		new String[] { roleName });
	return matchingContributors;
}

Finally the code below creates the approval if there are approvers that are passed. It gets the full state of the work item. Then it gets the approvals and creates a new descriptor for the new approval. For each approver it creates an approval with the new descriptor and then adds it to the approvals. Finally it saves the work item.

In case there are no approvers or the save is prevented, an error info is generated.

/**
 * Creates an approval and adds all approvers from an array
 * 
 * @param workItem
 * @param collector
 * @param monitor
 * @throws TeamRepositoryException
 */
private void createApproval(IWorkItem workItem,
		IParticipantInfoCollector collector, IContributorHandle[] approvers, 
		IProgressMonitor monitor) throws TeamRepositoryException {

	if (approvers.length==0) {
		String description = NLS.bind("Unable to create the Approval",
			"Unable to find an approver for the work item ''{0}''.",
			workItem.getItemId());
		IReportInfo info = collector.createInfo(
			"Unable to create approval.", description);
		info.setSeverity(IProcessReport.ERROR);
		collector.addInfo(info);
		return;
	}
	// Get the full state of the parent work item so we can edit it
	IWorkItem workingCopy = (IWorkItem) fWorkItemServer.getAuditableCommon()
		.resolveAuditable(workItem, IWorkItem.FULL_PROFILE, monitor)
		.getWorkingCopy();

	IApprovals approvals = workingCopy.getApprovals();
	IApprovalDescriptor descriptor = approvals.createDescriptor(
		WorkItemApprovals.REVIEW_TYPE.getIdentifier(), APPROVAL_NAME);
	for (IContributorHandle approver : approvers) {
		IApproval approval = approvals.createApproval(descriptor, approver);
		approvals.add(approval);
	}
	IStatus saveStatus = fWorkItemServer.saveWorkItem2(workingCopy, null, null);
	if (!saveStatus.isOK()) {
		String description = NLS.bind("Unable to create the Approval",
			"Unable to save the work item ''{0}''.",
			workItem.getItemId());
		IReportInfo info = collector.createInfo(
			"Unable to create approval.", description);
		info.setSeverity(IProcessReport.ERROR);
		collector.addInfo(info);
	}
}

The code to download contains other examples for how to get approvers.

Summary
The code is experimental. I have tested it in a Jetty based test server using the Scrum template. It is by no means production ready and can be enhanced for various scenarios. However, as always, I hope the code is an inspiration and helps someone out there to save some time. If you are just starting to explore extending RTC, please have a look at the hints in the other posts in this blog on how to get started.

Resolve Parent If All Children Are Resolved Participant


This post is about a Participant that resolves the parent work item, if all children are resolved. It is the example I used to understand the server API for manipulating work item states. I would probably not use it on a production system, because I strongly believe that a human being should do a conscious decision in cases like that, but it is a nice example about the RTC Work Item Server API.

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

You can download the code here.

The download contains the (slightly enhanced) UpdateParentDuration Participant described in this post, the UpdateParentStateParticipant and the WorkFlowPathfinder from this post as Eclipse projects. In Eclipse use File>Import and select Existing Projects into Workspace. Select the downloaded archive file and do the import. This version of the WorkFlowPathFinder has the Plain Java Client Libraries dependency removed and is treated as real plug in, to provide its services to the other plug ins.

Please note, If you just get started with extending RTC, I would suggest to start reading this and the linked posts to get some guidance on how to set up your environment. Then read the article Extending Rational Team Concert 3.x and follow the Rational Team Concert 4.0 Extensions Workshop at least through the setup and Lab 1. This provides you with a development environment and with a lot of example code and information that is essential to get started. You should be able to use the following code in this environment and get your own extension working.

UpdateParentSateParticipant

The project com.ibm.js.team.workitem.extension.updateparent.participant contains the class UpdateParentState. This implements the Participant that

  1. Checks if there was a state change
  2. Checks if the new state is a closed state
  3. Checks if there is a parent
  4. If there is a parent, checks if all children are closed
  5. Resolves the parent If all children are closed

The UpdateParentStateParticipant can use different strategies to do the state change. The code below shows some of the available options.

if (!resolveBF(parentHandle, monitor)) {
// if (!resolveDF(parentHandle, monitor)) {
// if (!resolveWithResolveActionID(parentHandle, monitor)) {
// if (!gotoStateBF(parentHandle,"4",true, monitor)) {

The methods resolveBF() and resolveDF() use the Breadth First and the Depth First strategy against the resolve action. The method gotoStateBF() uses the Breadth First strategy but uses a target state instead. The method resolveWithResolveActionID() only tries to apply the action ID. You can play around with the different strategies and pick the one you like best.

The resolve Action needs to be defined in the RTC work Item workflow as shown below, otherwise the state change can’t be done, because no path can be found.

Define teh Resolve Action The WorkflowPathFinder class in the download provides two other ways to find a target state, instead of a target action. The methods are called findPathToState_DF() and findPathToState_BF().

UpdateParentDurationParticipant

The UpdateParentDurationParticipant is slightly enhanced compared to the state in RTC Update Parent Duration Estimation and Effort Participant post. It checks if there are changes to the duration and estimate values before doing anything else.

The code also contains several methods to analyze the work item links on the server as described in The RTC WorkItem Server Link API post.

Summary
The code is experimental. I have tested it in a Jetty based test server using the Scrum template. It is by no means production ready and can be enhanced for various scenarios. However, as always, I hope the code is an inspiration and helps someone out there to save some time. If you are just starting to explore extending RTC, please have a look at the hints in the other posts in this blog on how to get started.

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.

Handling Iterations – Automation for the “Planned For” Attribute


One thing that I started thinking about several times during the last few years is how to handle the “Planned For” or other iteration attributes during automation. I was looking into how to copy work items from one repository to another for example. It was not immediately obvious how to work with it and I always had other more pressing demands so that I never finished thinking about it. Some pretty big projects at IBM, with very mature and highly automated processes, that have migrated to RTC. I am helping some of them and the question came up again. This time around, I found the time to really look into it and I can share something. Here goes….

*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 the iteration attribute types. 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 had to do manual changes to the code snippets below. So it might not compile. Use the downloaded code, instead of copying the code examples into your code.

* Update * Pugazhenthi Samidurai also published a neat solution here Jazz.net Forum question.

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.

Download

* Update *: The code can be downloaded from here. The code also has some enhancements over the code shown below, e.g. more options to compare.

About Iterations

If you look at iterations in the API they appear to be an ordinary item at the first glance. Looking deeper, they are a bit special. Like categories, iterations, together with development lines (timelines), compose a tree structure. Unlike categories there is no built in API that allows to find the iteration object based on the path. Categories can be found using code like this.

List path = Arrays.asList(categoryName.split("/"));
ICategoryHandle category = workItemClient.findCategoryByNamePath(projectArea, path, monitor);

There is no such API available for iterations. However, to display an iteration some means to get the path and create a string are required. In case a string, describing the path in the iteration structure, is available some means to find the related iteration is also required. To make it more complex an iteration and a development line have an ID as well as a display name. If someone wants to do a mapping based on some strings it would be nice to be able to map by a path by id as well as by name.

So I ended up developing some helper classes to support me with the effort. One helper class just creates a path from any given iteration up to the development line. The other helper tries to find an iteration based on a given path.

Lets look at the path helper, that creates a paths for a given iteration handle, first. The code I developed can be found below. Some explanations and how to use it follow.

/*******************************************************************************
 * Licensed Materials - Property of IBM
 * (c) Copyright IBM Corporation 2012.  
 * 
 * PathHelper
 * 
 * Note to U.S. Government Users Restricted Rights:  Use, 
 * duplication or disclosure restricted by GSA ADP Schedule 
 * Contract with IBM Corp.
 *******************************************************************************/
package com.ibm.js.team.api.tools.process;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.eclipse.core.runtime.IProgressMonitor;

import com.ibm.team.process.common.IDevelopmentLine;
import com.ibm.team.process.common.IDevelopmentLineHandle;
import com.ibm.team.process.common.IIteration;
import com.ibm.team.process.common.IIterationHandle;
import com.ibm.team.repository.client.IItemManager;
import com.ibm.team.repository.client.ITeamRepository;
import com.ibm.team.repository.common.IFetchResult;
import com.ibm.team.repository.common.TeamRepositoryException;

/**
 * Helps to create a path representation of iteration that build up a logical
 * path down to the root development line. Handles three parallel path
 * representations based on an path created from ID's, from a name and from a
 * label. Allows to output the path using a separator that can be customized. *
 * 
 * @author rschoon
 * 
 */
public class PathHelper {

	List fIdPath = null;
	List fNamePath = null;
	List fLabelPath = null;
	ITeamRepository fTeamRepository = null;
	IProgressMonitor fProgressMonitor = null;

	String fSeperator = "/";

	public PathHelper(ITeamRepository teamRepository,IProgressMonitor progressMonitor) {
		super();
		fTeamRepository = teamRepository;
		fProgressMonitor = progressMonitor;
	}

	private void initialize() {
		fIdPath = new ArrayList();
		fNamePath = new ArrayList();
		fLabelPath = new ArrayList();
	}

	/**
	 * Calculate the path for the iteration. Make paths available for ID's displaynames and labels 
	 * 
	 * @param handle
	 * @throws TeamRepositoryException
	 */
	public void calculateIterationPath(IIterationHandle handle)throws TeamRepositoryException {
		initialize();
		IIteration iteration = resolveIterationHandle(handle);
		getIterationPath(iteration);
	}

	/**
	 * @return the calculated path with names or null if the calculation never ran.
	 */
	public List getNamePath() {
		return fNamePath;
	}

	/**
	 * @return  the calculated path with ID's or null if the calculation never ran.
	 */
	public List getIdPath() {
		return fIdPath;
	}

	/**
	 * @return  the calculated path with labels or null if the calculation never ran.
	 */
	public List getLabelPath() {
		return fLabelPath;
	}

	/**
	 * @return the id path as string or null, if it was never created.
	 */
	public String toIDPathString() {
		return toPathString(fIdPath);
	}

	/**
	 * @return the label path as string or null, if it was never created.
	 */
	public String toLabelPathString() {
		return toPathString(fLabelPath);
	}

	/**
	 * @return the name path as string or null, if it was never created.
	 */
	public String toNamePathString() {
		return toPathString(fNamePath);
	}

	/**
	 * @return the seperator used to build up the string representation
	 */
	public String getSeperator() {
		return fSeperator;
	}

	/**
	 * Set a seperator string used to build up the string representation
	 * 
	 * @param fSeperator
	 */
	public void setSeperator(String seperator) {
		this.fSeperator = fSeperator;
	}

	/**
	 * Find the path to an iteration using the iteration handle
	 * 
	 * @param iteration
	 * @throws TeamRepositoryException
	 */
	private void getIterationPath(IIteration iteration) throws TeamRepositoryException {
		this.add(0, iteration.getId(), iteration.getName(),iteration.getLabel());
		IIterationHandle parentHandle = iteration.getParent();
		if (parentHandle != null) {
			// Recurse into the parent
			IIteration parent = resolveIterationHandle(parentHandle);
			getIterationPath(parent);
		} else {
			IDevelopmentLineHandle devLineHandle = iteration.getDevelopmentLine();
			List handles = new ArrayList();
			handles.add(devLineHandle);
			IFetchResult result = getTeamRepository().itemManager().fetchCompleteItemsPermissionAware(handles,
				IItemManager.REFRESH, getProgressMonitor());
			IDevelopmentLine developmentLine = (IDevelopmentLine) result.getRetrievedItems().get(0);
			add(0, developmentLine.getId(), developmentLine.getName(),developmentLine.getLabel());
		}
	}

	/**
	 * Get the iteration from its handle
	 * 
	 * @param handle
	 * @return
	 * @throws TeamRepositoryException
	 */
	private IIteration resolveIterationHandle(IIterationHandle handle) throws TeamRepositoryException {
		List handles = new ArrayList();
		handles.add(handle);
		IFetchResult result = getTeamRepository().itemManager().fetchCompleteItemsPermissionAware(handles,
			IItemManager.REFRESH, getProgressMonitor());
		return (IIteration) result.getRetrievedItems().get(0);
	}

	private IProgressMonitor getProgressMonitor() {
		return fProgressMonitor;
	}

	private ITeamRepository getTeamRepository() {
		return fTeamRepository;
	}

	/**
	 * Add a path segment to the path
	 * 
	 * @param i
	 * @param id
	 * @param name
	 * @param label
	 */
	private void add(int i, String id, String name, String label) {
		fIdPath.add(i, id);
		fNamePath.add(i, name);
		fLabelPath.add(i, label);
	}

	/**
	 * @param list
	 * @return
	 */
	private String toPathString(List list) {
		boolean first = true;
		String outStr = "";
		for (Iterator iterator = list.iterator(); iterator.hasNext();) {
			String name = (String) iterator.next();
			if (first) {
				first = false;
			} else {
				outStr += fSeperator;
			}
			outStr += name;
		}
		return outStr;
	}
}

The code can be called like below.

PathHelper ph = new PathHelper(teamRepository, monitor);
ph.calculateIterationPath(handle);
String paths = "IDPath: [" + ph.toIDPathString() + "] NamePath: (" + ph.toNamePathString() + ")";

Basically you create a new PathHelper and use the new object to call PathHelper.calculateIterationPath(handle) to calculate the paths for ID and name. The path helper stores both paths and makes them accessible using PathHelper.getNamePath() or PathHelper.getIdPath(). You can also get the string representation using PathHelper.toIDPathString() or PathHelper.toNamePathString(). The string is simply a string of the format "development/Release 1.0/Sprint 2". This is a string containing the name or ID of the development line and the iterations separated by a separator string. The separator can be changed if needed.

The PathHelper class basically allows to read and analyze iterations. Now it would be desirable to be able to locate an iteration based on a given path. The code below finds a development line based on the path.

/*******************************************************************************
 * Licensed Materials - Property of IBM
 * (c) Copyright IBM Corporation 2012.  
 * 
 * DevelopmentLineHelper
 * 
 * Note to U.S. Government Users Restricted Rights:  Use, 
 * duplication or disclosure restricted by GSA ADP Schedule 
 * Contract with IBM Corp.
 *******************************************************************************/
package com.ibm.js.team.api.tools.process;

import java.util.List;

import org.eclipse.core.runtime.IProgressMonitor;

import com.ibm.team.process.common.IDevelopmentLine;
import com.ibm.team.process.common.IDevelopmentLineHandle;
import com.ibm.team.process.common.IIteration;
import com.ibm.team.process.common.IIterationHandle;
import com.ibm.team.process.common.IProjectArea;
import com.ibm.team.process.common.IProjectAreaHandle;
import com.ibm.team.repository.client.IItemManager;
import com.ibm.team.repository.client.ITeamRepository;
import com.ibm.team.repository.common.TeamRepositoryException;
import com.ibm.team.workitem.client.IAuditableClient;
import com.ibm.team.workitem.common.model.ItemProfile;

/**
 * Tries to find a development line and enclosed iteration for a project area. 
 * 
 * @author rschoon
 *
 */
public class DevelopmentLineHelper {

	private ITeamRepository fTeamRepository;
	private IProgressMonitor fMonitor;
	private IAuditableClient fAuditableClient;

	public DevelopmentLineHelper(ITeamRepository teamRepository, IProgressMonitor monitor) {
		fTeamRepository = teamRepository;
		fMonitor = monitor;
	}

	/**
	 * Find a development line based on the path provided.
	 * 
	 * @param projectArea
	 * @param path
	 * @param byId search by id or name
	 * @return a development line found or null.
	 * @throws TeamRepositoryException
	 */
	public IDevelopmentLine findDevelopmentLine(IProjectArea projectArea,
			List path, boolean byId) throws TeamRepositoryException {
		int level = 0;
		String lookFor = path.get(level);
		IDevelopmentLineHandle[] developmentLineHandles = projectArea
				.getDevelopmentLines();

		for (IDevelopmentLineHandle developmentLineHandle : developmentLineHandles) {
			IDevelopmentLine developmentLine = fAuditableClient
					.resolveAuditable(developmentLineHandle,
							ItemProfile.DEVELOPMENT_LINE_DEFAULT, fMonitor);
			String compare = "";
			if (byId) {
				compare = developmentLine.getId();
			} else {
				compare = developmentLine.getName();
			}
			if (lookFor.equals(compare)) {
				if (path.size() > level + 1) {
					IIteration found = findIteration(iteration.getChildren(),
						path, level + 1, byId);
					if (found != null) {
						return found;
					}
				} else {
					return iteration;
				}
			}
		}
		return null;
	}
}

Now we have the development line, we can find the iteration with the following code:

/**
 * Find an iteration based on the path provided.
 * 
 * @param iProjectAreaHandle
 * @param path
 * @param byId
 * @return an iteration if one can be found or null otherwise
 * 
 * @throws TeamRepositoryException
 */
public IIteration findIteration(IProjectAreaHandle iProjectAreaHandle,
		List path, boolean byId) throws TeamRepositoryException {
	fAuditableClient = (IAuditableClient) fTeamRepository
			.getClientLibrary(IAuditableClient.class);
	IIteration foundIteration = null;
	IProjectArea projectArea = (IProjectArea) fTeamRepository.itemManager()
			.fetchCompleteItem(iProjectAreaHandle, IItemManager.REFRESH,
					fMonitor);
	IDevelopmentLine developmentLine = findDevelopmentLine(projectArea,
			path, byId);
	if (developmentLine != null) {
		foundIteration = findIteration(developmentLine.getIterations(),
				path, 1, byId);
	}
	return foundIteration;
}

The code uses the following method inside, to find the iteration.

/**
 * Find an Iteration
 * 
 * @param iterations
 * @param path
 * @param level
 * @param byId
 * @return
 * @throws TeamRepositoryException
 */
private IIteration findIteration(IIterationHandle[] iterations,
		List path, int level,  boolean byId)
		throws TeamRepositoryException {
	String lookFor = path.get(level);
	for (IIterationHandle iIterationHandle : iterations) {

		IIteration iteration = fAuditableClient.resolveAuditable(
				iIterationHandle, ItemProfile.ITERATION_DEFAULT, fMonitor);
		String compare = "";
		if (byId) {
			compare = iteration.getId();
		} else {
			compare = iteration.getName();
		}
		if (lookFor.equals(compare)) {
			if (path.size() > level + 1) {
				IIteration found = findIteration(iteration.getChildren(),
						path, level + 1, byId);
				if (found != null) {
					return found;
				}
			} else {
				return iteration;
			}
		}
	}
	return null;
}

The DevelopmentLineHelper class can be used like below.

IIteration found = devLineUtil.findIteration(workItem.getProjectArea(), ph.getIdPath(), true);
if (found == null) {
	found = devLineUtil.findIteration(workItem.getProjectArea(), ph.getNamePath(), false);
}
if (found != null) {
	log("Found:" + found.getLabel());
	if(found.hasDeliverable()){
		workItem.setTarget(found);
	}
}

This code uses the DevelopmentLineHelper to try to match the path for the iteration based on ID first. If you are working in the same project area this should work. If not, you might want to check if the path by name matches.

If the iteration can be located, and is marked as a deliverable, you can set it as Planned for the work item.

In case you only have a string representation you can use the following code to use the DevelopmentLineHelper the code can be downloaded from here.

String iterationPath = "DevLine/Release 1/Iteration 1";
List path = Arrays.asList(iterationPath.split("/"));
// map by ID
IIteration found = devLineUtil.findIteration(projectArea, path , true);
if (found == null) {
	IIteration found = devLineUtil.findIteration(projectArea, path , false);
}

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.

The RTC WorkItem Server Link API – Linking to Work Items and Other Elements


Yesterday, I blogged about the RTC Client Link API. I excluded the server API, mainly because I realized I was not able to access the references from a WorkItemWorkingCopy on the server. The server API is simply different. I missed looking into an example that I had created some years ago, where I had already solved the issue. So here is the “missing link”.

*Update* See the post Creating CLM Links With Back Link Between Work Items for some new information on CLM links between work items.
*Update* Creating the Links using the ILinkServiceLibrary does not trigger operational behavior.
*Update* I took a deeper look at what to do with URI references on the server.
*Update* Use the new method instead of ILinkServiceLibrary to create links that can be governed by operational behavior.
*Update* Please find some of the code in the download at the post Resolve Parent If All Children Are Resolved Participant.

  • Update * This forum post shows the code that links a work item with a RTC SCM change set.

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.

Accessing References on the Server

I found two ways to access references for work items on the server. The post RTC Update Parent Duration Estimation and Effort Participant provides an example that shows how to use the information about existing and new references provided by the ISaveParameter operation. For example using this code:

IWorkItemReferences newRreferences = saveParameter.getNewReferences();

The code provides you with the references for the state of the work item. If you want to access the references for the old work item, or if you just have a work item (for instance a parent) and want the references for that, you have to take a different approach as presented below.

IWorkItemReferences oldReferences=null;
IAuditable oldState = saveParameter.getOldState();
if(oldState!=null){
	IAuditableCommon common = (IAuditableCommon)getService(IAuditableCommon.class);
	IWorkItem oldItem = (IWorkItem)common.resolveAuditable(oldState, IWorkItem.FULL_PROFILE, monitor);
	IWorkItemServer workItemServer = getService(IWorkItemServer.class);
	oldReferences = workItemServer.resolveWorkItemReferences(oldItem, monitor);
}

The code gets the IAuditable from the old state. If the old state is not null (there was a state), the code resolves the IAauditable to the IWorkItem. then it uses the IWorkItemServer service to get the references. Working with the references is identical to the client, except Creating a reference.

Handling CALM or URI References on the server

*Update* I took a deeper look at what to do with URI references.

The code below is the server API replacement for analyzeReferenceTarget() in The RTC WorkItem Client Link API – Linking to Work Items and Other Elements. All other methods to analyze the references found in the post can be used on the server too. The difference on the server is that the client library used in analyzeReferenceTarget() in the post The RTC WorkItem Client Link API – Linking to Work Items and Other Elements needs to be replaced by a service.

The code below tries to find the other end of an URI reference. Those references are used for CALM links such as “Tracks”. In case the other end is a work item it uses the location information to get the work item.

/**
* Further analyze an item referenced by an URI
* @param iReference
*/
public void analyzeReferenceTarget(IReference iReference) {
	URI uri = iReference.createURI();
	try {
		System.out.println(" Resolving URI: " + uri.toString());
		// get the location from the URI
		Location location = Location.location(uri);
		// resolve the item by location
		IAuditableCommon common = getService(IAuditableCommon.class);
		IAuditable referenced = common.resolveAuditableByLocation(
			location, ItemProfile.createFullProfile(
			location.getItemType()), null);
		// look for a referenced work item
		if (referenced instanceof IWorkItem) {
			IWorkItem referencedWI = (IWorkItem) referenced;
			System.out.println(" Resolved URI (resolve): "
				+ uri.toString() + " to: " + referencedWI.getId()
				+ " " + referencedWI.getState2().toString());
		}
	} catch (TeamRepositoryException e) {
	e.printStackTrace();
}
System.out.println(" Resolved URI: " + uri.toString());
}

Creating References on the Server

*Update* Creating the Links using the ILinkManager does not trigger operational behavior.Use creating IReferences instead and save them with IWorkItemServer.saveWorkItem3(). I also ran into a problem with CLM links such as Tracking work item links. I will post a solution as soon as I have one.

This was the hardest part really.

*UPDATE*

I had to write some code for a customer and I needed to create CLM links like Tracks. I used the code I had – the one published in the second next block. It did not work as expected. The issue was, the links where created, but I could not delete them anymore. I had to investigate, talked to one of our developers and we finally came up with the new code below.

/**
 * Link the trigger and the target work item
 * @param sourceWorkItem     the work item to link to
 * @param targetWorkItem     the work item to link to
 * @param monitor
 * @return
 */
private linkWorkItemsCLM(IWorkItem sourceWorkItem, IWorkItem targetWorkItem, IProgressMonitor monitor) {
	fWorkItemServer = getService(IWorkItemServer.class);
	// Get the references for the source work item, we need it to add new links
	IWorkItemReferences targetReferences = fWorkItemServer
				.resolveWorkItemReferences(resolvedTriggerItem, monitor);
	IEndPointDescriptor tracksEndpoint = ILinkTypeRegistry.INSTANCE
			.getLinkType(WorkItemLinkTypes.TRACKS_WORK_ITEM)
			.getTargetEndPointDescriptor();
	Location location = Location.itemLocation(targetWorkItem,
			this.getPublicRepositoryURL());
	IReference targetEndpoint = IReferenceFactory.INSTANCE
			.createReferenceFromURI(location.toAbsoluteUri());
	// The target work item is new, no need to check for duplicates
	targetReferences.add(tracksEndpoint, targetEndpoint);

	// Save the sourceWorkItemwork item with the links we created
	IStatus saveStatus = fWorkItemServer.saveWorkItem3(sourceWorkItem,
			targetReferences, null, null);
}

The code above is able to successfully create CLM and other links from work items to other items, including work items. The code above is different from the code below I came up with first.
It uses the work item server to save the work item, instead of using the ILinkService to create and save the link. That way, operational behavior is called for the save operation and operational behavior can govern the creation of these links. The code below shows how to call the code.

// Load the new trigger work item so that we can modify it and don't get
// stale data
fWorkItemServer = getService(IWorkItemServer.class);
ItemProfile loadProfile = IWorkItem.SMALL_PROFILE
	.createExtension(Arrays.asList(new String[] { IWorkItem.TARGET_PROPERTY }));
IWorkItem sourceWorkItem= (IWorkItem) fWorkItemServer
	.getAuditableCommon()
	.resolveAuditable(workItem, loadProfile, monitor)
	.getWorkingCopy();
linkWorkItemsCLM(IWorkItem sourceWorkItem, IWorkItem targetWorkItem, IProgressMonitor monitor)

In the above code a Location was used to create the target endpoint. This location needs the public repository URI, otherwise the work item won’t be linked properly. The code above works across repositories, if you provide the right public repository URI for the target work item. In the code above the assumption is that both work items are in the same repository.

If you want to link work items with work item references like parent-child references in the same repository, you can use the code below. It avoids having to create the reference from an URI.

IEndPointDescriptor tracksEndpoint = ILinkTypeRegistry.INSTANCE
	.getLinkType(WorkItemLinkTypes.PARENT_WORK_ITEM)
	.getTargetEndPointDescriptor();
IReference targetEndpoint = IReferenceFactory.INSTANCE
	.createReferenceToItem(targetWorkItem);

Note: Don’t use this method, as it avoids triggering operational behavior on save.

In the past, when I dug into the SDK and found bits and pieces. I dug around on Jazz.net and finally found this answer on Jazz.net which I shamelessly used to build my example code. However that was not enough and I had to refine it to get it working properly.

// Get the reference factory to create references
IReferenceFactory refFactory = IReferenceFactory.INSTANCE;
IReference source = refFactory.createReferenceToItem(parent);
IReference target = refFactory.createReferenceToItem(blocked);
// get the link service to have access to the ILinkServiceLibrary
ILinkService linkService = getService(ILinkService.class);
// get the ILinkServiceLibary
ILinkServiceLibrary linkServiceLibrary = (ILinkServiceLibrary) linkService
	.getServiceLibrary(ILinkServiceLibrary.class);
// Create the link
ILink link = linkServiceLibrary.createLink(
WorkItemLinkTypes.BLOCKS_WORK_ITEM, source, target);
// Save the link
linkServiceLibrary.saveLink(link);

Note, please don’t forget to declare the services you use in server extensions as requiredService in the plugin XML as described here.

The RTC WorkItem Client Link API – Linking to Work Items and Other Elements


It is sometimes interesting to follow links in work items. I have seen and answered several related questions at the jazz.net Forum. This post is supposed to summarize what I have found about linking work items using the Plain Java Client Libraries so far. I will focus on the Client Library in this post because I realized that there are some important differences between the client and the server API with respect to accessing the work item’s references. I will try to address the differences on the server side in a later post. For now RTC Update Parent Duration Estimation and Effort Participant provides an example that shows how to follow work item to work item parent-child links on the server in an Advisor or in a Participant. In this case the information about existing and new references can be retrieved using the ISaveParameter.

*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, including the creation of all kinds of links. 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* See the post Creating CLM Links With Back Link Between Work Items for some new information on CLM links betwen work items.

*Update* I figured the server side API and you can find the information in this post.

*Update* I took a deeper look at what to do with URI references.

*Update* See this link for code to get a work item handle from an URI

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.

Creating References Using WorkItemEndPoints

The following code shows the code used to create a reference to another work item. It is based on the WorkitemOperation code used in Uploading Attachments to Work Items. It can however be used also with any kind of WorkItemWorkingCopy saved with the WorkingCopyManager too.

The code shows the most basic way to create a reference in RTC using the client API.

/**
 * Inner class to do the modification
 *
 */
private static class WorkItemReferencesModification extends WorkItemOperation {
	private IWorkItemHandle fOpposite;

	public WorkItemReferencesModification(IWorkItemHandle opposite) {
		super("Modifying Work Item References",IWorkItem.FULL_PROFILE);
		fOpposite = opposite;
	}

	@Override
	protected void execute(WorkItemWorkingCopy workingCopy, IProgressMonitor monitor) throws TeamRepositoryException {
		// Create a new reference to the opposite item
		IItemReference reference = IReferenceFactory.INSTANCE.createReferenceToItem(fOpposite);
		// Add the new reference using a specific work item end point
		workingCopy.getReferences().add(WorkItemEndPoints.BLOCKS_WORK_ITEM, reference);
	}
}

This is the easiest way to create a reference from a work item to another work item. The code creates a new reference to a work item handle. In the second step it uses endpoints predefined in com.ibm.team.workitem.common.model.WorkItemEndPoints to create the reference. The endpoints of a reference, sort of, define the type of the reference. A reference is some kind of link between two objects. From the perspective of the objects the relationship might be different. For example a work item could have a parent from its perspective. The parent from its perspective would have a child. The relationship between two work items is defined using the endpoints from com.ibm.team.workitem.common.model.WorkItemEndPoints. Both ends of the link have their own endpoint. By defining the opposite endpoint, the other endpoint is also determined.

When looking at the endpoints defined in WorkItemEndPoints it appears that the available endpoints are only for work item to work item links. Taking a closer look would reveal that even not all CLM work item to work item link types are available. In general there is no endpoint for any URL based relationship. You can only use this code if your link is based on these available WorkItemEndPoints.

Creating References Using WorkItemLinkTypes

If it is necessary to create other types of links for example to some URL this mechanism does not work. The code below shows an alternative approach that creates the endpoint using a more fundamental mechanism provided by using the com.ibm.team.links.common.registry.ILinkTypeRegistry.

/**
 * Inner class to do the modification
 *
 */
private static class WorkItemReferencesModification extends WorkItemOperation {

	private IWorkItemHandle fOpposite;

	public WorkItemReferencesModification(IWorkItemHandle opposite) {
		super("Modifying Work Item References",IWorkItem.FULL_PROFILE);
		fOpposite = opposite;
	}

	@Override
	protected void execute(WorkItemWorkingCopy workingCopy, IProgressMonitor monitor) throws TeamRepositoryException {
		// Create a new reference to the opposite item
		IItemReference reference = IReferenceFactory.INSTANCE.createReferenceToItem(fOpposite);
		// Create a new end point
		IEndPointDescriptor endpoint = ILinkTypeRegistry.INSTANCE.getLinkType(WorkItemLinkTypes.BLOCKS_WORK_ITEM).getTargetEndPointDescriptor();
		// Add the new reference using a specific work item end point
		workingCopy.getReferences().add(endpoint, reference);
	}
}

The code uses the ILinkTypeRegistry and the WorkItemLinkTypes to create an endpoint and then creates the reference. This code is more flexible and allows more link types to be created. This includes links to elements using a URL such as OSLC links to elements in other OSLC providers. The code below would create a tested by test case link for a work item.

reference = IReferenceFactory.INSTANCE.createReferenceFromURI(new URI("https://clm.example.com:9447/qm/oslc_qm/contexts/_Lm2UIACBEeGZqMjM3RLKTw/resources/com.ibm.rqm.planning.VersionedTestCase/_dJzNgQCBEeGZqMjM3RLKTw"));
workingCopy.getReferences().add(ILinkTypeRegistry.INSTANCE
	.getLinkType(WorkItemLinkTypes.TESTED_BY_TEST_CASE).getTargetEndPointDescriptor(),reference);

Accessing References

I am aware of two ways to access the references of a work item using the Client Libraries. If you have a WorkItemWorkingCopy you can access the references using this code:

// get all references from the work item workingcopy
IWorkItemReferences references = workingCopy.getReferences();

It is easy to get a working copy from a work item that already is derived from a working copy using

WorkItemWorkingCopy workingCopy = (WorkItemWorkingCopy) workItem.getWorkingCopy()

Another way to access the references, if you have a plain work item, is using the IWorkItemCommon client library. Note, in this case the ITeamRepository is required which can be retrieved using the method .getOrigin(). The IWorkItemClient client library provides this method as well, in case it is already available.

IWorkItemCommon common= (IWorkItemCommon) ((ITeamRepository)workItem.getOrigin()).getClientLibrary(IWorkItemCommon.class);
IWorkItemReferences references = common.resolveWorkItemReferences(workItem, null);

Once we have an IWorkItemReferences object we can analyze the references.

/**
 * Analyze the references of a workitem
 */
private void analyzeReferences(IWorkItemReferences references) {
	List endpoints = references.getTypes();
	for (IEndPointDescriptor iEndPointDescriptor : endpoints) {
		System.out.println("Endpoint: "
			+ iEndPointDescriptor.getDisplayName() + " ID: "
			+ iEndPointDescriptor.getLinkType().getLinkTypeId());
		List typedReferences = references.getReferences(iEndPointDescriptor);
		for (IReference iReference : typedReferences) {
			analyzeReference(iReference);
		}
	}
}

The IWorkItemReferences provides all available link types for the contained references. The code above gets the list and iterates it to look at the references for each type. The code prints some information. The next step is to get all the references for a given endpoint and analyze each reference.

/**
 * Analyze a reference
 */
public void analyzeReference(IReference iReference) {
	if (iReference.isItemReference()) {
		Object resolvedRef = iReference.resolve();
		analyzeItem(resolvedRef);
	}
	if (iReference.isURIReference()){
		analyzeReferenceTarget(iReference);
	}
}

The code above checks each reference if it is a reference to an IItem for example an IWorkItem or an URI reference. For the item reference there is more to analyze so the code resolves it to get the object an passes it on. For an URI reference the code gets the URI and prints it.

The resolved object can now be analyzed in the code below. A cast is used to get to the contained element such as an IWorkItemHandle or a BuildResultHandle. Once the handle is available it is possible to use the ITeamreposiory.itemManager() to get the item from the handle and manipulate it.

/**
 * Analyze an Item
 */
private void analyzeItem(Object resolvedRef) {
	System.out.println(" Resolved item: "
		+ resolvedRef.toString());
	if(resolvedRef instanceof IWorkItemHandle){
		IWorkItemHandle handle = (IWorkItemHandle)resolvedRef;
	}
	if(resolvedRef instanceof BuildResultHandle){
		BuildResultHandle handle = (BuildResultHandle)resolvedRef;
	}
}

*Update* I looked deeper in what the client API provides in terms of accessing the elements referenced by the URI. Here is what I came up with. Please be aware that I am unsure if this works for all cases. It seems to be possible to resolve the element for example if it is in the same repository. If it is a work item you can then use the typical interfaces to access its data.

/**
 * Further analyze an item referenced by an URI
 * @param iReference
 */
public void analyzeReferenceTarget(IReference iReference) {
	URI uri = iReference.createURI();
	try {
		System.out.println("   Resolving URI: " + uri.toString());
		ITeamRepository teamRepo = (ITeamRepository) iReference.getLink().getOrigin();
		IAuditableClient auditableClient = (IAuditableClient) teamRepo.getClientLibrary(IAuditableClient.class);

		// get the location from the URI
		Location location = Location.location(uri);
		// resolve the item by location
		IAuditable referenced = auditableClient.resolveAuditableByLocation(location,
				ItemProfile.createFullProfile(location.getItemType()), null);
		// look for a referenced work item
		if (referenced instanceof IWorkItem) {
			IWorkItem referencedWI = (IWorkItem) referenced;
			System.out.println("   Resolved URI (resolve): "
				+ uri.toString() + " to: " + referencedWI.getId()  
				+ " " + referencedWI.getState2().toString());
		}
	} catch (TeamRepositoryException e) {
		e.printStackTrace();
	}
	System.out.println("   Resolved URI: " + uri.toString());
}

This is pretty much the summary of how links work on the client. I hope the code is useful, and it is easy enough to enhance the code for other purposes. Please remember that there is few error handling at this point. You might want to enhance this.

Common API to create Link URI’s

If you want to provide a URI for elements such as work items, you can use the Location class to do so.

The following code creates two different URI’s for a work item that are used in different types of links.

IWorkItemCommon common = (IWorkItemCommon) teamRepository.getClientLibrary(IWorkItemCommon.class);

int id = new Integer(idString).intValue();
IWorkItem workItem = common.findWorkItemById(id,IWorkItem.SMALL_PROFILE, monitor);
if (workItem == null) {
	System.out.println("Work item: " + idString + " not found.");
	return false;
}

System.out.println("Work item: " + workItem.getId() + ".");
Location location = Location.namedLocation(workItem,((ITeamRepository) workItem.getOrigin()).publicUriRoot());
System.out.println("Named Location URI: " + location.toAbsoluteUri());
		
location = Location.itemLocation(workItem,((ITeamRepository) workItem.getOrigin()).publicUriRoot());
System.out.println("Item Location URI: " + location.toAbsoluteUri());

The named location looks something like

https://server:port/rtc/resource/itemName/com.ibm.team.workitem.WorkItem/45943

The item location looks something like

https://server:port/rtc/resource/itemOid/com.ibm.team.workitem.WorkItem/_cdH5kJ0REean7cO1UYIcNw

Different link types use the different locations for their endpoints. The API is common api and available in the client as well as the server.

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.