Some Community Extensions for RTC


Here are some interesting extensions provided by members of the community that I have seen recently. I thought I should share them and give them a thumbs up as well.

Guido and his colleagues have created these extensions and share them here:

I have seen the Timebox Planning View and it is incredible. I will have a look at JAM as well.

Yasuyuki created this for a customer in Japan and shares it here

  • RTC Work Item Numbering – an extension that allows to create a custom work item number that is consecutive by type

I have added the links to my Interesting Links page as well.

Posted in Jazz, RTC Automation, RTC Extensibility | Tagged , , , | Leave a comment

The Day The JBE Stood Still


Last week I realized my Jazz Build Engines for my test environments where not working any longer. Since I need them to work, I tried to fix this. It turned out that this was a major effort and I thought I should share what I found and how I was able to fix it – hopefully forever.

The Problem

The initial symptom was that the JBE process shells I usually see running, terminated immediately.

My initial thought was, that recently there were a lot of updates running on my machine to remove all Java versions with security issues and installing new versions. I suspected an issue with the Java version running.

Finally this proved to be true, however, during the attempt to come up with a working fix, I ran into a variety of other issues, I will summarize below.

The Solution

The final solution for me was to provide a working JRE to the Jazz Build Engine. To avoid it getting removed during security upgrades and to make sure it is compatible, I used a JRE that comes with the Jazz products.

One important detail is, that the JBE is currently a 32 bit application. If you use a 64 bit JRE with it, a variety of error symptoms crop up.

So the solution for me is, to

  1. Download the Plain ZIP package for the Eclipse client – make sure to download the 32 bit version
  2. Extract the download to some temporary location
  3. Locate the folder jazz\client\eclipse\jdk in the extracted folder
  4. Copy the folder jdk and all of its content into some folder you want to keep; for example: C:\CLMJava32
  5. Edit the file jbe.ini in \jazz\buildsystem\buildengine\eclipse and add the following lines where indicated
    -vm
    C:\CLMJava32\jdk\jre\bin

Your jbe.ini file should now look similar to the image below:

jbe.ini

You can probably get away with just the jre/bin folder, but I wanted the full JDK for other uses.

If you restart the JBE processes, everything should work.

Problems With the JBE and Wrong JRE’s

During my attempts to fix it I ran into various other issues, which I will try to list below.

  • The JBE shell just terminates
    This seems to happen due to incompatible JRE e.g. a newer version and for 64 bit JRE
  • The build process finishes with errors in the build result like
    !ENTRY org.eclipse.core.filesystem 1 1 2015-04-07 14:18:52.303
    !MESSAGE Could not load library: localfile_1_0_0.dll.  This library provides platform-specific optimizations for certain file system operations.  This library is not present on all platforms, so this may not be an error.  The resources plug-in will safely fall back to using java.io.File functionality.
    !STACK 0
    java.lang.UnsatisfiedLinkError: C:\CLM2014\5.0.2\jazz\buildsystem\buildengine\eclipse\configuration\org.eclipse.osgi\bundles\184\1\.cp\os\win32\x86\localfile_1_0_0.dll: Can't load IA 32-bit .dll on a AMD 64-bit platform
    

    This seems to happen due to using a 64 bit JRE; Errors can be found in the build logs or in the log of the build engine workspace; the log is usually located in \jazz\buildsystem\buildengine\eclipse\workspace\.metadata

  • The build process finishes with errors in the build result like
    2015-04-07 14:23:23 [Jazz build engine] Fetching files to fetch destination "C:\somefolder\JKEBuild\I20150407-1423" ... com.ibm.team.build.internal.scm.SourceControlUtility$2: Status ERROR: com.ibm.team.filesystem.client code=0 Cannot create sandbox at C:\somefolder\JKEBuild\I20150407-1423 because one already exists at C:\somefolder\ null

    This seems to happen due to using a 64 bit JRE

  • The build process finishes with errors in the build result like
    Failed to load the JNI shared library "C:\CLM2014\5.0.2\jazz\client\eclipse\jdk\jre\bin\j9vm\jvm.dll".

    This seems to happen due to using a 64 bit JRE

  • The build process is abandoned

    This was the toughest issue; the builds where abandoned with the error message below

    2015-04-07 14:18:51 [Jazz build engine] Deleting fetch destination "C:\CLM2014\5.0.2\JazzTeamServer\server\JKEBuild\I20150407-1418" before fetching ...
    2015-04-07 14:18:51 [Jazz build engine] Fetching files to fetch destination "C:\CLM2014\5.0.2\JazzTeamServer\server\JKEBuild\I20150407-1418" ...
    com.ibm.team.build.common.TeamBuildStateException: Unable to "complete" build activity with label "_QCYgmd0gEeSrJ4S35NXZHw" because the build with ID "_P_aFEd0gEeSrJ4S35NXZHw", build definition ID "jke.dev", label "I20150407-1418" is in the "INCOMPLETE" state.
    	at com.ibm.team.build.internal.service.TeamBuildService.createIllegalBuildStateForActivityException(TeamBuildService.java:1599)
    	at com.ibm.team.build.internal.service.TeamBuildService$22.run(TeamBuildService.java:1471)
    	at com.ibm.team.build.internal.service.AbstractTeamBuildService.runAsRetryable(AbstractTeamBuildService.java:435)
    

    The reason for this finally was that the JBE processes that terminated left daemon Java processes, these processes tried to consume the build requests as well, which resulted in the build being abandoned

