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.

Advertisements

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.

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.

Creating Plans With the Plain Java Client API


Recently a customer approached me with the question how to automate creating plans. The customer had tried to get this done and got stuck. I was pretty convinced I had never tried this. However, I looked into the automation examples I had created over the years and was quite surprised to find an example I created some years back.

As always I thought it might be a good idea to share the code with the community.

Why is this interesting?

Wouldn’t it be nice if you could automate the process of managing timelines as much as possible?

I imagine having some code that:

  • Adds releases and iterations to timelines
    • Automatically setting start and end dates
    • Create ID’s, iteration types and names as expected in a naming convention
  • Creates all the standard plans you need to a new iteration for
    • The project area
    • Each team area
    • Name the plans as expected in a naming convention
  • Extends iterations (if you have to) and aligns the start and end dates of the iterations that come after

I think that would very beneficial and save a lot of time. Ultimately I’d like to have an example. Even bits and pieces could be interesting e.g. creating plans that are missing automatically.

I recently had to manually do this for 12 plans and it was no fun.

*Update*

Please be aware that the code below uses classes that are not shipped in the plain java client libraries. They are part of the client SDK.

The dependencies

import com.ibm.team.apt.common.IIterationPlanRecord;
import com.ibm.team.apt.internal.common.rcp.IIterationPlanService;
import com.ibm.team.apt.internal.common.rcp.dto.DTO_IterationPlanSaveResult;
import com.ibm.team.apt.internal.common.wiki.IWikiPage;

require the library com.ibm.team.apt.common  from the SDK. You can either copy it from the SDK and add it to the classpath or you add the SDK to your classpath.

Please also note that beginning with RTC 6.x the Eclipse client does no longer support browsing plans. So it is unclear if some of the SDK classes mentioned above would be removed from the RTC Client SDK over time.

Please note that the classes in com.ibm.js.team.api.workitem.common.utils are not shown in the code below, but they are part of the download.

Warning License, Further Reading

The code in this post is client API.

This blog post uses internal API which can be changed at any time.

Warning, some of the code uses internal API that might change in the future. If the Internal API changes, the code published here will no longer work.

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!

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

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 code attached to this post in the development environment you set up in the Rational Team Concert Extensions Workshop and get your own extensions or automation working there as well.

Download the Code

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

How The Code Works

As always lets look at how the code works a bit to understand it and be able to reuse it.

The code is, as most of my code examples based on the code that comes with the snippets in the Plain Java Libraries and the code from examples on Jazz.net.

The code comes in two Eclipse projects. Both are Plug In Development projects, because that allows me to use the Eclipse PDE to see, search and debug with the existing RTC SDK as described in Setting up Rational Team Concert for API Development and Understanding and Using the RTC Java Client API. It can later be run like any other java class.

It is two projects, because the are several helper classes, that I reuse across other examples and I get tired copying the classes around. You can move them into your project if you like. I will take them under SCM and re-use them across solutions in the future, enhancing them over time as well.

The main class is the usual structure that connects to the TeamPlatform and passes the parameters to the method run().

CreatePlan requires the following parameters:

CreatePlan [repositoryURI] [userId] [password] [planName] [planTypeID] [ownigProcessAreaName] [planIterationName]

It needs a repository URI, a user ID and password to log in and do anything. The user must have the required permissions to perform the operation.

The minimal information needed to configure a plan is:

PlanConfiguration

  • A name for the plan.
  • The Plan Type ID
  • The project or team area that is going to be the owner of the plan
  • The iteration that is set for the plan.

An Example:

CreatePlan https://clm.example.com:9443/ccm/ ralph ralph "Ralph's Plan" "com.ibm.team.apt.plantype.crossProject" "JKE Banking (Change Management)/Energy Efficiency Matters" "Main Development/Release 1.0/Sprint 3"

The plan type ID can be found in the process configuration:

PlanIDsThe owning process area name is constructed as path created from the area names beginning with the Project area down to the nested process area we are interested in, separated by ‘/’. If the plan is owned by the project area take only that name. Use ” if the names have spaces.

Example:

“JKE Banking (Change Management)/Energy Efficiency Matters”

From the Team Organization:

PlanConfigurationTeamOrganizationThe iteration path is constructed from the name of the timeline and the parent iterations down to the iteration we are interested in, separated by ‘/’. Use ” if the names have spaces. You can also use the iteration ID’s with the same structure.

For example:

“Main Development/Release 1.0/Sprint 3”

From the Team Artifacts view:

PlanConfigurationIteration

The method run() gets the string representation of the required parameters. It tries to connect to the RTC server. Then it tries to find the owning process area using a small utility. It then tries to find the plan iteration.

If it can find those, it calls a method that performs the plan creation.

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

	if (args.length != 7) {
		System.out
				.println("Usage: CreatePlan [repositoryURI] [userId] [password] [planName] [planTypeID] [ownigProcessAreaName] [planIterationName]");
		return false;
	}

	String repositoryURI = args[0];
	String userId = args[1];
	String password = args[2];
	String planName = args[3];
	String planTypeID = args[4];
	String ownigProcessAreaName = args[5];
	String planIterationName = args[6];
		
		
	IProgressMonitor monitor = new NullProgressMonitor();
	ITeamRepository teamRepository = TeamPlatform
			.getTeamRepositoryService().getTeamRepository(repositoryURI);
	teamRepository.registerLoginHandler(new LoginHandler(userId, password));
	teamRepository.login(monitor);

	System.out.println("Logged in as: "
			+ teamRepository.loggedInContributor().getName());

	IProcessClientService processClient = (IProcessClientService) teamRepository
			.getClientLibrary(IProcessClientService.class);

	// Find the project or team area that is going to be the owner of the plan
	IProcessArea ownigProcessArea = ProcessAreaUtil.findProcessArea(ownigProcessAreaName, processClient, monitor);
	if (ownigProcessArea == null) {
		System.out.println("Process area " + ownigProcessAreaName + " not found.");
		return false;
	}
		
	// Find the plan iteration in the development line 
	List path = Arrays.asList(planIterationName.split("/"));
	DevelopmentLineHelper deveLineHelper= new DevelopmentLineHelper(teamRepository, monitor);
	IIteration planIteration = deveLineHelper.findIteration(ownigProcessArea.getProjectArea(), path, DevelopmentLineHelper.BYLABEL); 
	if (planIteration == null) {
		System.out.println("Iteration " + planIterationName + " not found.");
		return false;
	}
		
	// If we have everything, create the paln
	createPlan(ownigProcessArea, planIteration, planName, planTypeID, teamRepository.loggedInContributor(), monitor);
	teamRepository.logout();

	return true;
}

The plan is created using the method createPlan(). This method creates all the pieces needed for the plan. It uses internal API pretty much everywhere. It has to, the plan API is not supported and not made consumable. You have to be aware that internal API can change without notice and even without documentation how to replace the old approach.

On the other hand, the internal API is also used in various places and it is likely to be needed in the future.

The method gets all the required services. Then it creates the new Plan item and gets the IIterationPlanRecord that represents it.

The next step is to set all the values for the plan configuration, name, owner, iteration and plan type ID.

It is not apparent, but the plan needs also a wiki page to be able to save it. This wiki page is created next.

Finally the plan is saved. If this succeeded, the new plan item interface is returned.

