Managing Contributor Licenses using the Java API

The question how to manage licenses using the plain java client libraries came up recently. There used to be a blog post in the internet that explained it, but that has been taken down. Questions on Jazz.net are not very clear for this, so I dug into the API myself. Here is the result.

License

The post contains published code, so 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. I found a section relevant to source code at the and of the license. Please also remember, as stated in the disclaimer, that this code comes with the usual lack of promise or guarantee. Enjoy!

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.

Solution

The service to access and assign licenses is the class com.ibm.team.repository.common.ILicenseAdminService.

Note that this class is not available as a client library such as other common and client services. The way to get this class in a client extension or a plain java API base application looks as follows:

ILicenseAdminService licenseAdminService = (ILicenseAdminService) ((IClientLibraryContext) teamRepository).getServiceInterface(ILicenseAdminService.class);

To access the class in a server extension like a pre-condition/advisor or a follow up action/participant is however using the typical com.ibm.team.repository.service.AbstractService.getService(Class) that is usually used e.g.:

ILicenseAdminService licenseService = getService(ILicenseAdminService.class);

The licensing code should work against each of the CLM applications JTS, CCM, QM, RM and should work for any available license.

This code can be used to print the license information for the available license types:

	IContributorLicenseType[] Licensetypes = licenseAdminService.getLicenseTypes();

	for (IContributorLicenseType iContributorLicenseType : Licensetypes) {
		System.out.println("License Type: ");
		System.out.println("License Type ID: " + iContributorLicenseType.getId());
		System.out.println("License Type Name: " + iContributorLicenseType.getName());
		System.out.println("License Type Description: " + iContributorLicenseType.getDescription());
		System.out.println("License Type ProductName: "	+ iContributorLicenseType.getProductName());
	}

The resulting list of licenses would look like below.

License Type: 
License Type ID: com.ibm.team.clm.stakeholder
License Type Name: Stakeholder
License Type Description: Diese Stakeholder-Lizenz ist zur Unterstützung von peripheren Benutzern gedacht, z. B. von externen Kunden, von Mitarbeitern der Benutzerunterstützung oder von Benutzern, die Arbeitselemente modifizieren und den Projektfortschritt beobachten müssen. Ein Benutzer mit zugewiesener Clientzugriffslizenz für Stakeholder hat Lese- und Schreibzugriff auf Change Management sowie Lesezugriff auf Berichte und Planungsfunktionen, sofern rollenbasierte Prozessberechtigungen keine diesbezüglichen Einschränkungen beinhalten. Floating-Lizenzen werden dynamisch von einem Lizenzserver zugewiesen. Ein Benutzer mit zugewiesener Floating-Lizenz kann in den Pool der Benutzer aufgenommen werden, die die verfügbaren, auf dem Lizenzserver installierten Floating-Lizenzen gemeinsam nutzen. Führt ein solcher Benutzer eine Operation aus, für die diese Lizenz erforderlich ist, wird ihm vom Lizenzserver dynamisch eine Floating-Clientzugriffslizenz zugewiesen, wenn eine solche verfügbar ist. 
License Type ProductName: Rational solution for Collaborative Lifecycle Management
License Type: 
License Type ID: com.ibm.team.clm.practitioner
License Type Name: Practitioner
License Type Description: Diese Lizenz 'CLM Practitioner' ist für Anwender bestimmt, die aktiv an Projekten der Rational-Lösung für Collaborative Lifecycle Management mitwirken. Ein Benutzer mit zugewiesener Clientzugriffslizenz 'CLM Practitioner' hat vollen Lese- und Schreibzugriff auf das Änderungsmanagement, auf die Anpassung von Berichten, auf die Planung, auf das Softwarekonfigurationsmanagement, auf die Automation (Buildsystem), auf bestimmte Erweiterungen für IBM Unternehmensplattformen, auf Funktionen für Anforderungsdefinition und -management sowie Qualitätsmanagement und Lesezugriff auf Funktionen des Designmanagements, sofern rollenbasierte Prozessberechtigungen keine diesbezüglichen Einschränkungen beinhalten.
License Type ProductName: Rational solution for Collaborative Lifecycle Management
License Type: 
License Type ID: com.ibm.team.rrc.reviewer
License Type Name: Contributor
License Type Description: Diese Mitarbeiterlizenz ist für professionelle Teammitglieder bestimmt, die keine Entwickler sind, aber aktiv am Projekt beteiligt sind. Ein Benutzer mit zugewiesener Clientzugriffslizenz für Mitarbeiter hat vollen Lese- und Schreibzugriff auf das Änderungsmanagement, auf die Anpassung von Berichten und auf die Planung. Diese Lizenz ermöglicht außerdem den Lesezugriff auf Funktionen des Anforderungsmanagements, des Softwarekonfigurationsmanagements, der Automation (Buildsystem), des Testmanagements und des Designmanagements, sofern rollenbasierte Prozessberechtigungen keine diesbezüglichen Einschränkungen beinhalten.
License Type ProductName: Rational DOORS Next Generation
License Type ID: com.ibm.team.rtc.contributor
License Type Name: Contributor
License Type Description: Diese Mitarbeiterlizenz ist für professionelle Teammitglieder bestimmt, die keine Entwickler sind, aber aktiv am Projekt beteiligt sind. Ein Benutzer mit zugewiesener Clientzugriffslizenz für Mitarbeiter hat vollen Lese- und Schreibzugriff auf das Änderungsmanagement, auf die Anpassung von Berichten und auf die Planung. Diese Lizenz ermöglicht außerdem den Lesezugriff auf das Softwarekonfigurationsmanagement, auf die Automation (Build-System), auf das Anforderungsmanagement, das Testmanagement und das Designmanagement, sofern rollenbasierte Prozessberechtigungen keine diesbezüglichen Einschränkungen beinhalten.
License Type ProductName: Rational Team Concert
License Type: 
License Type ID: com.ibm.team.rtc.stakeholder
License Type Name: Stakeholder
License Type Description: Diese Stakeholder-Lizenz ist zur Unterstützung von peripheren Benutzern gedacht, z. B. von externen Kunden, von Mitarbeitern der Benutzerunterstützung oder von Benutzern, die Arbeitselemente modifizieren und den Projektfortschritt beobachten müssen. Ein Benutzer mit zugewiesener Clientzugriffslizenz für Stakeholder hat Lese- und Schreibzugriff auf Change Management sowie Lesezugriff auf Berichte und Planungsfunktionen, sofern rollenbasierte Prozessberechtigungen keine diesbezüglichen Einschränkungen beinhalten.
License Type ProductName: Rational Team Concert
License Type: 
License Type ID: com.ibm.team.rtc.buildsystem
License Type Name: Build System
License Type Description: Die Clientzugriffslizenz 'Build System' kann nur einer Benutzer-ID zugewiesen werden, die von einem automatisierten Build-System verwendet wird. Damit haben Einheiten des Build-Systems Lesezugriff auf das gesamte Leistungsspektrum sowie Schreibzugriff auf alle Leistungsmerkmale, sofern rollenbasierte Prozessberechtigungen keine diesbezüglichen Einschränkungen beinhalten.
License Type ProductName: Rational Team Concert
License Type: 
License Type ID: com.ibm.team.rtc.developer
License Type Name: Developer
License Type Description: Diese Lizenz ist für professionelle Entwickler bestimmt, die aktiv am Projekt beteiligt sind. Ein Benutzer mit zugewiesener Clientzugriffslizenz für Entwickler hat vollen Lese- und Schreibzugriff auf das Änderungsmanagement, auf die Anpassung von Berichten, auf die Planung, auf das Softwarekonfigurationsmanagement und auf die Automation (Buildsystem). Diese Lizenz ermöglicht außerdem den Lesezugriff auf das Anforderungsmanagement, das Testmanagement und das Designmanagement, sofern rollenbasierte Prozessberechtigungen keine diesbezüglichen Einschränkungen beinhalten.
License Type ProductName: Rational Team Concert
License Type: 
License Type ID: com.ibm.team.clm.contributor
License Type Name: Contributor
License Type Description: Diese Mitarbeiterlizenz ist für professionelle Teammitglieder bestimmt, die keine Entwickler sind, aber aktiv am Projekt beteiligt sind. Ein Benutzer mit zugewiesener Clientzugriffslizenz für Mitarbeiter hat vollen Lese- und Schreibzugriff auf das Änderungsmanagement, auf die Anpassung von Berichten und auf die Planung. Diese Lizenz ermöglicht außerdem den Lesezugriff auf das Softwarekonfigurationsmanagement, auf die Automation (Buildsystem), auf das Anforderungsmanagement, das Testmanagement und das Designmanagement, sofern rollenbasierte Prozessberechtigungen keine diesbezüglichen Einschränkungen beinhalten. Floating-Lizenzen werden dynamisch von einem Lizenzserver zugewiesen. Ein Benutzer mit zugewiesener Floating-Lizenz kann in den Pool der Benutzer aufgenommen werden, die die verfügbaren, auf dem Lizenzserver installierten Floating-Lizenzen gemeinsam nutzen. Führt ein solcher Benutzer eine Operation aus, für die diese Lizenz erforderlich ist, wird ihm vom Lizenzserver dynamisch eine Floating-Clientzugriffslizenz zugewiesen, wenn eine solche verfügbar ist. 
License Type ProductName: Rational solution for Collaborative Lifecycle Management