The abandoned builds cost me the most time, because it is completely hidden why this happens. Looking into the process list finally revealed the reason.

Summary

Providing a stable and compatible JRE is important to keep the build engines running. The solution above should work on all architectures. Searching the internet I did not find a lot of hints how to solve this, so I hope providing this post will come in handy and helps users that run into the same problem.

Posted in Jazz | Leave a comment

The Work Item Time Tracking API


Recently I was contacted by a colleague about how to work with work item time tracking, available in the Formal Project Management Process. This is an area of the work item API I had not touched so far. So I decided to have a look at it and share the result.

License and Getting Started

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.

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.

Download

The code is available for download here.

Work Item Time Tracking

Time Tracking can be enabled in projects based on the Formal Project Management Process. If enabled the user can no longer edit the time spent attribute of work items. Instead the user adds time sheet entries with hours worked on specific time codes on a special tab of the work item editor. The information will be summed up and the total work time will be automatically set in the work item’s time spent attribute.

The image below shows the time tracking data.

Time TrackingTab

The time spent attribute is read-only and calculated from the data above.

Time TrackingTime Spent

In certain circumstances it would be desirable to be able to access the data using the API. The question is, how is the data stored? How can it be read and, most important, how can it be updated using the API?

The Time Tracking API

There is a short explanation in the wiki about how the time tracking API works in general. However, this leaves out some very important details.

Time sheets are stored in RTC as special objects of type ITimeSheetEntry. These objects are associated to the work item they belong to using a special link type using the end point WorkItemEndPoints.WORK_TIME.

It is easy enough to follow the Wiki Entry and create a new time sheet entry.

However, I found out the hard way that it is not as easy to update an ITimeSheetEntry.

The reason is, that the work item API does not detect an update to a time sheet entry alone. So just updating the entry and then saving the work item will not work. The work item API will detect that no change has been done to the work item itself and not perform a save operation. Since the work item save also triggers the save of the dependent work item data, the save for the time sheet entry is also not performed. Adding a new relationship will also not work as you will end up with multiple relationships to the same item.

The only way to update the time sheet data is to force an update to the work item by setting the duration value. This way the API detects that it has to save the work item as well as the time sheet entries as dependent items.

The code I finally ended up with looks like below.