The code looks like below. Please note the internal API.

/**
 * WARNING, this method uses INTERNAL API
 * 
 * Creates a plan based on the process area that the plan is to be set in the 
 * Owner configuration element (can not be null),
 * the iteration that is set as root iteration of the plan configuration element
 * (can not be null), name and plan type Id. The creator user can not be null.
 *   
 * @param planOwner - the process area that owns that plan, can not be null
 * @param planIteration - the root iteration the plan selects, can not be null
 * @param planName - the name the plan will show have
 * @param planTypeID - the ID of the plan type from the plan process administration
 * @param creator - the user that will be set as creator of the plan
 * @param monitor - the progress monitor, can be null
 * @return The IIterationPlanRecord if the plan was created successfull or null
 * 
 * @throws UnsupportedEncodingException
 * @throws TeamRepositoryException
 */
public static IIterationPlanRecord createPlan(IProcessArea planOwner, IIteration planIteration,
		String planName, String planTypeID, IContributor creator, IProgressMonitor monitor)
		throws UnsupportedEncodingException, TeamRepositoryException {

	// Get the Team Repository
	ITeamRepository teamRepository = (ITeamRepository) planOwner.getOrigin();
		
	// get classes for plan creation
	IAuditableCommon auditableCommon = (IAuditableCommon) teamRepository
			.getClientLibrary(IAuditableCommon.class);
	// The IIterationPlanService - this is an internal API class
	IIterationPlanService planService = (IIterationPlanService) ((IClientLibraryContext) teamRepository).getServiceInterface(IIterationPlanService.class);

	// create necessary plan items
	// The IIterationPlanRecord 
	IIterationPlanRecord plan = (IIterationPlanRecord) IIterationPlanRecord.ITEM_TYPE
			.createItem();

	// setup plan values
	plan.setName(planName);
	plan.setIteration(planIteration);
	plan.setPlanType(planTypeID);
	plan.setOwner(planOwner);

	// The IWikiPage - this is an internal API class
	IWikiPage wiki = (IWikiPage) IWikiPage.ITEM_TYPE.createItem();

	// setup wiki page
	String encoding = "UTF8";
	String xmlText = "";
	byte[] bytes = xmlText.getBytes(encoding);
	InputStream inputStream = new ByteArrayInputStream(bytes);

	// The IWikiPage methods - these methods are all internal API
	wiki.setName("");
	wiki.setWikiID(IIterationPlanRecord.OVERVIEW_PAGE_ID);
	wiki.setCreator(creator);
	wiki.setOwner(plan);
	wiki.setContent(auditableCommon.storeContent(IContent.CONTENT_TYPE_TEXT,
			encoding, inputStream, bytes.length, monitor));

	// save plan
	// The these classes and methods are all internal API
	DTO_IterationPlanSaveResult saveResult = planService.save(planOwner,
			plan, wiki);
	// check if the save was successful
	if (saveResult.isSetIterationPlanRecord() == false) {
		System.out.println("Saving failed!");
		return null;
	}
	return plan;
}

Summary

The example above shows how you can automate creation of plans. With some more code to create and maintain timelines, this can make RTC project administration a lot easier and remove repetitive tasks from your plate.

As always I hope this helps someone out there to get their job done more efficient.

Extending the WorkItem Command Line With New Commands


If one needs a new command to be run in the WorkItem Command Line, what needs to be done? Lets explore this with the background from the post The WorkItem Command Line Explained.

As always, keep in mind, this is not production code, under development and not designed to win a beauty pageant. Testing has not been very thorough, so using this code is at your own risk.

Latest Version

See A RTC WorkItem Command Line Version 3.0 for the latest version.

The New Command

The new command is supposed to help with migrating Multi Select Enumeration  attributes available until RTC 3.x to Enumeration List attributes available in RTC 4.x and later.

As explained in Workaround: Migrate from Rational Team Concert 3.X string attribute used as multi-select lists to RTC 4.X enumeration lists, in RTC 3.x these multi select lists where implemented as a string attribute, that stored the enumeration literal ID’s separated by comma. A special presentation handled input and output. Downside of this approach was, that it was hard to query these attributes and create dashboards and reports. The new way is a special list type for each enumeration.

The workaround explains how to use CSV export and import to migrate the data. This solution uses the API to do so. What this solution does not attempt so far is having a mapping that changes the data.

Related posts

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.

On the other hand, you have the code and are able to add your own code to it. It would be nice to know what you did and how, if you do so.

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 code attached to this post in the development environment you set up in the Rational Team Concert Extensions Workshop and get your own extensions or automation working there as well.

Download

Download the latest version that includes the code from this post.

Requirements

The command is supposed to work on work items of a specific type in a project area.

  • For a source attribute (of type string), read the value, split it into enumeration literal ID’s
  • Find the enumeration literals and build a list from them
  • Store the list of enumeration literals in a target attribute
  • Perform this for one work item of the type (e.g. for testing)
  • Perform this for all work items of the type
  • Be able to tolerate errors and run nonetheless

The image below shows the source attribute as shown in RTC 4.x with a regular string presentation and the new EnumerationList attribute. The values from the source attribute have been migrated over to the target attribute.

Source and target attribute

Source and target attribute

Creating the new Command

The first step is to create a new command class. As explained in The WorkItem Command Line Explained there are some abstract classes available to be used. The AbstractWorkItemModificationCommand seems to be it, but looking closer, it is very specialized in creating or modifying a work item with a list of provided attributes and values in a parameter list. So the choice here is the AbstractTeamRepositoryCommand instead.

So the first step is to create the class MigrateWorkItemAttributeCommand in the package com.ibm.js.team.workitem.commandline. Create a public constructor as required from the abstract class and override the abstract methods. In the method getCommandName() return the name of the command. Also override the method setRequiredParameters() but make sure to add a call to super() to get the parameters it requires added. The initial step would look similar to this:

public class MigrateWorkItemAttributeCommand extends
		AbstractTeamRepositoryCommand {

	public MigrateWorkItemAttributeCommand(ParameterManager parametermanager) {
		super(parametermanager);
	}

	/***
	 * Add required parameters
	 * 
	 * (non-Javadoc)
	 * 
	 * @see com.ibm.js.team.workitem.commandline.framework.AbstractTeamRepositoryCommand#setRequiredParameters()
	 */
	@Override
	public void setRequiredParameters() {
		super.setRequiredParameters();
	}

	/***
	 * Return the command
	 * 
	 * (non-Javadoc)
	 * 
	 * @see com.ibm.js.team.workitem.commandline.framework.IWorkItemCommand#getCommandName()
	 */
	@Override
	public String getCommandName() {
		return "migrateattribute";
	}

	/***
	 * Perform the command
	 * 
	 * (non-Javadoc)
	 * 
	 * @see com.ibm.js.team.workitem.commandline.framework.AbstractCommand#process()
	 */
	@Override
	public OperationResult process() throws TeamRepositoryException {
		return getResult();
	}
}

In addition to the repository URL, the user name and the password, the command needs to know the project area, and the the work item type. It also needs the parameter for the source attribute ID and the target attribute ID. Some flags like ignoreErrors might come in handy as well.