The license type ID can be used to assign a license to the user.

	IContributor licenseContributor = teamRepository.contributorManager().fetchContributorByUserId(licenseUser, monitor);
	licenseAdminService.assignLicenseWithResult(licenseContributor,	licenseID);

For Example for user “bob” and the license type ID “com.ibm.team.rtc.developer”:

	IContributor licenseContributor = teamRepository.contributorManager().fetchContributorByUserId("bob", monitor);
	licenseAdminService.assignLicenseWithResult(licenseContributor,	"com.ibm.team.rtc.developer");

To remove a license use the code below and provide the license type ID

	IContributor licenseContributor = teamRepository.contributorManager().fetchContributorByUserId(licenseUser, monitor);
	licenseAdminService.assignLicenseWithResult(licenseContributor,	licenseID);

	licenseAdminService.unassignLicense(licenseContributor, licenseID);

Assigning licenses in a batch file

Using a plain java API application is not the only way to assign licenses. It is also possible to use the repo tools to do that. I use the following batch file to assign licenses to the users created for the JKE Banking sample life cycle project that I use to explore CLM.

echo on
set SERVERFOLDER="C:\CLM2016\6.0.3\JazzTeamServer\server"
set REPOSITORY="https://clm.example.com:9443/jts"
set USERID="myadmin"
set PASSWORD="myadmin"


rem primary users
call %SERVERFOLDER%\repotools-jts -createUser userId=bob licenseId=com.ibm.team.rrc.author repositoryURL=%REPOSITORY% adminUserId=%USERID% adminPassword=%PASSWORD%
call %SERVERFOLDER%\repotools-jts -createUser userId=marco licenseId=com.ibm.rqm.tester repositoryURL=%REPOSITORY% adminUserId=%USERID% adminPassword=%PASSWORD%
call %SERVERFOLDER%\repotools-jts -createUser userId=marco licenseId=com.ibm.team.rtc.developer repositoryURL=%REPOSITORY% adminUserId=%USERID% adminPassword=%PASSWORD%
call %SERVERFOLDER%\repotools-jts -createUser userId=deb licenseId=com.ibm.team.rtc.developer repositoryURL=%REPOSITORY% adminUserId=%USERID% adminPassword=%PASSWORD%
call %SERVERFOLDER%\repotools-jts -createUser userId=tanuj licenseId=com.ibm.rqm.tester repositoryURL=%REPOSITORY% adminUserId=%USERID% adminPassword=%PASSWORD%
call %SERVERFOLDER%\repotools-jts -createUser userId=rebecca licenseId=com.ibm.team.rtc.developer repositoryURL=%REPOSITORY% adminUserId=%USERID% adminPassword=%PASSWORD%

rem Build user
call %SERVERFOLDER%\repotools-jts -createUser userId=build licenseId=com.ibm.team.rtc.buildsystem repositoryURL=%REPOSITORY% adminUserId=%USERID% adminPassword=%PASSWORD%

rem secondary users
call %SERVERFOLDER%\repotools-jts -createUser userId=ursula licenseId=com.ibm.team.rrc.author repositoryURL=%REPOSITORY% adminUserId=%USERID% adminPassword=%PASSWORD%
call %SERVERFOLDER%\repotools-jts -createUser userId=curtis licenseId=com.ibm.rqm.viewer repositoryURL=%REPOSITORY% adminUserId=%USERID% adminPassword=%PASSWORD%
call %SERVERFOLDER%\repotools-jts -createUser userId=tammy licenseId=com.ibm.rqm.tester repositoryURL=%REPOSITORY% adminUserId=%USERID% adminPassword=%PASSWORD%
call %SERVERFOLDER%\repotools-jts -createUser userId=sally licenseId=com.ibm.team.rrc.author repositoryURL=%REPOSITORY% adminUserId=%USERID% adminPassword=%PASSWORD%
rem Design Manager License 
rem %SERVERFOLDER%\repotools-jts -createUser userId=al licenseId= repositoryURL=%REPOSITORY% adminUserId=%USERID% adminPassword=%PASSWORD%
pause

This works very well, except that it takes a long time to run since each repotools command has to individually log in.

Download and Compatibility

This code has been used with RTC 6.0 and is prepared to be used with RTC 6.0.x with no changes and it is pretty safe to assume, that the code will work with newer versions of RTC. It should however run with any version of RTC that has the specific API already implemented. The code shown below should work with almost all versions of RTC.

The post shows client, common API that are available in the RTC Server SDK and the RTC Plain Java Client Libraries.

You can download the Eclipse project with the application to print and assign a license to a user here.

You can download the batch file to assign the licenses for the JKE Banking Example in a zip archive here.

Related posts

Summary

The code above can be used to assign and remove licenses from users in RTC and CLM. An alternative method is using the repotools. as always I hope that this helps users out there with their tasks.

Query Models or how to find stuff with the RTC Java API

Although I have done my share using and blogging about the API there are still a lot of uncharted areas. How can I use the RTC API to find a user by the name? This is a question that came up recently in the forum and is one of many questions, I did not have a good answer until today.

While writing my last post, for whatever reason I started thinking about this question. I decided to have a quick peek and try to find out. As a result this blog post describes how questions like that can be approached with the RTC Java API.

The Problem

The API provided for objects such as contributors, build results and a lot more model elements used in the RTC application is not necessarily the API a human would expect. This is because the API is written to make it easy to develop the tool and not to make it easy for a human to access data. So a question “How do I find a user by the name” is not necessarily something the RTC API would be optimized for.

If a user logs into RTC, they provide the ID and not the name. After login the user ID is available in the API and that is the glue used for almost all internal computation. The user name is usually nothing of interest to the API. However there are cases, for example integration scenarios, where questions like this might be of interest. So how does RTC solve this under the covers?

Work Item Queries

Please don’t confuse the Query Models in this post with work item queries and work item expressions. To search for work items see

License

The post contains published code, so 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. I found a section relevant to source code at the and of the license. Please also remember, as stated in the disclaimer, that this code comes with the usual lack of promise or guarantee. Enjoy!

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.

Compatibility

This code has been used with RTC 6.0 and is prepared to be used with RTC 6.0.x with no changes and it is pretty safe to assume, that the code will work with newer versions of RTC. It should however run with any version of RTC that has the specific API already implemented. The code shown below should work with almost all versions of RTC.

The post shows client, common and server API that are available in the RTC Server SDK and the RTC Plain Java Client Libraries.

Download

You can download the Eclipse project with the examples.  Please note, there might be restrictions to access Dropbox and therefore the code in your company or download location.

Solution

RTC works against a database. Some domains such as work items and SCM provide higher level query mechanisms to support user configurable queries in the UI to find stuff. Other domains do not.  But under the cover, the RTC API provides query mechanisms to query the database for data and to manage result sets.

The RTC API provides a common query service and  query models to be able to define and run queries for a wide variety of RTC objects.  The query service and the query models are common API and can be used in Java client applications, Eclipse client extensions as well as in RTC Server extensions.

Although there are questions and examples in the forums and there are some examples like this wiki page and somewhat hidden in this article (section Querying for Items), there is no good description how this works on a broader level. Needless to say that there is no description how to find the entry points into the API either. This post tries to help as good as possible.

How to find the query models

The best way to approach this is to setup a RTC Development environment by following the getting started post. Thsi means follow Setting up Rational Team Concert for API Development and the Extensions workshop and perform at least lab 1. Now you have an Eclipse with a RTC SDK set up that provides you with searchable example code. Without this environment, it is pretty pointless to try to approach this API.

You can use the naming conventions used while developing RTC to search for the query models. In the Plug-in Development Perspective select the menu Search>Java. Type *QueryModel as search string. Select declarations and the other choices shown below, then click Search.

searchquerymodel

Be patient while the SDK is searched. Dependent on the version of RTC you should finally see a search view similar to the one below.

searchquerymodelresult

Note that more than 960 query modes are found. There might be some duplicates and some might be totally uninteresting but there is obviously an enormous potential to access data in RTC.

If you know the model element interface you are interested in, for example an IBuildResult,you can use these approaches to find the related query model.

You can try to use the Eclipse content assist capability of the Plugin Development Environment (PDE) to find the related query model. Type in the name of the model element without the leading I and append QueryModel to it. To find the query model for IBuildResult, type BuildResultQueryModel. While you type use Ctrl+Space for content assist. The PDE should find the query model class.

contentassistpde

You can also use the search approach from earlier to search for the specific class BuildResultQueryModel with or without using the asterisk.

specificsearch

That way the Eclipse client, if set up correctly, allows to find the query model for the model element interfaces you are interested in.

What is provided by the query models?

The query models allow to define queries, to find model elements related to the query model. The query model provides comparisons, Boolean operations on properties specific to the model elements, sort and filter operations. The queries can be constructed and called with parameters. The queries can then be run using a query service and return a result set that can be further processed.

Example 1: Find the build results related to a specific build definition that are tagged with a specific tag.