public void updateOrCreateTimeSheetEntry(WorkItemWorkingCopy workingCopy,
		ITimeCode timeCode, Timestamp startDateTimeStamp,
		Duration workDuration, Identifier workType,
		IProgressMonitor monitor) throws TeamRepositoryException {

	// set the active work item from the working copy
	setWorkItem(workingCopy.getWorkItem());

	// Find a matching time sheet if it exists.
	ITimeSheetEntry timeSheet = findTimeSheetEntry(timeCode,
			startDateTimeStamp, monitor);
	if (timeSheet == null) {
		// There is no time sheet for this entry
		// Create a new one and create the link
		timeSheet = createTimeSheet();
		workingCopy.getReferences()
				.add(WorkItemEndPoints.WORK_TIME,
						IReferenceFactory.INSTANCE
								.createReferenceToItem(timeSheet));
		// Add the entry to the map to hold the data
		addEntry(timeSheet, monitor);
	} else {
		// There is a time sheet, we need to update it
		// Get the workingCopy of the time sheet
		timeSheet = (ITimeSheetEntry) timeSheet.getWorkingCopy();
		// remove the time spent from current time
		setTotalDuration(new Duration(getTotalDuration().longValue()
				- timeSheet.getTimeSpent().longValue()));
	}

	// Set the new data
	timeSheet.setStartDate(startDateTimeStamp);
	timeSheet.setTimeCodeId(timeCode.getTimeCodeId());
	// TODO: If I leave this out it fails....
	timeSheet.setTimeCode(timeCode.getTimeCodeLabel());
	timeSheet.setTimeSpent(workDuration);
	timeSheet.setWorkType(workType);
	// add the new time back
	setTotalDuration(getTotalDuration().add(workDuration));
	// Update the value
	// Note: it is important to set the duration value, of the work item
	// otherwise the work item is not marked as dirty and in need to update
	// in the repository and the save process will not save the time sheet
	getWorkItem().setDuration(getTotalDuration().longValue());
	workingCopy.getDependentItems().add(timeSheet);
}

How does the code above work?

First the code tries to find a time sheet entry for the time code and the date that is given.

If it can not find a time sheet entry, a new entry is created and the required reference is also created and added to the work item references. This sets the work item to changed already.

If a time sheet entry can be found, the code gets a working copy that can be modified. The total duration of all the time spent on time sheets is reduced by the amount of time spent on that time sheet (as if the time sheet duration was set to zero).

The data on the tine sheet is then finally set to the current data. In addition the new over all duration is calculated and the result set to the work item’s duration.

Finally the time sheet is added to the work item’s dependent items. Updating the work items duration and adding the time sheet as dependent results in both being saved if the work item is saved.

A few remarks on the code.

  • I did this with RTC 4.0.1
  • For whatever reason the method setTimeCode() is deprecated. However, if I run the code against a 5.0.2 server and don’t use the method, the entry does not show up
  • To create the start date I use SimpleDateFornat and provide the data only down to the year, month and date, this way the entry is automatically created with a timezone and time that works for me
  • The work type is either provided and then used; otherwise the work item type is used as work type

If the start date is created with a wrong timezone and time, the entry might not show up in the UI. To fix that, look at the data used in your repository and change the creation of the start data accordingly.

The TimeTrackingHelper

To make it easier to use the code above, it is part of a class called TimeTrackingHelper. This helper class implements loading existing time sheet entries into a hash map structure that makes searching for the entry easier. The hash map should allow to iteratively use the helper on the same class if required. If the work item changes, the hash map is built up again.

The time tracking helper has code to convert text/string values to the elements needed for a time sheet. It also implements a pattern that allows to iterate the hash map and run an interface on each entry to print the data or do something else with it.

The example below shows how to use the helper in a WorkItemOperation to add a time sheet entry.

// TimeTracking
private static class ModifyTimeTracking extends WorkItemOperation {

	private String fTimeCode = null;
	private String fStartDate = null;
	private String fWorkHours = null;
	private String fWorkType;