Most of this can be harvested from existing commands like the CreateWorkItemCommand.  A few new need to be added. Introducing some constants is a good idea for later. The code added to setRequiredParameters() would finally look like below. The call to super() makes sure the basic parameters for the login process are required as well.

public static final String COMMAND_MIGRATE_ENUMERATION_LIST_ATTRIBUTE = "migrateattribute";
public static final String PARAMETER_SOURCE_ATTRIBUTE_ID = "sourceAttributeID";
public static final String PARAMETER_SOURCE_ATTRIBUTE_ID_EXAMPLE = "com.acme.custom.enum.multiselect";
public static final String PARAMETER_TARGET_ATTRIBUTE_ID = "targetAttributeID";
public static final String PARAMETER_TARGET_ATTRIBUTE_ID_EXAMPLE = "com.acme.custom.enum.list";


/***
 * Add required parameters
 * 
 * (non-Javadoc)
 * 
 * @see com.ibm.js.team.workitem.commandline.framework.AbstractTeamRepositoryCommand#setRequiredParameters()
 */
@Override
public void setRequiredParameters() {
	super.setRequiredParameters();
	// Copied from CreateWorkItemCommand
	getParameterManager()
			.syntaxAddRequiredParameter(
					IWorkItemCommandLineConstants.PARAMETER_PROJECT_AREA_NAME_PROPERTY,
					IWorkItemCommandLineConstants.PARAMETER_PROJECT_AREA_NAME_PROPERTY_EXAMPLE);
	getParameterManager()
			.syntaxAddRequiredParameter(
					IWorkItemCommandLineConstants.PARAMETER_WORKITEM_TYPE_PROPERTY,
					IWorkItemCommandLineConstants.PARAMETER_WORKITEM_TYPE_PROPERTY_EXAMPLE);
	getParameterManager().syntaxAddSwitch(
			IWorkItemCommandLineConstants.SWITCH_IGNOREERRORS);
	getParameterManager().syntaxAddRequiredParameter(
			PARAMETER_SOURCE_ATTRIBUTE_ID,
			PARAMETER_SOURCE_ATTRIBUTE_ID_EXAMPLE);
	getParameterManager().syntaxAddRequiredParameter(
			PARAMETER_TARGET_ATTRIBUTE_ID,
			PARAMETER_TARGET_ATTRIBUTE_ID_EXAMPLE);
}

The method getCommandName() would look like this now, with the string extracted into a constant:

	/***
	 * Return the command
	 * 
	 * (non-Javadoc)
	 * 
	 * @see com.ibm.js.team.workitem.commandline.framework.IWorkItemCommand#getCommandName()
	 */
	@Override
	public String getCommandName() {
		return COMMAND_MIGRATE_ENUMERATION_LIST_ATTRIBUTE;
	}

Add The New Command to the Main Class

The new command needs to be added to the WorkItem Command Line. In the class WorkitemCommandLine add it to the method method addSupportedCommands() like shown below.

/**
 * Add the supported commands. If introducing a new command, add it here.
 * 
 * @param parameterManager
 */
private void addSupportedCommands(ParameterManager parameterManager) {
	addSupportedCommand(new PrintTypeAttributesCommand(
			new ParameterManager(parameterManager.getArguments())));
	addSupportedCommand(new CreateWorkItemCommand(new ParameterManager(
			parameterManager.getArguments())));
	addSupportedCommand(new UpdateWorkItemCommand(new ParameterManager(
			parameterManager.getArguments())));
	addSupportedCommand(new MigrateWorkItemAttributeCommand(new ParameterManager(
			parameterManager.getArguments())));
}

Now it can be used like shown below. It is also possible to start debugging it using Launches.

wcl -migrateattribute /ignoreErrors repository="https://clm.example.com:9443/ccm" user=ralph password=ralph projectArea="JKE Banking (Change Management)" workItemType=task sourceAttributeID=custom.medium.string targetAttributeID=custom.enumeration.list

or this

wcl -migrateattribute /ignoreErrors repository="https://clm.example.com:9443/ccm" user=ralph password=ralph projectArea="JKE Banking (Change Management)" workItemType=task  id=275 sourceAttributeID=custom.medium.string targetAttributeID=custom.enumeration.list

Implementing the Command Logic

Now the method process() needs to be implemented. It orchestrates the work to be done. This obviously took some iterations. Here is the final result.

/***
 * Perform the command
 * 
 * (non-Javadoc)
 * 
 * @see com.ibm.js.team.workitem.commandline.framework.AbstractCommand#process()
 */
@Override
public OperationResult process() throws TeamRepositoryException {
	// From CreateWorkItemCommand
	// Get the parameters such as project area name and Attribute Type and
	// run the operation
	String projectAreaName = getParameterManager()
			.consumeParameter(
					IWorkItemCommandLineConstants.PARAMETER_PROJECT_AREA_NAME_PROPERTY)
			.trim();
	// Find the project area
	IProjectArea projectArea = ProcessAreaUtil.findProjectArea(
			projectAreaName, getProcessClientService(), getMonitor());
	if (projectArea == null) {
		throw new WorkItemCommandLineException("Project Area not found: "
				+ projectAreaName);
	}

	String workItemTypeID = getParameterManager().consumeParameter(
			IWorkItemCommandLineConstants.PARAMETER_WORKITEM_TYPE_PROPERTY)
			.trim();
	// Find the work item type
	IWorkItemType workItemType = WorkItemHelper.findWorkItemType(
			workItemTypeID, projectArea.getProjectArea(),
			getWorkItemCommon(), getMonitor());

	// Get the parameter values - The source attribute
	String sourceAttributeID = getParameterManager().consumeParameter(
			PARAMETER_SOURCE_ATTRIBUTE_ID).trim();
	// check if old attribute ID is string type
	IAttribute sourceIAttribute = getWorkItemCommon().findAttribute(
			projectArea, sourceAttributeID, getMonitor());
	if (sourceIAttribute == null) {
		throw new WorkItemCommandLineException(
				"Source Attribute not found: " + sourceAttributeID);
	}
	if (!AttributeTypes.STRING_TYPES.contains(sourceIAttribute
			.getAttributeType())) {
		throw new WorkItemCommandLineException(
				"Source Attribute is not a String type: "
						+ sourceAttributeID);
	}

	// Get the parameter values - The target attribute
	String targetAttributeID = getParameterManager().consumeParameter(
			PARAMETER_TARGET_ATTRIBUTE_ID).trim();
	// check if new attribute ID is EnumerationList
	IAttribute targetIAttribute = getWorkItemCommon().findAttribute(
			projectArea, targetAttributeID, getMonitor());
	if (targetIAttribute == null) {
		throw new WorkItemCommandLineException(
				"Target Attribute not found: " + targetAttributeID);
	}
	if (!AttributeTypes.isEnumerationListAttributeType(targetIAttribute
			.getAttributeType())) {
		throw new WorkItemCommandLineException(
				"Target Attribute is not an EnumerationList: "
						+ targetAttributeID);
	}
	if (getParameterManager().hasSwitch(
			IWorkItemCommandLineConstants.SWITCH_IGNOREERRORS)) {
		setIgnoreErrors();
	}
	String wiID = getParameterManager().consumeParameter(
			IWorkItemCommandLineConstants.PARAMETER_WORKITEM_ID_PROPERTY);
	if (wiID != null) {
		IWorkItem wi = WorkItemHelper.findWorkItemByID(wiID,
				IWorkItem.SMALL_PROFILE, getWorkItemCommon(), getMonitor());
		if (!wi.getWorkItemType().equals(workItemType.getIdentifier())) {
			throw new WorkItemCommandLineException(
					"Work item type mismatch: "
							+ workItemType.getIdentifier() + " specified "
							+ workItemType.getIdentifier());
		}
		migrateSingleWorkItem(wi, sourceIAttribute, targetIAttribute);
	} else {
		// Update all work items of this type.
		migrateAllWorkItems(projectArea, workItemType, sourceIAttribute,
				targetIAttribute);
	}
	// If we got here, we succeeded
	getResult().setSuccess();
	return getResult();
}