Example 2: Find a Contributor by user name.

The entry point of a query model is the ROOT entry of the query model. It allows to instantiate queries against the query model and defines the interfaces available for the specific query model. These interfaces also specify which properties an object has, how to access them and the Boolean and other operations available to operate on this model element.

The image below shows the query model root BuildResultQueryModel.ROOT. It returns an implementation class. The BuildResultQueryModel interface also extends interfaces BaseBuildResultQueryModel and ISingleItemQueryModel.

querymodelroot

The interface BaseBuildResultQueryModel defines which properties the model element IBuildResult exposes in queries.

buildresultquerymodel

ISingleItemQueryModel defines operators such as equals or contained in providing a IPredicate interface.

isingleitemquerymodel

The IPredicate interface provides the interfaces to create the Boolean operations and, or, not.

ipredicate

Given this pattern, it is possible to create complex expressions.

Using the query models

Using the Query mechanism typically works in the following steps.

Create a query for the QueryModel for the model element (example [ModelElementName]=BuildResult.

IItemQuery query = IItemQuery.FACTORY.newInstance([ModelElementName]QueryModel.ROOT);

Create a predicate to filter the results based on some properties. This specific example uses a parameter of type string that gets a value passed. Instead of the paramter, it would also be possible to hard code a string here.

IPredicate predicate = [ModelElementName]QueryModel.ROOT.property()._eq(query.newStringArg());

Use the predicate from the step before as filter for the query.

IItemQuery filtered = (IItemQuery) query.filter(predicate);

Finally use the query service to run the query. Here a parameter of type string is passed to the query. Result sets can be big, so the last parameter is used to pass how many results should be retrieved.

IItemQueryPage page = queryService.queryItems(filtered, new Object[] { "Jerry Jazz"}, 1 );

All this can be as compact as in the following example.

IItemQueryPage page = queryService.queryItems(IItemQuery.FACTORY.newInstance(IterationPlanRecordQueryModel.ROOT), IQueryService.EMPTY_PARAMETERS, IQueryService.DATA_QUERY_MAX_PAGE_SIZE);

Get contributor by user name

Lets look at how the code for this forum question.: “How can I get the contributor for a user if I have the user name and not the ID?”. The code is inspired by very similar code that is used for the RTC Jabber integration.

We are looking for an IContributor. So the looking for the query model seems to be ContributorQueryModel.

First the code creates the query for the ContributorQueryModel. Then it creates a predicate to filter out a contributor with a specified name. The predicate uses an argument for the user name instead of providing the user name already here as string. The predicate is set as filter.

This is plain java client library code. There is no direct access to the com.ibm.team.repository.common.service.IQueryService. To get the IQueryService the code uses a trick. the IQueryService is available from the Implementation Class for ITeamRepository.  The teamRepository object is casted to com.ibm.team.repository.client.internal.TeamRepository. This makes the usage of QueryModels unsupported due to using unsupported internal code.

The IQueryService is indirectly available in some client libraries as well.

ITeamBuildClient buildClient = (ITeamBuildClient) teamRepository.getClientLibrary(ITeamBuildClient.class);
IItemQueryPage queryPage = buildClient.queryItems(query, parameters, IQueryService.ITEM_QUERY_MAX_PAGE_SIZE, monitor);

Once the QueryService is available the query is executed. In the example the user name is passed as a new parameter to the query. The result expects no more than one result.

The paged result is retrieved as list of handles and further processed.

// Create a query for the ContributorQueryModel
final IItemQuery query = IItemQuery.FACTORY.newInstance(ContributorQueryModel.ROOT);
// Create a predicate with a parameter to search for name property  
final IPredicate predicate = ContributorQueryModel.ROOT.name()._eq(query.newStringArg());
// Use the predicate as query filter 
final IItemQuery filtered = (IItemQuery) query.filter(predicate);
// Get the query service. This is a cast to an internal class. Note TeamRepository and not ITeamRepository is casted.
final IQueryService qs = ((TeamRepository) teamRepository).getQueryService();
// Run this ItemQuery. Note, there are also other types of queries qs.queryData(dataQuery, parameters, pageSize)
final IItemQueryPage page = qs.queryItems(filtered, new Object[] { findUserByName }, 1 /* IQueryService.DATA_QUERY_MAX_PAGE_SIZE */);
// Get the item handles if any
final List<?> handles = page.getItemHandles();
System.out.println("Hits: " + handles.size());
if (!handles.isEmpty()) {
	System.out.println("Found user.");
	// Resolve and print the information to the contributor object.
	final IContributorHandle handle = (IContributorHandle) handles.get(0);

Please note that there are various operations available to create the predicate and that the predicate can actually more complex. The code above uses the method

_eq()

to check for an equal string. In this case the match has to be exact.

Another option would be to search with ignore case option. This can be done using the following predicate definition:

final IPredicate predicate = ContributorQueryModel.ROOT.name()._ignoreCaseLike(query.newStringArg());

The complete code can be found below and is available for download. The downloadable code contains the example to search for the user by exact (case sensitive) user name and an example to search for the contributor by e-mai with a case insensitive match.

If the QueryUserByName example class is called with the required parameters like.

"https://clm.example.com:9443/ccm" "myadmin" "myadmin" "John Doe"

the result looks like this, provided the user exists, of course.

executionresult

Differences in the Server API

Server extensions must extend com.ibm.team.repository.service.AbstractService. This allows to use com.ibm.team.repository.service.AbstractService.getService(Class) to get the IQueryService in a server Extension like this:

IQueryService queryService = this.getService(IQueryService.class);

The rest of the API is as described above.

Dynamic Query  Model

There is also a dynamic query model based on the IItemType. It can be created as shown below.

IDynamicItemQueryModel dynamicQueryModel = IBuildResult.ITEM_TYPE.getQueryModel();

From the documentation in the code:

Generally, static query models should be used whenever they are visible and the types/properties are known at compile time. Note also that there are no API contracts regarding dynamic APIs – model objects may change shape, queryable properties, etc.”

So use the static version as described above.

Interesting examples in the SDK

Search for “Owned By” is in com.ibm.team.repository.client.tests.query.ExistsPredicateQueryTests.testExistsPredicateUsingLinks()

The full example code

Please find below the complete code for the RTC Plain Java Client Library version to find a contributor by its user name.

/*******************************************************************************
 * Licensed Materials - Property of IBM
 * (c) Copyright IBM Corporation 2017. All Rights Reserved. 
 *
 * Note to U.S. Government Users Restricted Rights:  Use, duplication or 
 * disclosure restricted by GSA ADP Schedule Contract with IBM Corp.
 *******************************************************************************/
package com.ibm.team.workitem.ide.ui.example;

import java.util.List;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;

import com.ibm.team.repository.client.IItemManager;
import com.ibm.team.repository.client.ITeamRepository;
import com.ibm.team.repository.client.ITeamRepository.ILoginHandler;
import com.ibm.team.repository.client.ITeamRepository.ILoginHandler.ILoginInfo;
import com.ibm.team.repository.client.TeamPlatform;
import com.ibm.team.repository.client.internal.TeamRepository;
import com.ibm.team.repository.common.IContributor;
import com.ibm.team.repository.common.IContributorHandle;
import com.ibm.team.repository.common.TeamRepositoryException;
import com.ibm.team.repository.common.model.query.BaseContributorQueryModel.ContributorQueryModel;
import com.ibm.team.repository.common.query.IItemQuery;
import com.ibm.team.repository.common.query.IItemQueryPage;
import com.ibm.team.repository.common.query.ast.IPredicate;
import com.ibm.team.repository.common.service.IQueryService;

/**
 * Uses the ContributorQueryModel to search for a user by the user name 
 * and not the ID.
 * 
 * 
 * Example code, see
 * https://jazz.net/wiki/bin/view/Main/ProgrammaticWorkItemCreation.
 */
public class QueryUserByName {

	private static class LoginHandler implements ILoginHandler, ILoginInfo {

		private String fUserId;
		private String fPassword;

		private LoginHandler(String userId, String password) {
			fUserId = userId;
			fPassword = password;
		}

		public String getUserId() {
			return fUserId;
		}

		public String getPassword() {
			return fPassword;
		}

		public ILoginInfo challenge(ITeamRepository repository) {
			return this;
		}
	}

	public static void main(String[] args) {

		boolean result;
		TeamPlatform.startup();
		try {
			result = run(args);
		} catch (TeamRepositoryException x) {
			x.printStackTrace();
			result = false;
		} finally {
			TeamPlatform.shutdown();
		}

		if (!result)
			System.exit(1);
	}

	private static boolean run(String[] args) throws TeamRepositoryException {

		if (args.length != 4) {
			System.out
					.println("Usage: QueryContributorByName [repositoryURI] [userId] [password] [NameOfUserToSearch]");
			return false;
		}

		IProgressMonitor monitor = new NullProgressMonitor();
		final String repositoryURI = args[0];
		final String userId = args[1];
		final String password = args[2];
		final String findUserByName = args[3];
		ITeamRepository teamRepository = TeamPlatform
				.getTeamRepositoryService().getTeamRepository(repositoryURI);
		teamRepository.registerLoginHandler(new LoginHandler(userId, password));
		teamRepository.login(monitor);

		/***
		 * There is a wide variety of query models available for several domains that allow to query 
		 * the elements and filter the results.
		 *
		 * For some examples on the topic
		 * @see https://jazz.net/wiki/bin/view/Main/QueryDevGuide#ExampleOne
		 * @see https://jazz.net/library/article/1229
		 */

		// Create a query for the ContributorQueryModel
		final IItemQuery query = IItemQuery.FACTORY.newInstance(ContributorQueryModel.ROOT);
		// Create a predicate with a parameter to search for name property  
		final IPredicate predicate = ContributorQueryModel.ROOT.name()._eq(query.newStringArg());
		// Use the predicate as query filter 
		final IItemQuery filtered = (IItemQuery) query.filter(predicate);
		// Get the query service. This is a cast to an internal class. Note TeamRepository and not ITeamRepository is casted.
		final IQueryService qs = ((TeamRepository) teamRepository).getQueryService();
		// Run this ItemQuery. Note, there are also other types of queries qs.queryData(dataQuery, parameters, pageSize)
		final IItemQueryPage page = qs.queryItems(filtered, new Object[] { findUserByName }, 1 /* IQueryService.DATA_QUERY_MAX_PAGE_SIZE */);
		// Get the item handles if any
		final List<?> handles = page.getItemHandles();
		System.out.println("Hits: " + handles.size());
		if (!handles.isEmpty()) {
			System.out.println("Found user.");
			// Resolve and print the information to the contributor object.
			final IContributorHandle handle = (IContributorHandle) handles.get(0);
			IContributor foundContributor = (IContributor) teamRepository.itemManager().fetchCompleteItem(handle, IItemManager.DEFAULT, monitor);
			System.out.println("UUID: " + foundContributor.getItemId());
			System.out.println("ID: " + foundContributor.getUserId());
			System.out.println("Name: " + foundContributor.getName());
			System.out.println("E-Mail: " + foundContributor.getEmailAddress());
			System.out.println("Archived: " + foundContributor.isArchived());
		}			
		teamRepository.logout();
		return true;
	}
}

Additional Examples

Summary

As always I hope this helps someone out there with running RTC. Please keep in mind that this is as usual a collection of very basic examples with no or very limited testing and error handling. Please see the links in the getting started section for more examples, especially if you are just getting started with the RTC APIs.

DIY stream naming convention advisors

Organizations might be interested in enforcing naming conventions for streams or make sure that stream names must be unique. This post shows some simple example advisor that check for a naming convention and make sure the stream name is unique.

License

The post contains published code, so 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. I found a section relevant to source code at the and of the license. Please also remember, as stated in the disclaimer, that this code comes with the usual lack of promise or guarantee. Enjoy!

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.

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

Compatibility

This code has been used with RTC 6.0 and is prepared to be used with RTC 6.0.x with no changes and it is pretty safe to assume, that the code will work with newer versions of RTC. It should run with any version that provides the operation ID.

The code in this post uses common and server libraries/services that are available in the RTC Server SDK.

Download

The code can be downloaded from here. Please note, there might be restrictions to access Dropbox and therefore the code in your company or download location.

Solution

Since some versions of RTC the operation ID com.ibm.team.scm.server.modifyStream has been made available. The operation allows to write RTC advisors (pre-conditions) and participants (follow up actions) that trigger on saving of a stream. The data that is made available in these operations allows to detect a variety of change types including a rename  of a stream.

RTC (version 6.x) ships the following out of the box preconditions for this operation ID:

  1. Ensure that snapshot names are unique for streams in the process area.
  2. Prevent Adding Component to Stream When Component and Stream Owners are Different.
  3. Prevent Adding User Owned Component
  4. Restrict Stream Visibility to be set to Public

The implementing classes are shipped with the SDK

  1. com.ibm.team.scm.service.internal.process.advisors.UniqueBaselineSetNameAdvisor
  2. com.ibm.team.scm.service.internal.process.advisors.StreamAddComponentAdvisor
  3. com.ibm.team.scm.service.internal.process.advisors.StreamAddUserOwnedComponentAdvisor
  4. com.ibm.team.scm.service.internal.process.advisors.StreamVisibilityAdvisor

Looking at the source code can be a great inspiration.

This post shows two advisors

  1. StreamNamingPatternAdvisor
  2. UniqueStreamNameAdvisor

StreamNamingPatternAdvisor checks for a simple naming convention and prevents creation or renaming of streams that violate the naming convention.

UniqueStreamNameAdvisor checks if the name of the stream is already used by another stream and prevents saving the stream if that is the case. Please note, the operation com.ibm.team.scm.server.modifyStream does not trigger if someone creates or saves a user repository workspace. This means it is not possible to provide this capability for  them.

General Approach

The operation ID provides a special interface IModifyStreamOperationData which makes the type of change and the related data available.

imodifystreamoperationdata

The code for the advisors first checks if the operation data provided is of the type IModifyStreamOperationData.If not, the operation terminates. If so, it casts to be able to use the interface to access the change details. If the data indicates anything other than a IModifyStreamOperationData.CHANGES, the advisor terminates.

IModifyStreamOperationData opData = (IModifyStreamOperationData) operationData;
if (!opData.isOperationType(IModifyStreamOperationData.CHANGES)) {
	// Nothing to do
	return;
}

If the operation is for such a change, the operation data can contain several different changes. So the code gets all the change ID’s for the changes in a set. The code then iterates the change ID’s and gets the change usinf the ID as IStreamChange. This interface allows to get more information, for example the identifier for the operation. In our case, if it is a name change IModifyStreamOperationData.NAME, the advisors are responsible to deal with it.

	// Get the set of change IDs and look through the changes
	Set changeIDs = opData.getChangeIds();
	for (String changeID : changeIDs) {
		IStreamChange change = opData.getChange(changeID);
		// Is this a stream name change?
		if (change.getIdentifier().equals(IModifyStreamOperationData.NAME)){
		........

The interface IStreamChange does not provide a whole lot of data to find out what actually happened. But the identifier gives a good idea. The IStreamChange is implemented by several interfaces that provide more information.

istreamchange

  • com.ibm.team.scm.common.process.IStreamComponentChanges is an interface to describe component changes on the stream
  • com.ibm.team.scm.common.process.IStreamFlowChange is an interface to describe flow target related changes on the stream
  • com.ibm.team.scm.common.process.IStreamPropertyChange is an interface to describe property changes such as a name change on the stream

The code can now cast to the specific interface IStreamPropertyChange and access the current and the new value of the property change. These values are the current name and the new name of the stream.

With the new name the code can run a basic validation. If the validation succeeds, the advisor terminates, otherwise it complains and fails the advisor.

The code below shows the whole advisor implementation. In this example the code looks for a very simple prefix ‘TEST_’ on the stream name. If the prefix is available it succeeds, it fails otherwise. This validation can be replaced by a more complex and potentially configurable method.

/*******************************************************************************

 * Licensed Materials - Property of IBM
 * (c) Copyright IBM Corporation 2017. All Rights Reserved. 
 *
 * 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.scm.naming.advisor.service;

import java.util.Set;

import org.eclipse.core.runtime.IProgressMonitor;

import com.ibm.team.process.common.IProcessConfigurationElement;
import com.ibm.team.process.common.advice.AdvisableOperation;
import com.ibm.team.process.common.advice.IAdvisorInfo;
import com.ibm.team.process.common.advice.IAdvisorInfoCollector;
import com.ibm.team.process.common.advice.runtime.IOperationAdvisor;
import com.ibm.team.repository.common.TeamRepositoryException;
import com.ibm.team.scm.common.process.IModifyStreamOperationData;
import com.ibm.team.scm.common.process.IStreamChange;
import com.ibm.team.scm.common.process.IStreamPropertyChange;
import com.ibm.team.scm.service.internal.AbstractScmService;

@SuppressWarnings("restriction")
/**
 * This advisor tests the name of a stream against a naming convention pattern.
 * If the stream name does not match the pattern, the stream can not be saved.
 * 
 * Note that this extension point does not get any change events for repository workspaces. It only works for streams.
 * 
 * 
 * There are several examples for extensions that are shipped with the product that can be looked into in the SDK.
 * @see com.ibm.team.scm.service.internal.process.advisors.UniqueBaselineSetNameAdvisor
 * @see com.ibm.team.scm.service.internal.process.advisors.StreamVisibilityAdvisor
 * @see com.ibm.team.scm.service.internal.process.advisors.StreamAddComponentAdvisor
 * @see com.ibm.team.scm.service.internal.process.advisors.StreamAddUserOwnedComponentAdvisor
 *
 */
public class StreamNamingPatternAdvisor extends AbstractScmService implements
		IOperationAdvisor {

	public static final String REQUIRED_PREFIX = "TEST_";
	public static final String STREAM_NAMING_ADVISOR = "com.ibm.js.scm.naming.advisor.service.streamNaming";

	@Override
	public void run(AdvisableOperation operation,
			IProcessConfigurationElement advisorConfiguration,
			IAdvisorInfoCollector collector, IProgressMonitor monitor)
			throws TeamRepositoryException {
		Object operationData = operation.getOperationData();
		if (!(operationData instanceof IModifyStreamOperationData)) {
			// There are some stream modify operations that are not process
			// enabled so they do not provide any data.
			return;
		}
		// com.ibm.team.scm.server.modifyStream is only called for streams.
		// Saving a repository workspace (which is really like a stream) does
		// not trigger the extension point.
		// This especially implies that it is not possible to test for unique
		// names across streams and workspaces. It is possible to
		IModifyStreamOperationData opData = (IModifyStreamOperationData) operationData;
		if (!opData.isOperationType(IModifyStreamOperationData.CHANGES)) {
			// Nothing to do
			return;
		}

		// Get the set of change IDs and look through the changes
		Set changeIDs = opData.getChangeIds();
		for (String changeID : changeIDs) {
			IStreamChange change = opData.getChange(changeID);
			// Is this a stream name change?
			if (change.getIdentifier().equals(IModifyStreamOperationData.NAME)) {
				/***
				 * Get the change details based on the change we identified
				 * 
				 * The supported types of changes for this extension point are
				 * 
				 * @see com.ibm.team.scm.common.process.IStreamPropertyChange
				 * @see com.ibm.team.scm.common.process.IStreamComponentChanges
				 * @see com.ibm.team.scm.common.process.IStreamFlowChange
				 * 
				 *      A name change is stored as IStreamPropertyChange
				 */
				if (change instanceof IStreamPropertyChange) {
					IStreamPropertyChange streamPropertyChange = (IStreamPropertyChange) change;
					String newName = streamPropertyChange.getNewValue()
							.toString();
					if (validateName(newName)) {
						// We are fine
						return;
					}
					String description = "The stream name violates the naming conventions stream name must have prefix '"
							+ REQUIRED_PREFIX + "'!";
					String summary = description;
					IAdvisorInfo info = collector.createProblemInfo(summary,
							description, STREAM_NAMING_ADVISOR);//$NON-NLS-1$
					collector.addInfo(info);
				}
			}
		}
	}

	/**
	 * Validate the name against the convention
	 * 
	 * @param name
	 * @return
	 */
	private boolean validateName(String name) {
		// Implement your own naming convention here
		if (name.startsWith(REQUIRED_PREFIX)) {
			return true;
		}
		return false;
	}
}

The UniqueStreamNameAdvisor only replaces the validation with a more complex one querying the SCM system for streams with a given name.

The code creates workspace search criteria to search for a stream, that happens to have the same name as the new name for this stream will be. If so, it fails. The search is limited to streams, because it is not possible to do this check for repository workspaces and a conflict would be hard to handle.

/**
 * Validate the name against the convention a stream must have a unique name
 * 
 * @param name
 * @return
 * @throws TeamRepositoryException
 */
private boolean validateName(String name) throws TeamRepositoryException {
	// Get the query service and set criteria
	IScmQueryService queryService = getService(IScmQueryService.class);
	final IWorkspaceSearchCriteria criteria = IWorkspaceSearchCriteria.FACTORY
			.newInstance();
	criteria.setExactName(name); // Look for the same name
	// Only for streams, we can not prevent creation of workspaces with the
	// same name either and a user could create a workspace with the same
	// name and prevent us from saving the stream later.
	criteria.setKind(IWorkspaceSearchCriteria.STREAMS);
	// We only have to find one other
	ItemQueryResult result = queryService.findWorkspaces(criteria, 1, null);
	if (!result.getItemHandles().isEmpty()) {
		return false;
	}
	return true;
}

Consolidation

It is relatively obvious and straight forward to consider refactoring the solution shown here. Create an abstract class that contains all the shared code of both classes, with an abstract method validateName(). Create implementations providing the implementation for the method validateName() and provide these implementation classes in the plugin.xml. That way it is simple to provide new versions for various naming conventions.

Code Structure

The code structure follows the general approach that has been used in this blog for quite some time now. The code comes in the following Eclipse projects used for the purpose explained below.

com.ibm.js.scm.naming.advisor.common is the project that defines the jazz component to be used by this extension. It is in a separate project to allow to use it in case of implementing an aspect editor.

com.ibm.js.scm.naming.advisor.launches is a project that contains the launches that can be used to debug the extension with Jetty.

com.ibm.js.scm.naming.advisor.service is the project that contains the server side extension code.

com.ibm.js.scm.naming.advisor.service.feature defines is the feature for the server side deployment of the extension.

com.ibm.js.scm.naming.advisor.service.updatesite contains the update site to build the server side extension.

com.ibm.js.scm.naming.advisor.service.serverdeploy contains the provision profile as well as the folder structure needed for the final deployment. To prepare deployment Build the update site and copy the plugins and feature folder into the  js_scm_naming_advisor folder. To deploy copy the provision_profiles and the sites folder with all their contents into the server/conf/ccm folder of the RTC server.

codestructure

Configuring the precondition

When running the custom advisor in Jetty or finally deployed the advisor can be configured as usual.

configuring

If configured, the advisors prevent saving streams that do not conform to the specific naming condition. The advisor displays the problem like below.

fail_unique_name

Summary

As always I hope this helps someone out there with running RTC. Please keep in mind that this is as usual a very basic example with no or very limited testing and error handling. Please see the links in the getting started section for more examples, especially if you are just getting started.

The RTC SDK is about to change in 6.0.3

Since some time now, the RTC and Jazz Development teams are in discussion how to cope with the version compatibility requirements driven by Eclipse clients and the server API. In RTC 6.0.3 the SDK is about to be split into separate SDK’s for the Eclipse client and the Server. This will impact how the development environment needs to be set up and how extensions are developed. I will try to share a summary of what to expect here. I have so far only been able to experiment with development builds, there has not been an official release of the SDK for 6.0.3 yet.

Download the new workshop

Update * the new Extensions Workshop is finished for a while now. it can be found at the original Rational Team Concert Extensions Workshop location.

Why splitting the SDK?

The RTC Clients have been based on Eclipse 3.6 for a considerable amount of time now. This has been the case for the Jazz Servers as well. However, there is pressure on the server infrastructure for the need to support Eclipse 4.4.x and higher. On the other hand there are client applications that RTC needs to integrate with, that are lagging behind in adoption of new Eclipse versions.

As described in What API’s are Available for RTC and What Can You Extend? the RTC SDK currently contains the RTC Server API, the RTC Common API and the RTC Client API in one delivery. The RTC Common API is part of the RTC Server API as well as the RTC Client API. This is a potential problem when shipping the SDK and trying to keep the Server compatible to Eclipse 4.4.2 and above and being compatible with Eclipse 3.6 clients. As it looks, the RTC SDK will be split into two parts.

  1. A RTC Server SDK bundling the RTC Server API and the RTC Common API compatible with Eclipse 4.4.x and higher
  2. A RTC Client SDK bundling the RTC Client API and the RTC Common API compatible with Eclipse 3.6.x and higher

Impact of splitting the SDK

The split has various impacts on how extension development will now work. Please find below a short summary of changes that I have found necessary to perform the workshop.

Changes to section: 1.1 Download and Unzip the Required Files from jazz.net

The Server SDK Target Platform now requires an Eclipse 4.4.2 or higher. You can download the base Eclipse 4.4.2 client here. For example download the version Eclipse IDE for Java EE Developers. Install the client similar to described in the workshop. Then download the RTC Eclipse Client p2 install package and install this into your Eclipse 4.4.2.

In the Feature Based Launches download the new launcher442.zip. Unzip the zip file, browse to the enclosed JAR file and copy that into the dropins folder in the Eclipse client.

You might want to consider to do the following changes to the eclipse.ini file.

  • Add a section -vm with an additional line for the java virtual machine to use. If you run Eclipse with a different JVM, e.g. from Oracle, consider to specify a JRE or JDK that is compatible with the one that ships with RTC. This vm would also be used in the workspace setup section.
  • Add -showLocation in the org.eclipse.platform section; this shows the Eclipse workspace path in the upper border of the Eclipse client as below

    eclipse-workspace_2016-11-07_11-23-52

    This makes it possible to actually work with multiple workspaces and knowing which an Eclipse instance is responsible for.

  • A vmargs argument -Duser.language=en to make sure you get a consistent language in the menus if you want.

The image below shows the changes in my caseeclipse-ini-2016-11-07_11-09-02

Changes to section: 1.2 Setup for Development

Once the SDK is split into two parts the Rational Team Concert Extensions Workshop can no longer be performed using just one Eclipse Workspace. An SDK is set up as a Target Platform in the Plug-in Development section. Since the SDK’s are now split, it is necessary to have two target platforms. Since it is not possible to have more than one Target platform active in one Eclipse workspace it is not possible to launch a server for debugging while running an Eclipse client from the same workspace.

The RTC Extensions workshop will have to be changed to set up two separate workspaces.

  • One workspace will have to be set up with the RTC Server SDK as active Target Platform, for example using the path: C:\RTC603Dev\Workspaces\Dev1\Server
  • The other  workspace will have to be set up with the RTC Client SDK as active Target Platform, for example using the path: C:\RTC603Dev\Workspaces\Dev1\Client

Both workspaces will require to be set up as described in the RTC Extensions workshop document in section 1.2 Setup for Development.However, you will set up different target platforms in this step. Using the Server SDK for the server development workshop and the Client SDK for the Client development workspace.

Please note, it is a good idea to configure Eclipse to use an external browser as well in this step.

Changes to section: 1.3 Setup the RTC Tomcat Server

I am modifying the WorkshopSetup tool and data to setup the RTC project named RTC Extension Workshop to support an easier setup for the two workspaces. Basically two separate RTC Repository workspaces will be available. One will provide the launch, the configurations and the components needed to develop the RTC Eclipse server extension part of the workshop. The other one will provide the launch and the components needed to develop the RTC Eclipse client extension part of the workshop.

As long as this is not yet available it is possible to start with the existing setup tool and the related repository workspace and to load that into the two Eclipse workspaces. One workspace has to be set up with the RTC Client SDK will be used for development of the client part. The other with the RTC Server SDK set up is used to develop the server parts. When performing the workshop it will be necessary to work with the two workspaces and use one for all the server related tasks and the other one with the client related tasks. When Accepting changes into these workspaces it is necessary to understand what is part of the client and what is part of the server or what is shared. The image below shows what belongs to what.

  • The parts colored in blue are only related to server development
  • The parts colored in yellow are only related to client development
  • The uncolored parts are related to client and server development

client-server-common

Make sure to keep in mind which parts of the code are relevant for what. As an example, the project net.jazz.rtcext.workitem.extensions.ide.ui will not compile in the server development workspace. Similarly the net.jazz.rtcext.workitem.extensions.service project will not compile in the client development workspace.

Changes to section: 1.4 Complete Setup of Your RTC Eclipse Client

After Loading the repository workspace you have the choice to split the information into a sever part and a client part. For example you can duplicate RTC Extension Workshop Configuration and create one that only contains the client launches. Or you keep everything as it is and basically close the project areas you don’t need and ignore launches not needed. This is the easiest approach until a new Extension Workshop is available.

The initial step of copying the files services.xml and scr.xml is only needed in the server workspace. So when copying and importing, copy the files services.xml and scr.xml from your server’s ccm application in the installs\JazzTeamServer\server\conf\ccm folder into the RTC Extension Workshop Configuration project into the folder conf/jazz in the server development workspace.

When importing the plugins and features import the following into the server workspace:

  • com.ibm.team.common.tests.utils
  • com.ibm.team.jazz.foundation.server.licenses.enterprise-ea (or com.ibm.team.licensing.product.clm)
  • com.ibm.team.licensing.product.rtc-standalone

When importing the plugins and features import the following into the client workspace:

  • com.ibm.team.rtc.client.feature

Other considerations

As already mentioned, make sure to keep track which workspace you are working in and keep in mind that the server development part will not work in the client development workspace and vice versa.

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.

Summary

We have major changes coming up in the RTC Extension development area. The RTC Extension workshop needs to be adjusted and parts of the workshop lab needs to be reorganized and rewritten. This post explains what to consider for experienced users. Once there is an update to the Extension workshop lab material this post will be updated.

As always, I hope this helps users out there and saves them some time.

Using the Work Item Server API to create Work Items

How can one create a work item in a follow up action? How to link such a work item to the work item that was just saved? If you are interested in some more details using the work item server API continue reading.

These are questions that come up often in the Jazz Forum. There are several answers already in the forum, but I did never take the time to publish anything here.  Lets change that now.

License

The post contains published code, so 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. I found a section relevant to source code at the and of the license. Please also remember, as stated in the disclaimer, that this code comes with the usual lack of promise or guarantee. Enjoy!

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.

Which API’s are available in the server SDK and which should I use?

The Java API available in the server SDK is the common API and the server API. The common API usually is in packages with *.common.* in the namespace. The server API usually is in packages named with *.server.* in the namespace.The Interfaces names available in the RTC SDK also often have a postfix such as Common or Server or Service. As example the interface

com.ibm.team.workitem.common.IWorkItemCommon

is an important common API for work item manipulation.

Another example is the interface

com.ibm.team.workitem.service.IWorkItemServer

which is is an important server API for work item manipulation.

It is important to note, that the common API is also available in the client SDK. This is important, because the common API can then be used in client code as well as in server extensions. The client SDK and the Plain Java Client Libraries package the common API and also provide a client API. The client API usually is in packages named with *.client.* in the namespace. Like the in the pattern above the interface names often have a postfix Client in the name.

So, the interface

com.ibm.team.workitem.common.IWorkItemCommon

is available in the server SDK/API as well as in the client SDK/API and an important common API for work item manipulation.

The interface

com.ibm.team.workitem.client.IWorkItemClient

is only available in the client SDK and the Plain Java Client Libraries.

It is also important to note, that he common Interfaces are usually included in the Client and Server Interfaces. As an example the interface IWorkItemServer extends IWorkItemCommon

public interface IWorkItemServer extends IWorkItemCommon {

Similar in the client API with IWorkItemClient

public interface IWorkItemClient extends IWorkItemCommon {

This pattern repeats across the available APIs. The specific client and server interfaces like IWorkItemServer and IWorkItemClient just add a few very specific client side capabilities.

TIP – Try to use the Common API over the specific client or server APIs

If you can, you should use the common API and prefer it over the more specific ones. That way it is possible to use a lot of code in both contexts. Unfortunately I became aware of the importance of this too late and a lot of the example code on this blog uses the more specific client and server interfaces. So look at the examples and always check if there is a common interface you could use. Fall back to the client or server related API if the common API does not have what is needed.

Creating a work item in the Server

In the client API it is possible to use the class com.ibm.team.workitem.client.WorkItemOperation which deals with error handling. As we have learned in the paragraph before, this is client only API and not available in the server API.

For most of the operations needed to work with the data for a work item, the common interface IWorkItemCommon. Examples for methods likely needed are

  • IWorkItemCommon.findAttribute()
  • IWorkItemCommon.findWorkItemType()
  • IWorkItemCommon.resolveEnumeration()
  •  IWorkItemCommon.findCategories()
  •  IWorkItemCommon.findCategoriesOfProcessArea()

To create the work item on the server, you have to use IWorkItemServer.createWorkItem2(), then set the attributes and finally use IWorkItemServer.saveWorkItem3(), IWorkItemServer.saveWorkItem2() or IWorkItemServer.saveWorkItems() to save the work item(s). Which one you use depends on what needs to be saved.

Here some code that I used in an example follow-up action

/**
 * @param thisItem
 * @param workItemType
 * @param someAttribute
 * @param someLiteral
 * @param parsedConfig
 * @param monitor
 * @return
 * @throws TeamRepositoryException
 */
private IWorkItem createWorkItem(IWorkItem thisItem,
		IWorkItemType workItemType, IAttribute someAttribute,
		ILiteral someLiteral, ParsedConfig parsedConfig,
		IProgressMonitor monitor) throws TeamRepositoryException {

	IWorkItemServer fWorkItemServer = this.getService(IWorkItemServer.class);

	IWorkItem newWorkItem = fWorkItemServer.createWorkItem2(workItemType);

	XMLString targetSummary = thisItem.getHTMLSummary();
	XMLString newSummary = targetSummary.concat(addition);
	newWorkItem.setHTMLSummary(XMLString.createFromXMLText(newSummary
			.getXMLText()));
	ICategoryHandle category = thisItem.getCategory();
	if (category != null) {
		newWorkItem.setCategory(thisItem.getCategory());
	}

	IIterationHandle iteration = thisItem.getTarget();
	if (iteration != null) {
		newWorkItem.setTarget(iteration);
	}
	if (null != workItemType) {
		newWorkItem.setValue(someAttribute,someLiteral.getIdentifier2() );
	}

	Set additionalParams = new HashSet();
	additionalParams.add(ICreateTracedWorkItemsParticipant.CREATE_TRACED_WORKITEMS_PARTICIPANTS_ACTION_SAVENEW);

	fWorkItemServer.saveWorkItem3(newWorkItem, null, null, additionalParams);
	return newWorkItem;
}

Please note, that rules apply in the server  as well. Required attributes will be required and need to be provided to be able to save. The server operation runs in a context with specific privileges and it has only the permissions provide by the user context.

The next part of the code is for creation of tracks links and saving the links with a work item. The idea is t

/**
 * This method manages creating and linking the work items and saving the
 * new references.
 * 
 * @param thisWorkItem
 * @param monitor
 * @throws TeamRepositoryException
 */
private void createAndLinkPhaseWorkItems(IWorkItem thisWorkItem, IProgressMonitor monitor) throws TeamRepositoryException {

	IWorkItemServer fWorkItemServer = this.getService(IWorkItemServer.class);

	// Create the work items and the links and return the references
	List newItems = createWorkItems(thisWorkItem, monitor);
	IWorkItemReferences sourceReferences= fWorkItemServer.resolveWorkItemReferences(thisWorkItem, monitor);
	for (Iterator iterator = newItems.iterator(); iterator.hasNext();) {
		IWorkItem targetItem = (IWorkItem) iterator.next();
		ILinkType tracksLinkType= ILinkTypeRegistry.INSTANCE.getLinkType(WorkItemLinkTypes.TRACKS_WORK_ITEM);
		sourceReferences.add(tracksLinkType.getTargetEndPointDescriptor(), IReferenceFactory.INSTANCE.createReferenceFromURI(Location.namedLocation(targetItem, getPublicRepositoryURL()).toAbsoluteUri()));
	}
	Set additionalParams = new HashSet();
	additionalParams.add(ICreateTracedWorkItemsParticipant.CREATE_TRACED_WORKITEMS_PARTICIPANTS_ACTION_SAVEREFERENCES);

	// Save the work item we created new linked items for
	IStatus saveStatus = fWorkItemServer.saveWorkItem3(thisWorkItem,
			sourceReferences, null, additionalParams);
	// Handle the error
	if (!saveStatus.isOK()) {
	
	}

Summary

I wanted to post that for a long time, finally I was able to take the time. I hope this helps someone out there starting with extending and automating RTC.

Build On State Change Work Item Save Participant

In the unlikely event you have missed it or just to complete the hit list if you search for examples on this blog, the Build On State Change Work Item Save participant/follow up action is a complete example as part of the Rational Team Concert Extensions Workshop.

The Build On State Change Work Item Save participant monitors work item state changes of configured work item types and state changes. If a qualifying change happens, it issues a build request for a configured build definition. The example comes with a complete package including the configuration UI.

Just Starting with API work?

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.

Due Date Notifier – an Asynchronous Task Example

I always wanted to look into asynchronous tasks and also in creating events and e-mails on the server. Especially mail notification is a popular request on the forum. So here a working example.

Origin – Other Posts Around This Topic

This is not all my work, it is based on the QueryDev Guide in the the Jazz Developer Wiki and on Jorge Diaz’ blog post “RTC Custom Scheduled Async Task example explained”. I was looking into these sources, but I found the QueryDev Guide did not ship a complete example – it was the basic classes. Jorge ships the extension, but both examples don’t compile with the RTC 6.x versions of RTC and potentially earlier versions. As mentioned in the QueryDev Guide the feed category used there is no longer available and in both posts one of the API methods used is missing a parameter. This one of the few examples where I have actually seen minor changes in the API so far.

This gave me enough reasons to look into creating a working example again, to refine it and to try to better understand what the extension does and how.

Jorge has an additional post around asynchronous tasks. See Custom Services Configuration Properties – naming collision issues for additional hints.

License and Download

The post contains published code, so 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. I found a section relevant to source code at the and of the license. Please also remember, as stated in the disclaimer, that this code comes with the usual lack of promise or guarantee. Enjoy!

You can download the project source code from here.

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.

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

Solution Outline

The code implements an asynchronous task that is deployed on the server and uses a query to find work items that are overdue and not closed, in any project area. It creates a notification for these work items. The solution runs in two configurable modes.

  1. Mode “event” is the default. In this mode the asynchronous task creates a Message At Me event in the server feed using the IWorkItemFeedConstants.MESSAGES_AT_ME_FEED_CATEGORY. This event also triggers E-Mail notification for the owner. The Event is only created if there is no related MESSAGES_AT_ME_FEED_CATEGORY event that has been created in a configurable time span. This prevents from flooding the events and the user e-mail from duplicates.
  2. Mode “mail” can be configured. This mode sends a dedicated mail to the owner of the work item. This mode can not detect if it has recently sent a mail to the user. This means the asynchronous task should run with a delay that prevents it from running multiple times a day to avoid spamming the mail folders. It would also be worthwhile to look into a way to schedule the task only at a certain time of the day.

The reason why the second mode was added is two fold.

  1. I wanted to have an example how to send an e-mail with the server API.
  2. The MESSAGES_AT_ME_FEED_CATEGORY creates a mail that has a subject similar to

    “You were mentioned in work item ’52: This is a test work item’ [s]”.

    You can see the example message below. Althought he message body mentions its cause, this message is not really specific in its topic and I rather wanted something more specific.

Message Generated By Event

I tested several different event categories, including a custom one, but the results where even worse. For example, the message body did not even contain the event description “The work item was due 6 weeks 3 days 4 hours ago”.

The custom message sent in the second mode looks like this.

Custom Message

Since this is only an example, you might want to think about how to avoid sending multiple e-mails e.g. somehow storing information when a message was sent to whom and for which work item somewhere.

I tried if I could create an event, that would not trigger an e-mail to store the information, but that did not work for me.

Set-up and Use a Test Mail Server and Client

You probably don’t want to run your tests against the company mail server. To be able to test and debug the extension, it is better to install a test server. The same probably applies to the business mail client, so consider to install a test mail client.

There are various free mail servers and mail clients available out there. I used hMailServer as mail server on Windows for my testing. I used Mozilla Thunderbird as mail client for testing.

Set up the mail server and create user mail accounts to be used for testing. Then make sure to be able to to connect the mail client to the mail server and send mails back and forth. This provides all the information needed to set up RTC for mail notification.

To verify the required settings needed for the RTC/JTS server, enable e-mail notification on the development server (the one that is installed in the folder C:\RTCxxDev\installs\JazzTeamServer). Start the development server, if it is not already running.  Open the JTS setup page https://localhost:9443/jts/setup.  Proceed to the step Configure E-mail Notification. Enter the values needed to configure the development test server to enable mail notification. Test the connection. The page should look similar to the one below with different values. Note that you can use domain names or configure IP aliases in your hosts file to avoid using localhost as the server address.

Mail Notification SetupCreate test users with e-mail accounts matching your test accounts on the test mail server. Send test e-mails e.g. invitations for a user to make sure everything works. Note that there is a delay in the server, you would have to wait a while to receive the e-mail. Check the JTS server log for any errors with respect to mail sending if needed.

If this is working, open the JTS teamserver.properties file in C:\RTCxxDev\installs\JazzTeamServer\server\conf\jts to locate the values that were added. The values corresponding to the setup above are shown below.

E-mail settings in teamserver.properties FileThe entries should all start with the prefix com.ibm.team.repository.notification. The best option to locate the values is to search for that prefix, as the properties are not saved in an alphabetic order and will be spread across the file.

Copy the entries into some text editor, as the are needed to set up and enable e-mail notification in the Jetty server debug launch.

How to Test and Debug an Asynchronous Task

For debugging it is important to enable e-mail notification in the Jetty debug server. So it is necessary to provide the inforation above to enable the notification.

The Jetty launch files in the Rational Team Concert Extensions Workshop have a teamserver.property file located in the project RTC Extension Workshop Configuration in the subfolder jazz.

Jetty Configuration teamserver.properties file It would be possible to use this file to pass the special configuration parameters. But the file would be shared between all the launches that are configured this way.

As an alternative, it is possible to specify properties in the launch itself. This makes it possible to have various different launches that manage the different setups. Since a launch file can be stored in a project related to the extension itself, this is a way better option and also allows to share the launch in SCM and keep the RTC Extension Workshop Configuration on default values. This is the approach used here.

To configure e-mail notification and other properties, create your own Jetty launch, by copying and giving it a reasonable name. Then add the properties needed to the System Properties of the Jetty Launch.  The next image shows such settings. Use the common tab of the launch to change the save location and save it in one of your extension projects.

Mail Notification Setup in Jetty LaunchTo be able to test and debug an asynchronous task, you want to be able to run the task more frequently as in a typical deployment. To avoid having an unrealistic default setting for the frequency of the task in the production environment, it must be avoided to set the default to a high frequency. This can be solved by configuring the task frequency by adding a configuration property in the same place that was used above.

The special property to change the delay between running the asynchronous task is basically the property that the server would save in the teamserver.properties if the task default would be overwritten. The ID of the property depends on the ID of the Jazz component defined in the task and on the name of the property.

Asynchronous Task Configuration Override PropertyIn the case of this example the component ID is com.ibm.js.team.workitem.extension.duedatenotifier.component

The name of the property is task.remind.fixedDelay.

So the ID of the property is com.ibm.js.team.workitem.extension.duedatenotifier.component.task.remind.fixedDelay

In the image below, the value is set to 10 so that the tasks is run every 10 seconds.

Configuration Parameters in the Server Launch

As shown in the image above there are other parameters for the asynchronous task that can be overwritten for testing in a similar way.

The mail task also does not run very often and for debugging it would be convenient if it runs more frequently. This can be fixed similar to above. The parameter com.ibm.team.workitem.notification.mail.fixedDelay=15 at the end of the  list in the above image is used to set the mailer mail sending frequency to 15 seconds, to avoid to have to wait 10 minutes or whatever the default is until the messages are actually sent.

A launch with these settings is shipped as part of the code. You have to change the e-mail settings for your setup to make it work though.

Launch Included in ProjectChanges in the Code

The documentation in the QueryDev Guide in the the Jazz Developer Wiki is still valid for the code that can be downloaded from this post. There are some few changes that were done.

A new configuration property with name task.remind.notification.type was introduced as shown below.

Additional Configuration ParameterIt can have the following values

  • event (default) – Create notification events, user will get e-mail if configured
  • mail – Create a special e-mail and always send it to the user; run the task only once  day in this mode

The new capability to send a special e-mail was introduced based on Jorge Diaz’ blog post “RTC Custom Scheduled Async Task example explained”. To support sending e-mails additional prerequisites where added to the Extension as well.

There is some more documentation added in the code.

In the method runTask() a decision is taken to create events or to send a special e-mail, based on the configuration.

/*
 * (non-Javadoc)
 * 
 * @see
 * com.ibm.team.repository.service.async.AbstractAutoScheduledTask#runTask()
 */
@Override
protected void runTask() throws TeamRepositoryException {
	IProgressMonitor monitor = new NullProgressMonitor();

	// get the configuration for the notification
	getEventConfiguration();
	// Get the attributes and durations for the reminders
	List reminders = getReminders();
	// Get the project areas to be able to query the work items
	List<IProjectAreaHandle> projectAreas = findProjectAreas(monitor);
	// Get the recent notifications to be able to suppress additional
	// reminders
	Set<UUID> recentNotifications = findRecentNotifications(monitor);

	// iterate all the project areas
	for (IProjectAreaHandle projectArea : projectAreas) {
		// For each configured reminder
		for (Reminder reminder : reminders) {
			// get the work items that qualify
			List<IWorkItem> workItems = findWorkItems(projectArea,
					reminder, monitor);
			if (!workItems.isEmpty()) {
				// If there are work items, get the attribute for the
				// reminder
				IAttribute attribute = getWorkItemCommon().findAttribute(
						projectArea, reminder.attributeId, monitor);
				// For each work item found
				for (IWorkItem workItem : workItems) {
					if (enableMail) {
						createNotificationMail(workItem, attribute);
					} else {
						// If there is no recent notification
						if (!recentNotifications.contains(workItem
								.getItemId()))
							// Create one as a feed
							createNotificationEvent(workItem, attribute);
					}
				}
			}
		}
	}
}

The code that implements sending the e-mail in Jorge Diaz’ blog post “RTC Custom Scheduled Async Task example explained” is shown below. Jorge’s code was changed to be able to show the URL of the work item in the e-mail. It also does not send mail to the owner if there is no owner (the owner has the ID unassigned).

This might be language dependent. I did not find a great way to get the unassigned user. Please leave better ways to do this in a comment, if you come across one.

It would be possible to send the e-mail to some other account in case it has no owner. It would be possible to search for a user with a certain role such as ‘Project Manager’ and make sure that the task is not forgotten.

/**
 * Creates a custom notification mail to the owner.
 * 
 * @param workItem
 * @param attribute
 * @throws TeamRepositoryException
 */
private void createNotificationMail(IWorkItem workItem, IAttribute attribute)
		throws TeamRepositoryException {
	IContributorHandle ownerHandle = workItem.getOwner();
	if (ownerHandle != null) { // We need to have an owner
		IContributor owner = (IContributor) getRepositoryService().fetchItem(
				ownerHandle, IRepositoryItemService.COMPLETE);
		if(owner.getUserId().equalsIgnoreCase(UNASSIGNED_USER_ID)){
			// You might want to do something, e.g. annoy someone else.
			return;
		}
		String eMail = owner.getEmailAddress();
		sendRememberingMail(workItem, attribute, eMail);
	}
}

The below code creates the message.

/**
 * Create an e-mail and send it around.
 * 
 * @param workItem
 * @param attribute
 * @param mail
 * @throws TeamRepositoryException
 */
private void sendRememberingMail(IWorkItem workItem, IAttribute attribute,
		String mail) throws TeamRepositoryException {
	Location location = Location.namedLocation(workItem,
			this.getRequestRepositoryURL());
	String itemLocation = location.toString();
	long diff = getDueDifference(workItem, attribute);
	Duration duration = new Duration(Math.abs(diff) / HOUR * HOUR);

	String attributeSuffix = "";
	if (!attribute.getIdentifier().equals(IWorkItem.DUE_DATE_PROPERTY)) {
		attributeSuffix = " for work item attribute: "
				+ attribute.getDisplayName();
	}
	String mailSummary = "'" + workItem.getId() + ": "
			+ workItem.getHTMLSummary().getPlainText() + "'"
			+ attributeSuffix;

	String subject = NLS.bind("Due Date Reminder Notification - {0}",
			mailSummary);
	String message = NLS.bind("Work item {0}  was due {1} ago",
			itemLocation, fDurationFormat.format(duration));
	MailSender sender = getMailService().getDefaultSender();

	try {
		getMailService().sendMail(sender, mail, subject, message, null);
	} catch (MessagingException e) {
		String warningMessage = NLS
				.bind("Failed to send DueDate Notification message of work item {0} to {1}",
						workItem.getHTMLSummary().toString(), mail);
		getLog().warn(warningMessage, e);
	}

}

The interesting part is to create the URL for the work item. It uses the class Location to get the URI (URL) created. It is important to note that the example uses a named location. The related method namedLocation() is used to create the Location. There are different types of locations and the URIs that are created are slightly different dependent of the location type. In some contexts it is important to use the correct location. One example is in creating links between artifacts of different applications such as requirements and test artifacts. Using the wrong location type will cause problems. In this case converting the location to a string creates the link to open the work item we want to be available in the mail.

Location location = Location.namedLocation(workItem,this.getRequestRepositoryURL());
String itemLocation = location.toString();

Packaging

The structure of the projects that are used to ship is similar to all the recent examples posted on this blog.

  • com.ibm.js.team.workitem.extension.duedatenotifier.common: contains the extension for the component
  • com.ibm.js.team.workitem.extension.duedatenotifier.service: contains the extension that implements the service and refers back to the component
  • com.ibm.js.team.workitem.extension.duedatenotifier.server.feature: defines the feature, combining the component and the service
  • com.ibm.js.team.workitem.extension.duedatenotifier.server.feature.updateSite: is the update site project to build the extension
  • com.ibm.js.team.workitem.extension.duedatenotifier.server.feature.deploy: is the deploy project that defines the additional files and folders for deployment such as the provision profile and the site folder

Deploying the Extension

To build and deploy the task, open the site.xml in tha update site project and run Build All. Then copy the site.xml file and the folders features and plugins into the folder sites/js_wi_duedate_notifier_task in the deploy project.

Once this is done, it is possible to copy and paste the whole content of the deploy project into the configuration folder server/conf/ccm and accept any overwrites.

The image below shows the first part.

Packaging of ProjectsSee the post Is The Extension Deployed? How Can I Redeploy? to understand how to reset the server to enforce deploying the new extension.

Note, all projects are set up to ignore the files that don’t need to be under version control in RTC.

Configure the Due Date Notifier in Production

The Due Date Notifier Asynchronous Task can be configured in the advanced properties of the CCM server it is deployed.

Configure Advanced Properties for Notifier

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.

RTC 6.0 – Does the API Change and Break my Code?

I was curious if there will be a lot of rework needed to bring extensions and automation over to RTC 6.0. Here is what I have seen so far.

I found the time to setup my RTC 6.0 environment for Extensions development. This went very smoothly and did not differ to what I describe in Running the RTC Extensions Workshop With RTC 6.0. I then brought all the extensions, API and automation examples from my RTC API Development environment over into the new workspace in the RTC 6.0 environment.

I did not see any errors except in the code related to Contributing Attribute Presentations and the error can likely be fixed by using a different method. So although my API code touches a broad range of the API in RTC, there are only minor changes. The majority of the API seems to be very stable, since this was also the case when I brought over my code into RTC 4.0 some years ago.

There are likely additions to the API, but the API that is there seems to be very stable otherwise. This includes even internal API that I have used on some occasions. Of course, you still want to test that everything work after you brought it over.

I have to confess that I am so far doing the majority of the work in an environment based on RTC 4.0.1. The reasons are basically that RTC 5.x had an annoying issue as explained in Running The RTC 4.x Extensions Workshop With RTC 5.0.x. Since I did not want to put up with it all the time I kept working with 4.0.1 and only tested the result in different versions.

Summary

The RTC client and server API used in extensions and for automation seems to be very stable and there should be not a lot of effort required to bring over your own automation and extensions over to RTC 6.0

As always I hope this saves users out there some time and worries.

Following CALM Links using the Java Client or Server API

Going from just Rational Team Concert to a full Collaborative Application Lifecycle Management Solution introduces URI based link types. This post answers this forum question about how to follow URI based references to work items.

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.

*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. With respect to links, the code shows which kinds of different location types are used.

I updated the post The RTC WorkItem Client Link API – Linking to Work Items and Other Elements and added client API code to analyze the references for a work item, including URI based links.

The code can be reused in server extensions as well, except for the method analyzeReferenceTarget(). The post The RTC WorkItem Server Link API – Linking to Work Items and Other Elements needed to be updated as well, with server specific code for analyzeReferenceTarget(). If you work on a server extension, get the client code from The RTC WorkItem Client Link API – Linking to Work Items and Other Elements and replace analyzeReferenceTarget() with the code for the server from The RTC WorkItem Server Link API – Linking to Work Items and Other Elements.

As always, license statements in the posts apply to the code and remember that this code comes with the usual lack of promise or guarantee. Enjoy!

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.