	public ModifyTimeTracking(String timeCode, String startDate,
			String workHours, String workType) {
		super("Modify TimeTracking", IWorkItem.FULL_PROFILE);
		this.fTimeCode = timeCode;
		this.fStartDate = startDate;
		this.fWorkHours = workHours;
		this.fWorkType = workType;
	}

	@Override
	protected void execute(WorkItemWorkingCopy workingCopy,
			IProgressMonitor monitor) throws TeamRepositoryException {
		TimeTrackingHelper helper = new TimeTrackingHelper();
		helper.updateTimeTrackingInfo(workingCopy, fTimeCode, fStartDate,
				fWorkHours, fWorkType, monitor);
		helper.printTimeSheets(workingCopy.getWorkItem(), monitor);
	}
}

The interesting part is in the method execute(), where the helper is instantiated first and then used to update (or create) a time sheet.

The code below shows how the helper can be used to just print (or access) the time sheet data.

	int id = new Integer(idString).intValue();

	IWorkItemClient workItemClient = (IWorkItemClient) teamRepository
			.getClientLibrary(IWorkItemClient.class);
	IWorkItem workItem = workItemClient.findWorkItemById(id,
			IWorkItem.FULL_PROFILE, monitor);
	System.out.println("Accessing work item: " + workItem.getId() + ".");
	TimeTrackingHelper helper = new TimeTrackingHelper();
	helper.printTimeSheets(workItem, monitor);
	System.out.println("Accessed work item: " + workItem.getId() + ".");

The code for the TimeTrackingHelper the example class to modify the time tracking data of a work item called ModifyWorkItemTimeTracking as well as the class to just print the data AccessTimeTracking is shipped with the download.

Summary

This post shows how time tracking data is managed using the API. As always I hope this helps someone out there to get their job done more efficient.

Posted in Jazz, RTC Automation, RTC Extensibility | Tagged , , , | Leave a comment

A RTC WorkItem Command Line Version 2.2


Creating links is not easy. Many things can go wrong.  Testing by a user showed that there was an issue with links between work items and build results. I found that I got the link direction wrong. I fixed that. Here is the updated source code.

Related posts

License and Getting Started

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.

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.

Download

You can download the latest version here:

Setup and Usage

Follow the description in A RTC WorkItem Command Line Version 2 and in A RTC WorkItem Command Line Version 2.1. Check the README.txt which is included in the downloads.

Posted in Jazz | Leave a comment

A RTC WorkItem Command Line Version 2.1


In the last post Extending the WorkItem Command Line With New Commands we added a new command to the WorkItemCommandLine to implement a solution for attribute data migration as explained in Workaround: Migrate from Rational Team Concert 3.X string attribute used as multi-select lists to RTC 4.X enumeration lists.  Here is the updated source code.

What’s new?

This version of the RTC WorkItem Command Line has a new command that allows to migrate RTC 3.x multi-select enumeration data stored in string type attributes, containing Enumeration Literals separated by comma ‘,’ into the new EnumerationList attribute type.

Related posts

License and Getting Started

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.

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.

Download

You can download the latest version here:

Setup

Follow the setup description in A RTC WorkItem Command Line Version 2

The Syntax of the new Command

WCL uses the following syntax for the new command:

wcl -migrateattribute {/ignoreErrors} repository=RepoitoryURI user=user password=password projectArea=ProjectAreaName workItemType=typeID { id=WorkItemID } sourceAttributeID=AttributeID targetAttributeID=AttributeID

Where the sourceAttributeID must be the ID of a work item attribute of any string type. The content must be Enumeration Literal ID’s, separated by comma stored by a RTC 3.x muti-select EnumerationList presentation. In RTC 3.x the attribute type used to store the enumeration literal ID’s was a small or medium string. The command only checks for any string.

The targetAttributeID must be the ID of a work item attribute of type EnumerationList, where the enumeration and thus the literal ID’s must match the enumeration used in the source attribute.

If the attribute types don’t match the required types or the finds literal ID’s that don’t match a literal in the target enumeration, the work item is not changed. The user requires the permission to read and write the work items.

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