What it basically does is to get the parameters needed for this command and work with them. Keep in mind that the abstract class AbstractTeamRepositoryCommand does the login process already. So what needs to be done is to get the project area, the work item type, the source and the target attribute ID and the flags. Most of the code has been harvested in existing commands as well as in the WorkItemHelper class. The methods migrateSingleWorkItem() and migrateAllWorkItems() will be explained in a bit.

Some new fields and getters/setters where introduced in the process. We also need the separator for the literals in the original string later. The code looks as below:

public static final String SEPARATOR_ENUMERATION_LITERAL_ID_LIST = ",";

private boolean fIgnoreErrors = false;

private void setIgnoreErrors() {
	fIgnoreErrors = true;
}

private boolean isIgnoreErrors() {
	return fIgnoreErrors;
}

public MigrateWorkItemAttributeCommand(ParameterManager parametermanager) {
	super(parametermanager);
}

There was also a need to be able to access the ITeamRepository in the MigrateWorkItemAttributeCommand. I introduced a getter into the AbstractTeamRepositoryCommand class for convenience.

Note, it is always possible to get the ITeamRepository from an RTC object using the getOrigin() method and cast it to the ITeamRepository. However, it makes sense to add this to the framework, too.

Implementing the Work Item Modification

The final implementation steps are to implement the missing methods as well as add a inner class extending WorkItemOperation, to do the change to the work item. The code is shown below.

/**
 * Migrate one specific work item - for testing
 * 
 * @param wi
 * @param sourceIAttribute
 * @param targetIAttribute
 * @throws TeamRepositoryException
 */
private void migrateSingleWorkItem(IWorkItem wi,
		IAttribute sourceIAttribute, IAttribute targetIAttribute)
		throws TeamRepositoryException {
	MigrateWorkItem operation = new MigrateWorkItem("Migrate",
			IWorkItem.FULL_PROFILE, sourceIAttribute, targetIAttribute);
	performMigration((IWorkItemHandle) wi.getItemHandle(), operation);
}

/**
 * Migrate all work items of a specific type in a project area
 * 
 * @param projectArea
 * @param workItemType
 * @param sourceIAttribute
 * @param targetIAttribute
 * @throws TeamRepositoryException
 */
private void migrateAllWorkItems(IProjectArea projectArea,
		IWorkItemType workItemType, IAttribute sourceIAttribute,
		IAttribute targetIAttribute) throws TeamRepositoryException {
	// Find all work items of this type.
	// Create an Expression to find them
	IQueryableAttribute attribute = QueryableAttributes.getFactory(
			IWorkItem.ITEM_TYPE).findAttribute(projectArea,
			IWorkItem.PROJECT_AREA_PROPERTY, getAuditableCommon(),
			getMonitor());
	IQueryableAttribute type = QueryableAttributes.getFactory(
			IWorkItem.ITEM_TYPE).findAttribute(projectArea,
			IWorkItem.TYPE_PROPERTY, getAuditableCommon(), getMonitor());
	Expression inProjectArea = new AttributeExpression(attribute,
			AttributeOperation.EQUALS, projectArea);
	Expression isType = new AttributeExpression(type,
			AttributeOperation.EQUALS, workItemType.getIdentifier());
	Term typeinProjectArea = new Term(Term.Operator.AND);
	typeinProjectArea.add(inProjectArea);
	typeinProjectArea.add(isType);

	// Run the Expression
	IQueryClient queryClient = getWorkItemClient().getQueryClient();
	IQueryResult results = queryClient.getExpressionResults(
			projectArea, typeinProjectArea);
	// Override the result set limit so that we get more than 1000 items if
	// there are more
	results.setLimit(Integer.MAX_VALUE);
	MigrateWorkItem operation = new MigrateWorkItem("Migrate",
			IWorkItem.FULL_PROFILE, sourceIAttribute, targetIAttribute);
	// Run the operation for each result
	while (results.hasNext(getMonitor())) {
		IResult result = (IResult) results.next(getMonitor());
		performMigration((IWorkItemHandle) result.getItem(), operation);
	}
}

/**
 * Perform the update and
 * 
 * @param handle
 * @param operation
 * @throws WorkItemCommandLineException
 */
private void performMigration(IWorkItemHandle handle,
		MigrateWorkItem operation) throws WorkItemCommandLineException {
	String workItemID = "undefined";
	try {
		IWorkItem workItem = getAuditableCommon().resolveAuditable(
				(IWorkItemHandle) handle, IWorkItem.SMALL_PROFILE,
				getMonitor());
		workItemID = getWorkItemIDString(workItem);
		operation.run(handle, getMonitor());
		getResult().appendResultString(
				"Migrated work item " + workItemID + ".");
	} catch (TeamRepositoryException e) {
		throw new WorkItemCommandLineException(
				getResult().getResultString()
						+ "TeamRepositoryException: Work item "
						+ workItemID + " attribute not migrated. "
						+ e.getMessage(), e);
	} catch (WorkItemCommandLineException e) {
		String message = "WorkItemCommandLineException Work item " + workItemID
				+ " attribute not migrated. " + e.getMessage();
		if (!isIgnoreErrors()) {
			throw new WorkItemCommandLineException(getResult().getResultString() + message, e);
		} else {
			getResult().appendResultString(message);
		}
	}
}

The method migrateSingleWorkItem() basically runs the migration operation for a single work item. It creates the inner class MigrateWorkItem with the data it needs and then calls the method performMigration() to perform the migration. The method performMigration() calls the provided method and does all the error handling.

The method migrateAllWorkItems() basically creates an expression to get all work items of the specified type from the project area. The expression is run and performMigration() is performed on one result after the other.

Please note that the line

results.setLimit(Integer.MAX_VALUE);

makes sure that the built in query limit of the Eclipse client is overwritten to allow getting all possible results. To work it has to be exactly at the place it is.

The method performMigration() just performs the call and wraps everything up to be able to process exceptions. The information is provided in the result.

The methods above use some additional methods to get the client library IWorkItemClient to be able to perform the query expression.

/**
 * We need this client libraries to run queries
 * 
 * @return
 */
private IWorkItemClient getWorkItemClient() {
	return (IWorkItemClient) getTeamRepository().getClientLibrary(
			IWorkItemClient.class);
}

/**
 * Get the work item ID as string
 * 
 * @param workItem
 * @return
 */
private String getWorkItemIDString(IWorkItem workItem) {
	return new Integer(workItem.getId()).toString();
}

The WorkItemOperation Implementation

Finally lets look at the inner class MigrateWorkItem which extends WorkItemOperation. I use this approach in all the work item manipulation client API usage, because it conveniently handles all possible mishaps.