Below are examples how to launch the WorkItem command Line. The first example runs the migration for all work item of a specific type in a project area.

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

The second example is used for testing and only works on the work item with the specified ID.

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
Posted in Jazz, RTC, RTC Automation | Tagged | 3 Comments

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.

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 and Getting Started

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.

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.

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<? extends ILiteral> enumeration = getWorkItemCommon()
					.resolveEnumeration(fTargetAttribute, monitor);

			List<Object> results = new ArrayList<Object>();
			for (String literalID : values) {
				if (literalID == "") {
					// Nothing to do
					continue;
				}
				Identifier<? extends ILiteral> 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<? extends ILiteral> getLiteralEqualsIDString(
			final IEnumeration<? extends ILiteral> enumeration,
			String literalIDString) throws TeamRepositoryException {
		List<? extends ILiteral> literals = enumeration
				.getEnumerationLiterals();
		for (Iterator<? extends ILiteral> 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.

Posted in Jazz, RTC, RTC Automation | Tagged , , , | Leave a comment

The WorkItem Command Line Explained


This post explains how the WorkItem Command Line works. It explains its structure and the main classes. This should allow users to extend the capabilities the code and add new commands or extend the current commands.

Please note, as this is work in progress, things might change slightly in future versions, however the general structure should persist.

Related posts

License and Getting Started

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.

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.

Importing The Project

Download the code from the post A RTC WorkItem Command Line Version 2. The file with the source code is named WorkItemCommandLine_Project-Vx-YYYYMMDD.zip. The x represents the version number and is followed by the date it was created. The file is an exported Eclipse project.

The project expects the Eclipse workspace to be set up as described in the posts Setting up Rational Team Concert for API Development. It requires the SDK to be set up as well as the Plain Java Client Libraries. The SDK is needed, because the project is a Plugin Project. this is done to be able to use the Eclipse Plugin Development Environment (PDE) to look at the API source code. The Plain Java Client Libraries are needed to run the code a Java application.

Use the Eclipse import File>Import. In the wizard window select “Existing Projects into Workspace” in the section General. Click Next and chose the option “Select archive file”, browse to the file you downloaded and select it. Make sure you see the project com.ibm.js.team.workitem.commandline selected and press Finish to start the import.

After the import you should see the project in your workspace. You should see no errors in the project. If you see errors, the most likely reasons for that are:

  1. The SDK is not set up correctly and the classes can not be resolved
  2. The SDK version is prior to RTC 4.0.1
  3. The Plain Java Client Libraries are not installed or the User Library has a different name

The first two will show in the plugin.xml and the manifest file. Setup the SDK correctly, or change the minimal versions needed in the dependencies.

The third will show as an error in the the build path. Define a user library named PlainJavaApi as explained in  Setting up Rational Team Concert for API Development or remove the existing user library entry and add you own. Make sure the dependency order of SDK and user library are correct as explained in  Understanding and Using the RTC Java Client API.

In case you have other errors you should search the internet for a solution.

Explore the Project

You can now explore the project. The folder structure is shown below.

WCL Project Overview

There are the following files and folders

  • src – contains the source code files.
  • build – contains a jardesc file to build a jar file for packaging
  • Launches – contains launch files used for testing
  • License – contains the license files
  • scripts – contains the script files used to start WCL, as well as a file with help information
  • the root contains a readme file, explaining how to build a releasable version of WCL, scripts used to start WCL in the development setup and a test file for upload attachment tests.

The Source Files

The image below shows the structure of the source code.

Source StructureThe package com.ibm.js.team.workitem.commandline contains the class WirkitemCommandLine, which has the main method to call WCL. The class OperationResult is used to pass result information. This is necessary, since the code could run in RMI mode and the output needs to be transferred to the RMI client. This class needs to support serializing in order to pass the result back. IWorkItemCommandLineConstants contains various constants used by WCL.

The package com.ibm.js.team.workitem.commandline.commands contains the classes that implement the currently available commands. CreateWorkItemCommand creates a work item of a specific type in a specific project area and sets the attributes as provided. PrintTypeAttributesCommand prints the attributes of a specific work item type in a specific project area. UpdateWorkItemCommand finds a work item and updates its attribute values.

The package com.ibm.js.team.workitem.commandline.framework contains a basic framework that is used by commands that are implemented in WCL. The main class requires the interface IWorkItemCommand to run the command. I ended up using this kind of framework, because all commands required some kind of parameters. The command should be able to define the parameters needed. The commands also require to do error handling. To interact with the RTC repository commands also need to login. The framework handles all the common activities and allows to create new commands without having to redevelop all this.

The class AbstractCommand implements the interface IWorkItemCommand and leaves some methods abstract that extending classes need to implement.

The class AbstractTeamRepositoryCommand adds a login to the team repository and the class AbstractWorkItemModificationCommand adds a WorkItemOperation to perform the changes to the work item. WorkItemCommandLineException is the exception class that is used to wrap other exceptions and thrown in case of unrecoverable errors.

The package com.ibm.js.team.workitem.commandline.helper contains helper classes. The class DevelopmentLineHelper is from another blog post Handling Iterations – Automation for the “Planned For” Attribute. It allows to find development lines and iterations on a development line. WorkItemHelper implements modifying work item attribute modification. Most of the RTC API related code is in there. WorkItemTypeHelper helps with printing the attribute information for a work item type.

The package com.ibm.js.team.workitem.commandline.parameter contains classes that implement all the parameter handling needed. The class Parameter is used to describe a parameter, if it is required, if it was already consumed, if it is a switch and the like. ParameterIDMapper defines a list of aliases that can be used instead of an attribute ID. You can add your own aliases that can be used for convenience. ParameterList represents a list of parameters. The class ParameterManager manages a parameter list and provides the central access to the parameters. The class ParameterParser is used to parse the parameters passed from outside and store them in a parameter list.

The package com.ibm.js.team.workitem.commandline.remote contains the remote interface IRemoteWorkItemOperationCall that is used in RMI mode.

The package com.ibm.js.team.workitem.commandline.utils contains some utility classes (providing static methods as interfaces). The class ProcessAreaUtil allows to search process areas. The class SimpleDateFormatUtil helps with conversion of timstamps from and to a string representation.

 How The Code Works

The main method of the WorkitemCommandLine basically instantiates the class and then calls the method run(). We will look at that method later.

/**
 * The main entry point into the work item commandline
 * 
 * @param args
 *            - the arguments to be used by the commandline
 * @throws RemoteException
 */
public static void main(String[] args) {

	OperationResult result = new OperationResult();
	System.out.println("WorkItemCommandLine Version "
			+ IWorkItemCommandLineConstants.VERSIONINFO + "\n");
	WorkitemCommandLine commandline;
	try {
		commandline = new WorkitemCommandLine();
		result = commandline.run(args);
	} catch (RemoteException e) {
		result.appendResultString("RemoteException: " + e.getMessage());
		result.appendResultString(e.getStackTrace().toString());
	}
	System.out.println(result.getResultString());
	if (TeamPlatform.isStarted()) {
		TeamPlatform.shutdown();
	}
	if (!isServer()) {
		// If I am not in server mode, I need to exit and return success or
		// failure
		if (result.isSuccess()) {
			// If the operation was unsuccessful, terminate with an error
			System.exit(0);
		}
		System.exit(1);
	}
}

The operation run() will return a result if it terminates. The information in this result is used to create the exit code to terminate the call.

In case this WCL is started as RMI server, the process can not terminate with System.exit(). It needs to persist registered to the RMI registry. The static method isServer() is used to communicate this information.

The method run() parses the parameters passed. It then checks if it is supposed to run as RMI server or as RMI client. If that is the case it starts the RMI server mode or, uses RMI to call the server as client. If this is a normal run, it calls runCommands() with the parameters that have been parsed.

If started as RMI server, the method startRMIServer() is used to initialize RMI and to register the class to the registry. The method runOperation() is basically the interface that is used to run the command on the server and is called by RMI clients. The method runOperation() parses the parameters and calls runCommands() as well.

The method runCommands() really executes the command requested in the parameters.  The first steps it does is to initialize the data it needs. Then it runs addSupportedCommands() to add the commands that are available.

/**
 * 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())));
}

Now the method runCommands() gets the command from the Parameter Manager. If there is a command string, it gets the class that implements the command. If there is a command registered for this command string, runCommand() calls the command to validate if the required parameters for it to run are available. If this is the case, runCommand() calls the command and returns the result back.

In all other cases runCommand() prepares a result error and also uses the method helpGeneralUsage() to print a help for the command.

Adding Commands to the WorkItemCommandLine

It is easy to add new commands to the WorkitemCommandLine. You need to implement a new command and add a new entry for it in the method addSupportedCommands().

How Commands Work

Commands have to implement the IWorkItemCommand interface. You should pick one of the abstract classes in the framework and extend them. This makes sure the basic workflow will work. If you command needs to create or modify a work item based on property values, use the class AbstractWorkItemModificationCommand. If you only need to have a repository connection, use the class AbstractTeamRepositoryCommand. In both cases all you need to do really is to override and implement the methods required. There are three things that need to be there.

In the method getCommandName() you need to return the name of the command you implement.

@Override
public String getCommandName() {
	return IWorkItemCommandLineConstants.COMMAND_CREATE;
}

If your command needs additional parameters, override the method setRequiredParameters(). Call the method of the superclass to have it add its required parameters and add your parameters. Here is an example

/*
 * (non-Javadoc)
 * 
 * @see com.ibm.js.team.workitem.commandline.framework.
 * AbstractWorkItemCommandLineCommand#setRequiredParameters()
 */
public void setRequiredParameters() {
	super.setRequiredParameters();
	// Add the parameters required to perform the operation
	// getParameterManager().syntaxCommand()
	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().syntaxAddSwitch(
			IWorkItemCommandLineConstants.SWITCH_ENABLE_DELETE_ATTACHMENTS);
	getParameterManager().syntaxAddSwitch(
			IWorkItemCommandLineConstants.SWITCH_ENABLE_DELETE_APPROVALS);
}

Parameters added with syntaxAddRequiredParameter() will be assumed to be required. If they are not available the command line will show an error during the parameter validation. The error message is automatically created from the parameter information provided here.

Finally you have to override and implement the method process() to implement the command.

/*
 * (non-Javadoc)
 * 
 * @see com.ibm.js.team.workitem.commandline.framework.
 * AbstractWorkItemCommandLineCommand#process()
 */
@Override
public OperationResult process() throws TeamRepositoryException {
	// 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());
	// Create the work item
	createWorkItem(workItemType);
	return this.getResult();
}

To complete the code of this command, here is the method that creates the work item and uses the parameters to update the attributes.

/**
 * Create the work item and set the required attribute values.
 * 
 * @param workItemType
 * @return
 * @throws TeamRepositoryException
 */
private boolean createWorkItem(IWorkItemType workItemType)
		throws TeamRepositoryException {

	ModifyWorkItem operation = new ModifyWorkItem("Creating Work Item");
	this.setIgnoreErrors(getParameterManager().hasSwitch(
			IWorkItemCommandLineConstants.SWITCH_IGNOREERRORS));
	IWorkItemHandle handle;
	try {
		handle = operation.run(workItemType, getMonitor());
	} catch (TeamOperationCanceledException e) {
		throw new WorkItemCommandLineException("Work item not created. "
				+ e.getMessage(), e);
	}
	if (handle == null) {
		throw new WorkItemCommandLineException(
				"Work item not created, cause unknown.");
	} else {
		IWorkItem workItem = getAuditableCommon().resolveAuditable(handle,
				IWorkItem.SMALL_PROFILE, getMonitor());
		this.appendResultString("Created work item " + workItem.getId()
				+ ".");
		this.setSuccess();
	}
	return true;
}

In case you wonder where the actual work gets done – I wondered looking at it. The line

handle = operation.run(workItemType, getMonitor());

does all the work. By calling it this way, the WorkItemOperation creates the work item. The operation is based upon the code in the class AbstractWorkItemModificationCommand.

In that class, the execute() method is overwritten with this code:

/*
 * This is run by the framework
 * 
 * (non-Javadoc)
 * 
 * @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 {
	// run the special method in the execute.
	// This is called by the framework.
	update(workingCopy);
}

The call to the method update() does the real work. It walks through all the unconsumed parameters in the parameter list – which should contain the attributes and values to be set and applies the changes to the work item.

/**
 * This operation does the main task of updating the work item
 * 
 * @param workingCopy
 *            the workingcopy of the workitem to be updated.
 * 
 * @throws RuntimeException
 * @throws TeamRepositoryException
 */
public void update(WorkItemWorkingCopy workingCopy)
		throws RuntimeException, TeamRepositoryException {

	ParameterList arguments = getParameterManager().getArguments();

	// We use a WorkItemHelper to do the real work
	WorkItemHelper workItemHelper = new WorkItemHelper(workingCopy,
			arguments, getMonitor());

	// Run through all properties not yet consumed and try to set the values
	// as provide
	for (Parameter parameter : arguments) {
		if (!(parameter.isConsumed() || parameter.isSwitch() || parameter
				.isCommand())) {
			// Get the property ID
			String propertyName = parameter.getName();
			// Get the property value
			String propertyValue = parameter.getValue();
			try {
				workItemHelper.updateProperty(propertyName, propertyValue);
			} catch (WorkItemCommandLineException e) {
				if (this.isIgnoreErrors()) {
					this.appendResultString("Exception! " + e.getMessage());
					this.appendResultString("Ignored....... ");
				} else {
					throw e;
				}
			} catch (RuntimeException e) {
				this.appendResultString("Runtime Exception: Property "
						+ propertyName + " Value " + propertyValue + " \n"
						+ e.getMessage());
				throw e;
			} catch (IOException e) {
				this.appendResultString("IO Exception: Property "
						+ propertyName + " Value " + propertyValue + " \n"
						+ e.getMessage());
				throw new RuntimeException(e.getMessage(), e);
			}
		}
	}
}

The Class WorkItemHelper

This class is basically doing all the work related to modifying work item data. The helper needs to be instantiated. Then the method updateProperty() can be called.

public void updateProperty(String propertyID, String value)
		throws TeamRepositoryException, WorkItemCommandLineException,
		IOException {
.
.
.
}

The method checks if the attribute is one of the special ones like the type, or complex attributes such as workflow or state changes, approvals or other pseudo attribute ID’s and handles these if detected. Otherwise it calls the method updateGeneralAttribute() to handle the update.

private void updateGeneralAttribute(ParameterValue parameter,
		List exceptions) throws TeamRepositoryException,
		WorkItemCommandLineException {
.
.
.
}

The method updateGeneralAttribute() checks if this attribute is actually available on the work item. If so it calls getRepresentation() to get a value that can be set for the attribute.

private Object getRepresentation(ParameterValue parameter,
		List exceptions) throws TeamRepositoryException,
		WorkItemCommandLineException {
.
.
.
}

The method getRepresentation() basically is a huge list of checks to narrow down what type the attribute to modify is. If the type is narrowed down, it calls a related methods to parse the input data and to create a value for the attribute, that can be returned and set.

Summary

This post explains how the code works and how you can utilize it to implement your own commands. As always, I hope that helps someone out there.

While creating this post, I realized, that I should have named some of the classes differently. This framework is not only good for a work item command line. This code could be used for any command line. Maybe I will adjust this a bit in later versions, should time permit.

Posted in Jazz, RTC, RTC Automation, RTC Extensibility | Tagged , , , , , , , , , , , | 1 Comment