private class MigrateWorkItem extends WorkItemOperation {
	
	IAttribute fsourceAttribute = null;
	IAttribute fTargetAttribute = null;
	
	/**
	 * Constructor
	 * 
	 * @param The
	 *            title message for the operation
	 * @param message
	 * @param profile
	 * @param sourceAttribute
	 * @param targetAttribute
	 */
	public MigrateWorkItem(String message, ItemProfile profile,
			IAttribute sourceAttribute, IAttribute targetAttribute) {
		super(message, profile);
		fsourceAttribute = sourceAttribute;
		fTargetAttribute = targetAttribute;
	}
	
	/***
	 * This gets called if run() is called
	 * 
	 * @see com.ibm.team.workitem.client.WorkItemOperation#execute(com.ibm.team
	 *      .workitem.client.WorkItemWorkingCopy,
	 *      org.eclipse.core.runtime.IProgressMonitor)
	 */
	@Override
	protected void execute(WorkItemWorkingCopy workingCopy,
			IProgressMonitor monitor) throws TeamRepositoryException,
			RuntimeException {
	
		IWorkItem workItem = workingCopy.getWorkItem();
		String thisItemID = getWorkItemIDString(workItem);
		if (!workItem.hasAttribute(fsourceAttribute)) {
			throw new WorkItemCommandLineException(
					"Work Item "
							+ thisItemID
							+ " Source Attribute not available - Synchronize Attributes: "
							+ fsourceAttribute.getIdentifier());
		}
		if (!workItem.hasAttribute(fTargetAttribute)) {
			throw new WorkItemCommandLineException(
					"Work Item "
							+ thisItemID
							+ " Target Attribute not available - Synchronize Attributes: "
							+ fTargetAttribute.getIdentifier());
		}
		// get the old value - a string with literals separated by a comma
		Object ovalue = workItem.getValue(fsourceAttribute);
		// compute the result values
		String sourceValues = "";
		if (null != ovalue && ovalue instanceof String) {
			sourceValues = (String) ovalue;
		}
		if (!sourceValues.equals("")) {
			String[] values = sourceValues
					.split(SEPARATOR_ENUMERATION_LITERAL_ID_LIST);
			IEnumeration enumeration = getWorkItemCommon()
					.resolveEnumeration(fTargetAttribute, monitor);

			List results = new ArrayList();
			for (String literalID : values) {
				if (literalID == "") {
					// Nothing to do
					continue;
				}
				Identifier literal = getLiteralEqualsIDString(
						enumeration, literalID);
				if (null == literal) {
					throw new WorkItemCommandLineException("Work Item "
							+ thisItemID
							+ " Target literal ID not available: "
							+ literalID + " Attribute "
							+ fTargetAttribute.getIdentifier());
				}
				results.add(literal);
			}
			// Set the value
			workItem.setValue(fTargetAttribute, results);
		}
		getResult().appendResultString("Migrated work item " + thisItemID);
	}

	/**
	 * Gets an enumeration literal for an attribute that has the specific
	 * literal ID.
	 * 
	 * @param enumeration
	 *            - the enumeration to look for
	 * @param literalIDString
	 *            - the literal ID name to look for
	 * @return the literal or null
	 * @throws TeamRepositoryException
	 */
	private Identifier getLiteralEqualsIDString(
			final IEnumeration enumeration,
			String literalIDString) throws TeamRepositoryException {
		List literals = enumeration
				.getEnumerationLiterals();
		for (Iterator iterator = literals.iterator(); iterator
				.hasNext();) {
			ILiteral iLiteral = (ILiteral) iterator.next();
			if (iLiteral.getIdentifier2().getStringIdentifier()
					.equals(literalIDString.trim())) {
				return iLiteral.getIdentifier2();
			}
		}
		return null;
	}
}

The interesting method here is execute(). It basically checks if the work item has the source attribute and the target attribute. If one of the attributes is missing, it can not run. You have to synchronize attributes first.

As last step the method gets the string value and splits the string into its values. The list of enumeration literal ID’s is then used to look up the enumeration literals. In case a literal can not be found, this is an error. If the literals are retrieved, they are put into a list and finally set as the value for the new attribute.

The method getLiteralEqualsIDString() is actually mostly harvested from the WorkItemHelper class, where it is used with the literal display name.

Additional Changes to the Framework

Since this command works very different than the others, it became apparent that it would be nice to see progress directly, if we are not working in RMI server mode. this also helps the other commands and potential future commands. To provide a better user experience

    • In WorkitemCommandLine the method isServer() has been made public so that it can be read from the framework classes

 

  • In OperationResult the method appendResultString() was changed to output the information rather than storing it in a result string, if the WorkItemCommandLine does not run in server mode

This change makes the result output much more immediate, especially for long running commands in normal mode. In RMI mode, the data is provided as is, after the call finishes. Some Observations The Parameter handling does not handle optional parameters well. It should be possible to add an optional parameter like the work item ID in this case. However, this is just a minor problem here. In addition at some point it can get so complicated that the automation for printing the command syntax won’t be easily manageable any more. Summary This post explains how to extend the WorkItem Command Line with your own commands. As always, I hope that helps someone out there.

Adding Context Menus for Jazz Objects to the RTC Eclipse Client


Is it possible to add customized actions to the context menus in Eclipse? This is an interesting question that recently came up  in the context of my work.

This post shows how this can be done, in case you are asked to do this kind of work. It explains

  • What extensions are possible in general
  • What you need to know to try to create such extensions
  • How to get at some of the data you need
  • A simple example that shows the main steps you need to take and the most interesting code snippets

* Update * If you need to get rid of menu entries, check this post.

License

As always, 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. Remember 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

The example adds a context menu action on snapshots. It provides an example for an action directly in the context menu as well as an example for a sub menu that can be used to organize several actions. The outcome looks like below.

Pop up menu action for a Snapshot

Pop up menu action for a Snapshot

If the action is performed, the example will get the object and do something with it.

What Can be Extended?

I knew that this would be possible in general. I have created such context menu extensions for our Eclipse based modelling tools in the past. Eclipse is built to be extended. In general you can create all kinds of tools that extend Eclipse. For example

  • Eclipse Views – Tree, Table, Table Tree Views
  • Eclipse Editors
  • Eclipse Menus – menu bars, custom actions, pop up menus
  • Extend existing Eclipse extensions e.g. provide context menu extensions or add functionality
  • Decorators for tree views

All these capabilities are described in numerous books about extending Eclipse and in even more tutorials available on the web. This article describes how to create menu contributions. I won’t go too deep into what is covered in these information sources. Instead I will try to focus on the interesting topics related to RTC.

What is the Problem?

So, if this is all possible, why blog about it here? The problem is, that all these extensions require information about the objects and editors you want to extend. As an example a pop up menu contribution providing an action to the context menu of a view requires the class of the object that is provided in the tree node to work. Without this information you can’t do anything.

I found it very hard to get the information in the past. You can obviously start with a very general object and try to debug. However this can be very tedious. How can you find this information more efficient?

The answer to that is using tools available to find the information. I will show some of the tools available and how they can be used. Lets get started with creating our example.

Example Project

We want a pop up menu extension that allows to perform some action on a snapshot in some views.

The easiest way to start is to use the Plug-in Project wizard Use File>New>Project and create a Plug-in Project. Pick a good project name. As a best practice I always use a naming pattern that is aligned with the naming patterns used in Jazz, adding a unique infix that will later allow me to search for the code in case it is deployed. As an example I use com.ibm.js.team.filesystem.snapshot.menucontribution. Then use Next to go to the next specification step. As a best practice use the name as ID and specify the other values. The image below shows my choices.

Specify the project properties

Specify the project properties

Use Next to get to the extension templates. Select Plug-in with a Popup menu and click Finish. The wizard does its miracles and you now have a new project.

Inspecting the project reveals a plugin.xml and a new action class have been created. The project is fully functional. It would be possible to start an Eclipse Client Debug run-time and find a new pop up menu if clicking on a file in the Eclipse Navigator, Project Explorer or other views. If you click on a snapshot in the search view, you see – nothing.

However, that is not our target. What makes this work and what needs to be done to make this work for the given example?

For what objects this works is basically hidden in the plugn.xml. It looks as follows:

Generated Plugin XML

Generated Plugin XML

The extension is an object contribution. It works for objects displayed in views that are of the class org.eclipse.core.resources.IFile. The menu defines a sub menu that is inserted before a place called additions and defines a new menu group group1. The Action in this sub menu enables for selections of one element and calls the specified action class.

A snapshot is not of the class org.eclipse.core.resources.IFile. So much is clear, but what class do we need here?

YARI – Yet Another RCP Inspector

One way of finding out this crucial detail, that works for me, is using one of the many Eclipse inspection tools. I found YARI – Yet Another RCP Inspector searching the Web due to a hint and use this tool since then. It needs to be installed into Eclipse as described in the install instructions. Once installed it provides various new views that you can use to inspect Eclipse RCP applications, including Eclipse itself. The data it provides can be overwhelming at times, but is very useful. Use the documentation and FAQ’s provided to understand better what you can find in the data.

To find out what is exposed in the search view for a snapshot, open the view SWT Inspector that YARI provides.

Now search for a snapshot. Select the ‘capture mouse down‘ action in the SWT Inspector and click on the snapshot in the search view. Click at the ‘capture mouse down’ action in the SWT Inspector again, to switch it off. Now you can look at the captured data and use the mouse without capturing again.

The inspected data

The captured data

Look at the captured data. The data node in the TableItem element is what we need in this context. The class that is exposed here is class com.ibm.team.filesystem.ui.wrapper.SnapshotWrapper. This is basically the class that we need in the object contribution.

Plug-in Spy

Another Tool, that you can use to look under the covers of Eclipse extensions, is Plug-in Spy. This tool is now bundled with Eclipse in the Plugin Develupment Environment (PDE). You can invoke Plug-in Spy using SHIFT-ALT-F1 on a window. It tells you information such as which class provides the editor and what plugin contributes it. This is useful e.g. to find out how the Editors work and what API they use. We don’t need this information for the example, but other cases have proven this to be useful.

Plug-in Spy

Plug-in Spy

Plug-in Referencs

You can also, from the extension in a plugin.xml search for Plug-in Referencs. This shows other plug-ins that extend the same extension point. Since RTC does this, you can find and open related plugin.xml files and look for contribution classes, editor id’s and other useful information.

Plug-ins referencing the same extension point

Plug-ins referencing the same extension point

In this case com.ibm.filesystem.ide.ui is the best candidate to look at first. We don’t need the information for this example, but other examples would require to look here.

Modifying the Example

We can now modify the example to provide us with the desired solution. The steps are

  • Renaming the action class
  • Renaming ID’s and display text in the plugin XML
  • Adding a new contribution to the plugin
  • Adding required dependencies to RTC plug-ins
  • Adding code to the action

After renaming the acion class – and making sure that the new class name is actually correctly used in the plugin.xml – modify the plugin.xml to show menu captions as desired and add another contribution that directly contributes to the context menu, without creating a sub menu.

After the modifications the plugin.xml looks as below.

Final plugin.xmlBoth contributions are very similar. The difference is that the first contribution in the XML creates a new sub-meu and maps the action on to the menubarPath for this sub menu. The second contribution maps an action directly into the menubarPathadditions‘.

The order these contributions will show up in the menu is actually the reverse order of their definition in the plugin.xml.

Please Note: Don’t ask me how this works in detail. I just know enough and have example pattern that work for me. If you need more information search the Web for articles or find a good book about it.

What is left is to actually do something in the action.

SnapshotAction
The code below shows the class SnapshotAction that executes if the pop up menu is selected. Most of the code that actually works with the API is left out, because it is not that interesting. The code below can easily be used as a starting point for any menu action for any RTC object.

package com.ibm.js.team.filesystem.snapshot.menucontribution.popup.actions;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IActionDelegate;
import org.eclipse.ui.IObjectActionDelegate;
import org.eclipse.ui.IWorkbenchPart;

import com.ibm.team.filesystem.ui.wrapper.SnapshotWrapper;
import com.ibm.team.process.common.IProjectArea;
import com.ibm.team.process.common.IProjectAreaHandle;
import com.ibm.team.repository.client.IItemManager;
import com.ibm.team.repository.client.ITeamRepository;
import com.ibm.team.repository.common.IAuditableHandle;
import com.ibm.team.repository.common.TeamRepositoryException;
import com.ibm.team.scm.common.IBaselineSet;
import com.ibm.team.scm.common.IWorkspace;
import com.ibm.team.workitem.client.IQueryClient;
import com.ibm.team.workitem.client.IWorkItemClient;
import com.ibm.team.workitem.common.IAuditableCommon;
import com.ibm.team.workitem.common.expression.AttributeExpression;
import com.ibm.team.workitem.common.expression.Expression;
import com.ibm.team.workitem.common.expression.IQueryableAttribute;
import com.ibm.team.workitem.common.expression.QueryableAttributes;
import com.ibm.team.workitem.common.expression.Term;
import com.ibm.team.workitem.common.model.AttributeOperation;
import com.ibm.team.workitem.common.model.IWorkItem;
import com.ibm.team.workitem.common.query.IQueryResult;
import com.ibm.team.workitem.common.query.IResult;
import com.ibm.team.workitem.rcp.ui.WorkItemUI;

public class SnapshotAction implements IObjectActionDelegate {

	private Shell shell;
	private IWorkbenchPart fTargetPart;
	private SnapshotWrapper fSelection;
	private IWorkItemClient fWorkItemClient;
	private IAuditableCommon fAuditableCommon;
	private ITeamRepository fTeamRepository;

	/**
	 * Constructor for Action1.
	 */
	public SnapshotAction() {
		super();
	}

	/**
	 * @see IObjectActionDelegate#setActivePart(IAction, IWorkbenchPart)
	 */
	public void setActivePart(IAction action, IWorkbenchPart targetPart) {
		shell = targetPart.getSite().getShell();
		fTargetPart = targetPart;
	}

	/**
	 * @see IActionDelegate#run(IAction)
	 */
	public void run(IAction action) {
		IProgressMonitor monitor = new NullProgressMonitor();
		IBaselineSet snapshot = fSelection.getSnapshot();
		String snapshotUUID = snapshot.getItemId().getUuidValue();
		try {
			fTeamRepository = (ITeamRepository) snapshot.getOrigin();
			fWorkItemClient = (IWorkItemClient) fTeamRepository
					.getClientLibrary(IWorkItemClient.class);
			fAuditableCommon = (IAuditableCommon) fTeamRepository
					.getClientLibrary(IAuditableCommon.class);

			performAction(snapshotUUID, monitor);
			MessageDialog.openInformation(
					shell,
					"SnapshotMenuContribution",
					"My Action was executed on Snapshot: '"
							+ snapshot.getName() + "' UUID [" + snapshotUUID
							+ "]");

		} catch (Exception e) {
			MessageDialog.openError(shell, "SnapshotMenuContribution",
					e.getMessage() + "\nMy Action was executed on Snapshot: '"
							+ snapshot.getName() + "' UUID [" + snapshotUUID
							+ "]");
		}
	}

	private void performAction(String snapshotUUID, IProgressMonitor monitor)
			throws Exception, TeamRepositoryException {
			// Do something here
	}

	/**
	 * @see IActionDelegate#selectionChanged(IAction, ISelection)
	 */
	public void selectionChanged(IAction action, ISelection selection) {
		fSelection = null;

		if (selection instanceof IStructuredSelection) {
			Object first = ((IStructuredSelection) selection).getFirstElement();
			if (first instanceof SnapshotWrapper) {
				fSelection = (SnapshotWrapper) first;
			}
		}
	}
}

Lets look at the interesting parts first. The class implements IObjectActionDelegate. This is an interface of the Eclipse framework that is always used for this type of actions.

The method selectionChanged() is used to get the selected element(s). It is called when selecting elements for the contribution. The code is fairly typical and can also be refined to get a list of selections.

/**
 * @see IActionDelegate#selectionChanged(IAction, ISelection)
 */
public void selectionChanged(IAction action, ISelection selection) {
	fSelection = null;

	if (selection instanceof IStructuredSelection) {
		Object first = ((IStructuredSelection) selection).getFirstElement();
		if (first instanceof SnapshotWrapper) {
			fSelection = (SnapshotWrapper) first;
		}
	}
}

The code inspects the selection, uses IStructuredSelection to get at the elements, and checks that the correct class is passed, casts the input and stores it in a field.

The method setActivePart() is used to store some information about the UI that can be used later. Some UI elements such as message dialogs need the current shell. Others need the target part. These values are stored in fields.

/**
 * @see IObjectActionDelegate#setActivePart(IAction, IWorkbenchPart)
 */
public void setActivePart(IAction action, IWorkbenchPart targetPart) {
	shell = targetPart.getSite().getShell();
	fTargetPart = targetPart;
}

The method run() finally is called when clicking the action. The interesting parts of this method are getting the snapshot from the wrapper and then getting the UUID.

Another interesting piece of code is to get the ITeamRepository. The code uses the method getOrigin() available from all RTC data objects and gets the team repository of the selected element. Keep in mind, the client could be connected to several different repositories and we want the one that contains the selected element. Once we have the ITeamRepository, it is possible to get client libraries needed for the areas of the API.

The code would do something to the selected element, or search for related elements and then perform some action with it. This code is left out in the example. The example shows a dialog window coming up on the end of the action or in case an exception occurred.

/**
 * @see IActionDelegate#run(IAction)
 */
public void run(IAction action) {
	IProgressMonitor monitor = new NullProgressMonitor();
	IBaselineSet snapshot = fSelection.getSnapshot();
	String snapshotUUID = snapshot.getItemId().getUuidValue();
	try {
		fTeamRepository = (ITeamRepository) snapshot.getOrigin();
		fWorkItemClient = (IWorkItemClient) fTeamRepository
				.getClientLibrary(IWorkItemClient.class);
		fAuditableCommon = (IAuditableCommon) fTeamRepository
				.getClientLibrary(IAuditableCommon.class);
			performAction(snapshotUUID, monitor);
		MessageDialog.openInformation(
				shell,
				"SnapshotMenuContribution",
				"My Action was executed on Snapshot: '"
						+ snapshot.getName() + "' UUID [" + snapshotUUID
						+ "]");
		} catch (Exception e) {
		MessageDialog.openError(shell, "SnapshotMenuContribution",
				e.getMessage() + "\nMy Action was executed on Snapshot: '"
						+ snapshot.getName() + "' UUID [" + snapshotUUID
						+ "]");
	}
}

Some interesting code we found during our work on Jazz.net is how to open a work item editor for a work item in the UI.

The code looks like this:

while (results.hasNext(null)) {
	IResult result = (IResult) results.next(null);
	IWorkItem workItem = fAuditableCommon.resolveAuditable(
			(IAuditableHandle) result.getItem(),
			IWorkItem.SMALL_PROFILE, monitor);
	workItemsFound += workItem.getId() + " ";
	WorkItemUI.openEditor(fTargetPart.getSite().getPage(), workItem);
}

The code runs on a query result as described in the posts Using Expressions For Automation or Using Work Item Queries For Automation and opens the work items found in the Eclipse UI.

Summary

The example above shows how you can add menu contributions for RTC objects in Eclipse and also, how to get the information needed. Interestingly the menu contribution also works in the Snapshot editor context menu.

Menu addition in the snapshot editor

Menu addition in the snapshot editor

As always, the code is very simple and has very few error handling implemented. You want to improve on that. However, I hope the example is useful for others out there trying to extend RTC.

Delivering Change Sets and Baselines to a Stream Using the Plain Java Client Libraries


How does delivering change sets from a repository workspace work and how can I create baselines and deliver them?

This post shows some of the Client API used to do this kind of operations.

As described in the articles

  1. Deploying Templates and Creating Projects using the Plain Java Clients Library
  2. Managing Workspaces, Streams and Components using the Plain Java Client Libraries
  3. This post
  4. Extracting an Archive Into Jazz SCM Using the Plain Java Client Libraries

I want to automatically set up a project, manage the streams and components and seed the SCM system with SCM data.

The last posts show how to create and manage the project area and how to manage the components, streams and repository workspaces. What is left is to extract some kind of archive file and share the files with a component in a repository workspace using Jazz SCM. Then it is necessary to do some SCM operations on the repository workspace.

This post explains how the SCM operations on the repository workspace and stream work. Specifically, how to

  1. Deliver outgoing change sets from a repository workspace to a stream
  2. Baseline the state of a component, in the repository workspace
  3. Deliver the baseline to the stream
  4. Set a component of the repository workspace to a specific baseline

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.

If you just get started with extending Rational Team Concert, or create API based automation, start with the post Learning To Fly: Getting Started with the RTC Java API’s and follow the linked resources.

You should be able to use the following code in this environment and get your own automation or extension working.

To keep it simple this example is, as many others in this blog, based on the Jazz Team Wiki entry on Programmatic Work Item Creation and the Plain Java Client Library Snippets. The example in this blog shows RTC Client API.

Deliver Change Sets and Baselines

Continuing in the run method of Deploying Templates and Creating Projects using the Plain Java Clients Library we now want to extract some data from an archive file, add the code Jazz SCM and manage the changes and baselines.

This happens in the code below. The code only shows changes to one component. The same mechanism can be used for other components however and we grab the component we want from the map components we created in the last post.

As you can see the code below basically delegates the work to another method exctractToComponentBaseline(). For the first step it remembers the baseline that was created. It uses this baseline at the end, to set the current baseline for the component in the repository workspace to the first baseline created. This allows the user to load the repository workspace and begin with the first version of the projects. Later the user can accept incoming baselines, that are already delivered to the stream, to discover the code changes made for the next labs.

public static void main(String[] args) throws Exception {
.
.
.
.

	IBaselineConnection lab2Baseline = exctractToComponentBaseline(
			teamRepository, workspace, extensionStream,
			components.get("Component 2"),
			"./LabCode/Lab2Code.zip", "Share Projects - Lab 2 Code",
			"Lab 2 Code", "Lab 2 Code", monitor);

	exctractToComponentBaseline(teamRepository, workspace, extensionStream,
			components.get("Component 2"),
			"./LabCode/Lab3Code.zip", "Share Projects - Lab 3 Code",
			"Lab 3 Code", "Lab 3 Code", monitor);

	exctractToComponentBaseline(teamRepository, workspace, extensionStream,
			components.get("Component 2"), 
			"./LabCode/Lab4Code.zip", "Share Projects - Lab 4 Code",
			"Lab 4 Code", "Lab 4 Code", monitor);

	exctractToComponentBaseline(teamRepository, workspace, extensionStream,
			components.get("Component 2"),
			"./LabCode/Lab5Code.zip", "Share Projects - Lab 5 Code",
			"Lab 5 Code", "Lab 5 Code", monitor);

	setActiveBaseline(workspace,
			components.get("Component 2"),
			lab2Baseline, monitor);
	return result;
}

The interesting code is used in exctractToComponentBaseline() below. The first step is to extract an archive file into a component in the repository workspace. The extraction process creates a change set with all the folder and file creations and changes. How that works is going to be discussed in the final post of this series to keep the tension up, and because it is quite a lot of code.

The next lines of code look quite innocent, but I had a lot of trouble to get to the point that it was working for me.

In the first step after extracting the archive, the code uses the IWorkspaceConnection.compareTo() API to compare the repository workspace to the target stream. It uses the flag WorkspaceComparisonFlags.CHANGE_SET_COMPARISON_ONLY to only compare the change stets. The 3rd parameter could be used to exclude components on the workspace from the compare operation. This is not used in this example and an Collections.EMPTY_LIST is passed instead. The effort to create the exclusion list would increase the code complexity but does not really save that much time in this example. If this is used with a lot of components with a lot of files, it would be worth spending the effort.

The result of the compare operation is a IChangeHistorySyncReport which is used in the next step to deliver the change sets.

To deliver the changes the code uses the IWorkspaceConnection.deliver() API of the repository workspace. The call provides the deliver target stream, the IChangeHistorySyncReport from the compare, an empty list of baselines and the outgoing change sets from the IChangeHistorySyncReport.

This operation also adds the target stream (workspace connection) as a flow target. You can access the flow targets using IWorkspaceConnection.getFlowTable() and use this information to get a target in other scenarios.

The next step is to create a new baseline on the repository workspace. This is done using the IWorkspaceConnection.createBaseline() API call. This call can be used with exactly on component, which is what we want in this example. The name of the baseline and a comment for the baseline are also provided in the call.

If we wanted to create a snapshot IWorkspaceConnection.createBaselineSet() would have been the AI of our choice. This creates a baseline set across several components (and can exclude components).

The next steps are similar to the delivery of the change sets. First, do a compare() of the repository connection and the target, then do a deliver() with the change set sync report and the outgoing baselines and change sets from the sync report.

It would have been possible to do the baseline creation and the deliver in one step, in our case. I choose to keep it separate, because it shows that you have to provide outgoing change sets as well as outgoing baselines from the sync report to be able to deliver a baseline. I contemplated over many IllegalArgumentExceptions to get to this insight.

private IBaselineConnection exctractToComponentBaseline(
		ITeamRepository teamRepository, 
		IWorkspaceConnection repoWorkspace,
		IWorkspaceConnection targetStream,
		IComponentHandle component, 
		String archiveFileName,
		String changeSetComment, 
		String baselineName,
		String baselineComment, 
		IProgressMonitor monitor) throws Exception, TeamRepositoryException {

	// Extract the archive file to the component in the repository workspace
	System.out.println("Extracting...");
	ArchiveToSCMExtractor extract = new ArchiveToSCMExtractor();
	extract.extractFileToComponent(archiveFileName, teamRepository, 
			repoWorkspace, component, changeSetComment, monitor);

	// Compare the repository workspace with the stream to find the changes
	// Deliver the change sets
	System.out.println("Comparing Change Sets...");
	IChangeHistorySyncReport changeSetSync = repoWorkspace.compareTo(
			targetStream,
			WorkspaceComparisonFlags.CHANGE_SET_COMPARISON_ONLY,
			Collections.EMPTY_LIST, monitor);
	System.out.println("Deliver Change Sets...");
	repoWorkspace.deliver(targetStream, changeSetSync,
				Collections.EMPTY_LIST,
				changeSetSync.outgoingChangeSets(component), monitor);

	// Create a baseline and compare the repository workspace with the
	// stream to find the changes and deliver the baselines
	System.out.println("Create Baseline...");
	IBaselineConnection baseline = repoWorkspace.createBaseline(
			component, baselineName, baselineComment, monitor);
	System.out.println("Comparing Baselines...");
	IChangeHistorySyncReport baselineSync = repoWorkspace.compareTo(
			targetStream,
			WorkspaceComparisonFlags.INCLUDE_BASELINE_INFO,
			Collections.EMPTY_LIST, monitor);

	// Deliver the baselines
	System.out.println("Deliver Baselines...");
	repoWorkspace.deliver(targetStream, baselineSync,
			baselineSync.outgoingBaselines(component),
			baselineSync.outgoingChangeSets(component), monitor);
	System.out.println("Extracting successful...");

	return baseline;
}

After extracting, sharing, creating baselines and delivering to setup the scenario right, the repository workspace needs to be set to a specific older baseline. This is done in setActiveBaseline().

The interesting part here is that it is necessary to provide a list of component operations (implementing IComponentOp). The component operations can be created using the IFlowNodeConnection.componentOpFactory() API which is inherited by IWorkspaceConnection.

The code replaces the baseline by an older one, created earlier in the process. The replaceComponent() operation takes the component, the baseline connection and a parameter that allows to calculate detailed item update reports. We don’t need this, because we don’t want to actually do anything with the baseline in our case.

private void setActiveBaseline(IWorkspaceConnection workspace,
		IComponentHandle component, IBaselineConnection lab2Baseline,
		IProgressMonitor monitor) throws TeamRepositoryException {
	workspace.applyComponentOperations(Collections.singletonList(workspace
			.componentOpFactory().replaceComponent(component, 
					lab2Baseline, false)), monitor);
}

Summary

The code in this post basically shows how to work with change sets and baselines, to manage workspace connections, deliver change sets and baselines and to replace a component with a specific baseline.

The last step needed is to understand how it is possible to extract an archive (or any other data source) into Jazz SCM. This is described in the post Extracting an Archive Into Jazz SCM Using the Plain Java Client Libraries.

As always, I hope that sharing this code helps users out there, with a need to use the API’s to do their work more efficient.

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!