Adding a check-in participant to Rational Team Concert source control


Rational Team Concert 6.0.4 tries to make the check-in operation available to advisors and follow up actions.

Michael Valenta recently published an article on Adding a check-in participant to Rational Team Concert source control. on Jazz.net which provides details about this.

If you are interested in advisors and follow up actions, especially for SCM, this is an important article. Have a look and give him a thumbs up.

Please make sure to check with the links in the next section if you are just starting with the RTC Java API.

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.

DIY stream naming convention advisors


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

License

The post contains published code, so our lawyers reminded me to state that the code in this post is derived from examples from Jazz.net as well as the RTC SDK. The usage of code from that example source code is governed by this license. Therefore this code is governed by this license. I found a section relevant to source code at the and of the license. Please also remember, as stated in the disclaimer, that this code comes with the usual lack of promise or guarantee. Enjoy!

Just Starting With Extending RTC?

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

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

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

Compatibility

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

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

Download

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

Solution

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

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

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

The implementing classes are shipped with the SDK

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

Looking at the source code can be a great inspiration.

This post shows two advisors

  1. StreamNamingPatternAdvisor
  2. UniqueStreamNameAdvisor

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

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

General Approach

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

imodifystreamoperationdata

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

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

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

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

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

istreamchange

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

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

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

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

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

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

import java.util.Set;

import org.eclipse.core.runtime.IProgressMonitor;

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

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

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

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

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

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

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

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

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

Consolidation

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

Code Structure

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

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

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

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

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

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

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

codestructure

Configuring the precondition

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

configuring

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

fail_unique_name

Summary

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

A component naming convention advisor


Organizations sometimes would like to implement naming conventions for components based on the architecture for example. This post shows a simple example advisor that checks for a naming convention.

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 run with any version that provides the operation ID.

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

Download

The code is included in the download in the post DIY stream naming convention advisors.

Solution

In the last few versions of RTC several operations have been made available for operational behavior. At least since RTC 5.0.2 the operation to modify a component is available with the operation ID com.ibm.team.scm.server.component. This allows to create advisors/preconditions as well as participants/follow up actions that operate on such events. An example shipped with the product is implemented in the class com.ibm.team.scm.service.UniqueComponentNameAdvisor.

There are examples shipped with the product in the SDK that you can look at for more sample code. For example: com.ibm.team.scm.service.internal.process.advisors.UniqueComponentNameAdvisor

The code below shows a very basic example how to test the component name for some simple naming schema.

The important information to take away is that the information about the save operation is provided in a special interface IComponentModificationData which allows access to the type of the operation, to old and new properties and to the component directly.

componentmodification

So it is possible to find out what operation is done on the component and based on that look at the properties that the component has.

The code below does exactly that. It checks what operation is going on and then takes the new name of the component and checks it.

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

import org.eclipse.core.runtime.IProgressMonitor;

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

/**
 * An example advisor that checks the name of a component for some naming convention
 *
 * Also @see com.ibm.team.scm.service.UniqueComponentNameAdvisor for product example code
 */
@SuppressWarnings("restriction")
public class ComponentNamingAdvisor extends AbstractScmService implements
		IOperationAdvisor {
	public static final String REQUIRED_PREFIX = "TEST_";
	public static final String COMPONENT_NAMING_ADVISOR = "com.ibm.js.scm.naming.advisor.service.componentNaming";

	@Override
	public void run(AdvisableOperation operation,
			IProcessConfigurationElement advisorConfiguration,
			IAdvisorInfoCollector collector, IProgressMonitor monitor)
			throws TeamRepositoryException {

		IComponentModificationData data = (IComponentModificationData) operation
				.getOperationData();
		if (data == null) {
			throw new TeamRepositoryException("Missing component data"); //$NON-NLS-1$
		}

		String compName;

		if (data.getOpType() == OpType.CREATE) {
			compName = data.getNewName();
		} else if (data.getOpType() == OpType.RENAME) {
			compName = data.getNewName();
		} else {
			return;
		}
		if (validateName(compName)) {
			// Nothing to do
			return;
		}
		String description = "The component name violates the naming conventions component name must have prefix '"
				+ REQUIRED_PREFIX + "'!";
		String summary = description;
		IAdvisorInfo info = collector.createProblemInfo(summary, description,
				COMPONENT_NAMING_ADVISOR);//$NON-NLS-1$
		collector.addInfo(info);
	}

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

Summary

This is as usual a very basic example with no or very limited testing and error handling. See the links in the getting started section for more examples. As always I hope this helps someone out there with running RTC.

Watch Bartosz Chrabski on how to create Rational Team Concert – Advisor & Participant Extensions


If you are new to extending RTC, you might want to look at Bartosz Chrabskis post How to create Rational Team Concert – Advisor & Participant Extension (step by step) and the related video for creating a participant as well as the video for creating an advisor. Don’t forget to like the post and give the videos a thumbs up.

More information about the process can be found in Learning To Fly: Getting Started with the RTC Java API’s.

 

A Custom Condition to Make Attributes Required or Read-Only by Role


Recently, a customer wanted to make attributes required and read only in a way the built in operational behavior does not support. So I tried to find out if there would be a way to achieve this by an extension. After struggling for some time, I finally found a way and want to share this with the community.

Update: see A Custom Condition to Make Attributes Required or Read-Only by Role Version 2 for some more tricks and information.

The Requirement

The requirement was,

  1. A user can have multiple roles
  2. Attributes are required or read only for a role in a certain state
  3. For a user all roles shall be evaluated and the attribute be required or read only if any of the roles the user has, specifies so

So the intended behavior was more like permissions work in RTC, accumulate over all roles.

The built in RTC mechanisms for required and read only attributes are based on operational behavior. Built in advisors can be configured to provide the information and behavior.

RTC Operation BehaviorYou can configure operational behavior for each role, in this example it is for the default role Everyone. However, the operational behavior in RTC does only look for the first configured operational behavior for the operation that is configured for a role the user has, as described in Process behavior lookup in Rational Team Concert 2.0. This is which is still valid for later versions of RTC.

A user has one (every user has the Everyone role) or more roles configured if he is member of a project or team area. The roles have an order, from top to bottom. The first configured operational behavior for the first role in that order that is found in the context of an operation will be executed. Only that first found operational behavior will be executed.

The idea behind this concept is, that it is possible to overwrite the operational behavior by having specific roles in a specific order. It is, for example, possible to have configured that users with the role Everyone need to provide several required attributes to save a work item and several attributes can be read only. But the Team Lead may have a lot less required attributes and no read only attributes, because the operational behavior is specified in a different way for the Team Lead role.

This also means, if the check box “Preconditions and follow-up actions are configured for this operation” is checked for a role and no preconditions are configured, RTC will do nothing, if the user has that role, even if a lot of preconditions are configured for the role Everyone. RTC will find the specification for that role and as there is nothing configured assume there is no operation behavior needed for the role.

It also means, if operational behavior is specified for all roles, only the operation behavior of the primary role the user has in the context, is executed.

The customer wanted a behavior that was different. Assume role1 and role2 exist. A work item attribute is specified to be read only for role1 but not read only for role2. The attribute shall be read only for all users that have role2 assigned, regardless of the order of the roles.

This is, again, more like permissions as described in Process permissions lookup in Rational Team Concert 2.0 where if one role a user has, has the permission, the user has this permission.

How Does the RTC Code Work?

I looked at how this is implemented in RTC by looking into the SDK in the hope to find a way to extend it somehow. It is pretty easy to find the code of the advisors/preconditions shipped with RTC.  It took me a while to figure out what is going on in the code.

One thing that had bothered me for a while became understandable in the process: Operation behavior is run after the save button is pressed. How can operational behavior have any impact on the UI before the button is pressed? How can the UI show attributes as read-only or as required before the operation is performed?

Well, it turns out, that the built in advisors have a static part, that reads the configuration. The UI knows the built in advisors and calls this part to get the configuration data in order to require attributes or make them read only. Mystery solved.

This, of course means there is no way to create your own advisor/precondition to extend RTC to work with different rules and the UI showing behaving like with the built in advisors. The UI does not know that it needs to get the custom configuration and that’s it. It is still possible to prevent a save, but the UI won’t provide any information up front.

At this point I was very close to giving up. The issue had passed a lot of smart people’s desks at that time, including mine – 5 times at least. Why should I find a solution if no one else had the slightest idea?

The Solution

Well, when looking at the code, I realized some other built in operation behavior was wired up there as well. Code for the advisors to control required and read only attributes for conditions.

Advisors for ConditionsThis turned out to be the approach that allowed to implement the requirement. Create a custom condition that allows to configure the condition advisors in a way to provide the functionality as requested.

One important issue that became apparent in the process of creating this solution is that conditions are different from all the other attribute customization providers. This almost led to me failing in finding a solution to implement this.

Conditions need to be instantiated like all the other providers but

  1. Condition instances are not configured at the attribute
  2. Conditions are only configured in the aforementioned preconditions
  3. Conditions don’t get the information about the attribute they are configured for

My initial idea was to somehow configure the condition’s additional script parameters in the process XML with information about attribute ID’s, for which roles in which states the condition returns false. This does not work. Instead the condition gets information for which state Id for which work item category, which roles are relevant. This way one condition can handle all cases as we will see in the following sections.

Solution Summary

The condition, lets call it User Role for Type and State Condition is a Java based extension for the Eclipse client and the RTC server. It allows configuring roles mapped to work item type categories and states.
The condition checks if the current work items category and state has roles configured. If roles are found, and the user has any of the roles, the condition returns true, false otherwise.
This condition can be configured and be used to make work item attributes read-only or required for a work item type category in a specific state based on the roles a user has in this context.

License and Download

The post contains published code, so our lawyers reminded me to state that the code in this post is derived from examples from Jazz.net as well as the RTC SDK. The usage of code from that example source code is governed by this license. Therefore this code is governed by this license. I found a section relevant to source code at the and of the license. Please also remember, as stated in the disclaimer, that this code comes with the usual lack of promise or guarantee. Enjoy!

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.

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.

In this context, please also consider to at least read through the Process Enactment Workshop for the Rational solution for Collaborative Lifecycle Management lab 4 and lab 5 to understand how attribute customization works.

Import The Code

Use the Eclipse importer to import existing projects into the workspace from an archive file.

Import Step 1

Browse for the archive.

Import Step 2

Select all projects and press finish. The code now shws up in your Eclipse workspace.

How The Code Works

The code comes in four projects.

Project Structure

com.ibm.js.team.workitem.attribute.user.role.condition.providers: The main project for the extension. It contains all the code that is needed and defines the plugin.xml.

com.ibm.js.team.workitem.attribute.user.role.condition.providers.feature: The feature project needed to be able to deploy the code.

com.ibm.js.team.workitem.attribute.user.role.condition.providers.updatesite: The update site needed to generate the code for deployment. This project output is also used to deploy the extension in RTC Eclipse clients.

com.ibm.js.team.workitem.attribute.user.role.condition.providers.serverdeploy: A special project to help deploying the code on RTC servers.

The structure of the main project looks like this.

Condition Code StructureThe source code is provided in four classes. The majority of the code is implemented by the class AbstractUserRoleTypeAndStateConditionProvider. It implements all of the behavior needed.

AbstractUserRoleTypeAndStateConditionProviderThe only piece missing in the code is the part that provides the process areas (project and team areas) used to look for the roles the user has. It is possible to use different approaches to configure this in RTC and based on how this is configured in RTC there are different possible approaches you want to use to get that information. So this is left abstract to be implemented in an extending class.

Possible strategies are:

  1. Look for the roles a user has in the team area that owns the work item
  2. Look for the roles a user has in the project area
  3. Look for all the roles a user has across the hierarchy of the area that owns the work item up to the project area

There might be other strategies, dependent on the context this is used in. It is possible to extend the abstract class and to provide the process areas to look into.

The class

  • ProcessAreaHierarchyUserRoleTypeAndStateConditionProvider implements strategy 3
  • ProcessAreaUserRoleTypeAndStateConditionProvider implements strategy 1
  • ProjectAreaUserRoleTypeAndStateConditionProvider implements strategy 2

Lets have a quick look at the code provided in the abstract class.it implements the method matches required by the Interface ICondition that is needed to be implemented for a condition.

/* (non-Javadoc)
 * @see com.ibm.team.workitem.common.internal.attributeValueProviders.ICondition#matches(com.ibm.team.workitem.common.model.IWorkItem, com.ibm.team.workitem.common.IWorkItemCommon, com.ibm.team.workitem.common.internal.attributeValueProviders.IConfiguration, org.eclipse.core.runtime.IProgressMonitor)
 */
@Override
public boolean matches(IWorkItem workItem, IWorkItemCommon workItemCommon,
		IConfiguration configuration, IProgressMonitor monitor)
		throws TeamRepositoryException {

	// Get the work item type category of the work item to determine if this
	// condition is configured for it
	IWorkItemType wiType = workItemCommon.findWorkItemType(
			workItem.getProjectArea(), workItem.getWorkItemType(), monitor);
	String typeCategory = wiType.getCategory();

	// Get the workflow action
	String actionId = configuration.getProviderContext()
			.getWorkflowAction();
	// Get the work item state. 
	// The state could be the current one, or it could be determined 
	// by the workflow action that is currently selected
	String wiState = findTargetStateId(workItem, actionId, workItemCommon,
			monitor);
	// Find the roles that are configured for the work item type (by type
	// category) for the work item state
	Set roles = getRoleConfiguration(typeCategory, wiState,
			configuration);
	if (null == roles) {
		// No Roles found, we can exit.
		return false;
	}

	// Get the roles the contributor has in this context
	Collection contributorRoles = getContributorRoles(workItem,
			workItemCommon, monitor);
	// Return true, if the contributor has any of the configured roles
	return hasMatchingRole(roles, contributorRoles);
}

This method first looks up the work item type and from that the work item type category. We use the type category, because all work item types of the same category have the same attributes and workflow. It would be possible to use the type directly, if needed.

Then it looks up the current workflow action from the provider context. As described in the AttributeCustomization wiki entry, conditions get the currently selected workflow action. The condition needs this to determine if a state change is about to happen and to get the roles configured for that state and not the current one. The state that is relevant for this operation is looked up.

The method then uses the work item type category, the relevant state and the configuration to determine the roles that are configured for this context. If there are no roles valid for this context the condition can end and return false.

If there are roles configured for this situation, the method gets the roles of the user trying to perform the operation.

The final check is, if the current user has any of the roles configured for this context.

Lets look at how finding the state of the work item works in findTargetStateId(). The code can be found in the SDK in the context of the preconditions.

/**
 * Find the target state of the work item for the condition. 
 * The target state is the current state if there is no workflow action selected
 * If there is a workflow action selected, the target state is the state
 * the action results in.
 * 
 * @param workItem
 * @param actionId
 * @param workItemCommon
 * @param monitor
 * @return the state ID (or null if there is no identifiable state) 
 * @throws TeamRepositoryException
 */
private static String findTargetStateId(IWorkItem workItem,
		String actionId, IWorkItemCommon workItemCommon,
		IProgressMonitor monitor) throws TeamRepositoryException {
	Identifier state = workItem.getState2();
	IWorkflowInfo wfInfo = workItemCommon.findWorkflowInfo(workItem,
			monitor);
	if (state == null && actionId == null && wfInfo != null) {
		actionId = wfInfo.getStartActionId() == null ? null : wfInfo
				.getStartActionId().getStringIdentifier();
	}
	if (wfInfo != null && state != null) {
		if (!Arrays.asList(wfInfo.getAllStateIds()).contains(state)) {
			actionId = wfInfo.getStartActionId() == null ? null : wfInfo
					.getStartActionId().getStringIdentifier();
		}
	}
	if (actionId != null && wfInfo != null) {
		state = wfInfo.getActionResultState(Identifier.create(
				IWorkflowAction.class, actionId));
		if (state == null) {
			actionId = wfInfo.getStartActionId() == null ? null : wfInfo
					.getStartActionId().getStringIdentifier();
			if (actionId != null) {
				state = wfInfo.getActionResultState(Identifier.create(
						IWorkflowAction.class, actionId));
			}
		}
	}
	// This is code that addresses a change in the process,
	// where the state of a work item can have only a number
	// Make sure the number is modified to reflect the state ID
	if (state != null) {
		String stateId = state.getStringIdentifier();
		try {
			Integer.parseInt(stateId);
			stateId = "s" + stateId;//$NON-NLS-1$
		} catch (NumberFormatException e) {
		}
		return stateId;
	}
	return null;
}

The code gets the work item state and the workflow information first. If there is no state and no action, then the work item is new and the action is the start action.

Then it looks at the case where there is a state and a work flow action, if it can’t find the current state in the workflow, there was a type change and the action is the start action (or none).

With the action identified, it calculates the target state. If there is none, the current state remains.

Finally there is a handling of the state ID’s. In some cases, for historical reasons, only a number is returned and not a sate id, The last bit of the code makes a proper state ID from the number, if needed.

The correct target state of the work item is returned at the end.

Another interesting part is to get the configuration for the roles from the process configuration done in getRoleConfiguration().

/**
 * Get the roles that are configured for the work item category and current target state
 * 
 * @param typeCategory
 * @param wiState
 * @param configuration
 * @return returns a set of roles that are configured for the 
 * work item category and state, or null, if there is not matching configuration 
 */
private Set getRoleConfiguration(String typeCategory,
		String wiState, IConfiguration configuration) {
	List workflowConfigurations = configuration
			.getChildren(CONFIGURATION_ELEMENT_WORKFLOW_PROPERTIES);
	if (null != workflowConfigurations) { // We got a configuration
		// For all configuration elements 
		for (IConfiguration workflowConfiguration : workflowConfigurations) {
			// Get the workitem state for this configuration element
			String foundStateID = workflowConfiguration
					.getString(CONFIGURATION_WORKFLOW_PROPERTY_ATTRIBUTE_STATE_ID);
			// Get the work item category for this configuration element
			String foundWorkflowCategory = workflowConfiguration
					.getString(CONFIGURATION_WORKFLOW_PROPERTY_ATTRIBUTE_WORK_ITEM_TYPE_CATEGORY);

			// If the configuration element applies to the current work item
			// state and category, get the roles that are configured
			if (foundStateID != null && foundWorkflowCategory != null
					&& foundWorkflowCategory.equals(typeCategory)
					&& foundStateID.equals(wiState)) {
				return getRoles(workflowConfiguration);
			}
		}
	}
	return null;
}

The conditions can be configured in the process configuration source as described here. The code reads the configuration data in the process.xml. It tries to find a configuration for the work item category and the state in the configuration. If one is found, it gets all roles specified and returns them.

The method getRoles() looks as below:

/**
 * Get the roles configured for this configuration element
 * 
 * @param workflowConfiguration
 * @return the roles found
 */
private Set getRoles(IConfiguration workflowConfiguration) {
	Set roles = new HashSet();
	List roleConfigurations = workflowConfiguration
			.getChildren(CONFIGURATION_ELEMENT_ROLE);
	for (IConfiguration roleConfiguration : roleConfigurations) {
		roles.add(roleConfiguration.getString(CONFIGURATION_ATTRIBUTE_ROLE_ID));
	}
	return roles;
}

It basically also reads the next level in the process configuration XML to get the configured role ID’s.

When designing the condition the following structure for the configuration was chosen.

Configuration SyntaxBasically provide the workflowProperties for the workitem type category and the state. Underneath provide the ID’s for the roles the condition should trigger.

As an example of the configuration in the process XML:

ConfigurationThe last step is to get the roles of the user that tries to perform the operation. This is done in getContributorRoles().

/**
 * Get the roles for a contributor
 * 
 * @param processArea
 * @param user
 * @param workItemCommon
 * @param monitor
 * @return
 * @throws TeamRepositoryException
 */
private Collection getContributorRoles(IWorkItem workItem,
		IWorkItemCommon workItemCommon, IProgressMonitor monitor)
		throws TeamRepositoryException {
	Collection roles = new HashSet();
	// Get Current User - we will check for the roles this user has
	IContributorHandle user = workItemCommon.getAuditableCommon().getUser();

	// Get the relevant process area(s) to look for the role
	Collection processAreas = getProcessAreas(workItem,
			workItemCommon, monitor);
		
	// Iterate the relevant process areas
	for (IProcessAreaHandle processAreaHandle : processAreas) {
		// Resolve the process area
		IAuditableCommonProcess auditableCommonProcess = workItemCommon
				.getAuditableCommon()
				.getProcess(processAreaHandle, monitor);
		IProcessArea processArea = (IProcessArea) workItemCommon
				.getAuditableCommon().resolveAuditable(processAreaHandle,
						ItemProfile.PROCESS_AREA_DEFAULT, monitor);
		// get the roles and add them to the list of roles the contributor has
		roles.addAll(auditableCommonProcess.getContributorRoles(user,
				processArea, monitor));
	}
	return roles;
}

The method gets the current user. Then it calls the abstract method getProcessAreas() to get the process areas to look for roles. It then iterates the process areas retrieved, gets the process and the roles of the user.

The method hasMatchingRole() just iterates the roles found and returns true, if the user has a role that is configured in the configuration for the given work item category and the state the work item has in this context.

/**
 * Check if the contributor has one of the configured roles.
 * 
 * @param roles
 * @param contributorRoles
 * @return true if the contributor has a role that is found in the configuration
 */
private boolean hasMatchingRole(Set roles,
		Collection contributorRoles) {
	for (IRole aRole : contributorRoles) {
		if (roles.contains(aRole.getId())) {
			// The user has a role that was relevant for this configuration
			return true;
		}
	}
	return false;
}

The classes that implement the AbstractUserRoleTypeAndStateConditionProvider basically have to implement which process areas to look at and to return them in getProcessAreas().

The version below is the most complex one, that iterates the whole hierarchy implemented in the class ProcessAreaHierarchyUserRoleTypeAndStateConditionProvider.

/***
 * 
 * Get the list of process areas to look up the roles for the contributor
 * Start with the process area a work item is filed against
 * and iterate the process area hierarchy up to the project area.
 * 
 * All found process areas are added to the search list.
 * 
 */
/*(non-Javadoc)
 * @see com.ibm.js.team.workitem.attribute.roletypestate.condition.providers.AbstractUserRoleTypeAndStateConditionProvider#getProcessAreas(com.ibm.team.workitem.common.model.IWorkItem, com.ibm.team.workitem.common.IWorkItemCommon, org.eclipse.core.runtime.IProgressMonitor)
 */
@Override
Collection getProcessAreas(IWorkItem workItem,
		IWorkItemCommon workItemCommon, IProgressMonitor monitor)
		throws TeamRepositoryException {
	// Get the project area
        HashSet processAreas = new HashSet();
        // Resolve with full data to get the hierarchy
	IProjectArea projectArea = (IProjectArea) workItemCommon
			.getAuditableCommon().resolveAuditable(workItem.getProjectArea(),
					ItemProfile.PROJECT_AREA_FULL, monitor);
		
	// Get the hierarchy to be able to find the process area parents
	ITeamAreaHierarchy hierarchy = projectArea.getTeamAreaHierarchy();

	// Start with the process area the work item is filed against
	IProcessAreaHandle processAreaHandle = workItemCommon.findProcessArea(workItem, monitor);
	do{
		// If this is a team area, add it and look for the parent area
		if (processAreaHandle instanceof ITeamAreaHandle) {
			processAreas.add(processAreaHandle);
			try {
				// Try to get the parent process area
				processAreaHandle = hierarchy.getParent((ITeamAreaHandle)processAreaHandle);
			} catch (TeamAreaHierarchyException e) {
				// this should not happen, if it does, stop the loop
				return processAreas;
			}
		} else if (processAreaHandle instanceof IProjectAreaHandle) {
			// If the area is the project area, we are done
			processAreas.add(processAreaHandle);
			return processAreas;
		}		
	} while (processAreaHandle!=null);	
	return processAreas;
}

It basically gets the project area of the work item and looks up the ITeamAreaHierarchy for it. Then it gets the process area that owns the work item. If that is a team area and not the project area, it adds the team area to the list and then tries the same with its patent process area. If the process area is a project area, it is added to the list and the method is done.

 The Plugin.XML

The plugin.XML basically defines the value providers that are available as well as the component for them.

Plugin.XMLThe condition providers are configured as shown below.

Condition Provider ConfigurationDeploying the Extension

Before deploying, the code has to be built. This is done in the project com.ibm.js.team.workitem.attribute.user.role.condition.providers.updatesite

Make sure the update site project is empty like below

Empty Update Site ProjectDelete any other files and folders visible besides the .project file and the site.xml, e.g. Jar-files and folders like plugins and features.

Open the site.xml and press Build All in the editor.

Build All Update SiteThe update site project now has new files and folders.

Update Site Ready to Deploy to ClientThese files will be used to deploy the extension to the server and later to deploy the extension on the RTC Eclipse client.

Deploy on the RTC CCM Server

There is a special project that was artificially created to help with deploying on the server. The project com.ibm.js.team.workitem.attribute.user.role.condition.providers.serverdeploy contains a folder structure that resembles the structure in the configuration folder of the server. The folder provision_profiles/ contains the provisioning file js_user_role_condition_provider.ini that contains the information needed to deploy the extension on the server. It also contains the reference to the folder site/js_user_role_condition_provider which is reflected in the project structure as well. This folder needs to contain the built features and plugins. By setting this up this way, it is relatively easy to successfully deploy the extension.

Server Deploy Project

After building, copy the folders features, plugins and the file site.xml from the project com.ibm.js.team.workitem.attribute.user.role.condition.providers.updatesite into the folder sites/js_user_role_condition_provider in the project com.ibm.js.team.workitem.attribute.user.role.condition.providers.serverdeploy as displayed below.

Prepare Server Update Site

Open the conf/ccm folder for your deployed server (Try this on a test server first). Open the folder \JazzTeamServer\server\conf\ccm like shown below.

Server Configuration Folder

In the project com.ibm.js.team.workitem.attribute.user.role.condition.providers.serverdeploy, select the folders provision_profiles and sites. Then select Copy.

Copy Server Extension

In the folder \JazzTeamServer\server\conf\ccm paste the folders and files you just copied. Acknowledge overwriting folders (and files if the extension has been deployed).

Request a server reset and restart the server. See Is The Extension Deployed? How Can I Redeploy? for details.

Check if the server Extension is deployed as described in Is The Extension Deployed? How Can I Redeploy? Search for the component com.ibm.js.team.workitem.attribute.user.role.condition.providers.component.

Chack Component DeployedThe condition provider is now deployed on your CCM server.

Deploy on the RTC Eclipse Client

The condition needs to be installed on an Eclipse client to be configured. It also needs to be installed on all Eclipse Clients that are used by users that have to use this condition.

It is possible to install this in an Eclipse client (installed from a zip file) and ship the Eclipse client with the extension installed by zipping it up again and providing the zip file.

For users that use the Web UI, the Condition works as soon as it is set up in the process.

To install the extension on an Eclipse client start Eclipse and select the menu Help>Install New Software…

Install Client Extension Step 1

In the install wizard select add.

Install Client Extension Step 2Then select Local and browse to the folder with your project com.ibm.js.team.workitem.attribute.user.role.condition.providers.updatesite

Select the User Role Condition Providers Feature, you might have to deselect the check boxes like below to see it.

Install Client Extension Step 3Press next and follow the wizard to install the extension. Restart the client.

Configure the Project

Open an Eclipse client that has the condition installed to configure it. Open the project area to configure its process.

Create Conditions for the Attributes

Open the Process Configuration. Select Process Configuration>Project Configuration>Configuration Data>Work Items>Attribute Customization.
For each attribute that needs to be read only or required, use the Add button to add a new condition.

Create New Conditions

Provide a name for the condition. The best approach is to name the condition in a naming schema that contains the usage of the condition and the attribute name. As an example name it Read_Only_AttributeName or Required_AttributeName. the reason will become apparent later. It basically helps finding it later to configure it in the operational behavior and the process XML.

You should now see the providers that you deployed in addition to Java Script. Select the provider that works best for you.

Select the ProviderPlease note, if you have groups of attributes that behave the same for all role configurations, you can create one condition for this group, instead of creating it for only one attribute.

After you created your configurations the Attribute Customization section should look as below.

Created Conditions

Configure the Operational Behavior

After creating the conditions needed, the next step is to activate the conditions for the attributes in the operational behavior.

Select Process Configuration>Team Configuration>Operation Behavior.

Select the “Everyone” role and add the preconditions Required Attributes For Condition and Read Only Attributes For Condition.

Precondition Configuration

Please note: if you want to configure this for another role, you have to configure the conditions for that role as well. How to do this efficiently is described below.

For each condition you created add a configuration for the related precondition. Select the condition and the attribute that the condition governs.

Configure Attribute In PreconditionIf you have groups of attributes that behave the same across all workflows and roles, you can use one condition for that group and select all affected attributes here.

The image below shows an example configuration.

Configured Example
Configure the Conditions in the Process Configuration Source

The Conditions don’t have any configuration for workflow states, work item categories and related roles yet. This needs to be done last. To configure the conditions it is necessary to add information to the process configuration source.
Locate the conditions in the process configuration source e.g. by searching for the name of a condition. The conditions are all in one block.

Configure Conditions In Process Configuration Source

For each condition, remove the closing /at the end of the condition element and add a new ending tag .

Configure Condition Step 1

The data should now look like the image below and there should be no errors. If there are errors, correct your XML.

Configure Condition Step 2

Now add the type, state and role configuration for each condition.

Configure Condition Step 3Configure Condition Step 4

You can configure for each condition for which work item type category (work item types with the same workflow), for which state, which roles should match the condition.

Save your work!

Test your work

To test, create a work item and move it through the workflow using user ID’s with role configurations that match your expectation. Make sure the user has roles that match the condition configuration.

Test ConditionIn this case the Attribute Filed Against is required due to the roles of the user and the description is read only in the state. Please note, that the work item is not yet initialized, but the target state is new. The section below explains the format and how to retrieve the data.

Retrieving the Configuration Data

To configure the condition, it is necessary to get the data to do the configuration. This section describes how to get the data.
The State ID’s and the Work Item Type categories as well as the Role ID’s can be retrieved from the process template.

The work item type category can be found in the web UI as well as in the Eclipse UI.
Find Work Item CategoryThe work item type category here is: “com.ibm.team.workitem.workItemType“.

To find the workflow state names use the Eclipse client and search the process configuration source  for the workflow name e.g. Defect Workflow in this case. Scroll down to find the state elements.

Find Workflow State NamesLook up the state ID’s for the states and document them. This is a one-time action and only needs to be maintained if you change workflows and add states.

The state New has the identifier “s1”, the state In Progress has id=”S2” etc.

To find the role Identifiers, open the project area or team area and select the roles.

Find Role Identifiers Step 1

The role test1 has the identifier “test1” the role Product Owner has the identifier “Product Owner”.

Find Role Identifiers Step 2The role “Everyone” has the identifier “default”.

The configuration below configures the condition to return true for

  • a work item of this type category (a defect) in the New state “s1” for the role “test1”.
  • a work item of this type category (a defect) in the Resolved state “s3” for the role “test1”.
  • a work item of this type category (a defect) in the In Progress state “s3” for the roles “test1”, “test2” and Everyone.

Configuration ExampleSee another more complex example below.

Configuration Example 2

Configuring the Operational Behavior for Multiple Roles

Operational behavior still works as explained in Process behavior lookup in Rational Team Concert 2.0. If it is necessary to configure the operational behavior for multiple roles but to make sure the Required and Read Only Attributes work the same as in the configuration for the Everyone role, this can be easily achieved.

Configure the other role(s).

Multi Role Behavior 1

When configuring the operational behavior name the precondition with the role included. For example Required Attributes For Condition Everyone.

Configure the operational behavior for the new role including the Role Name but don’t configure anything. For example name it Required Attributes For Condition Role1.

Search for the configuration for the configured role in the process configuration source.

Multi Role Behavior 2Copy the configuration details into a file. The interesting parts are the sections and <readOnlyAttributes….> .

Now search for the configuration for the new role the same way. Copy the XML with the configuration for the Required Attributes into the operational behavior configuration.   Copy the XML with the configuration for the Read-Only Attributes into the operational behavior configuration.

Save your work!

Since the conditions are configured globally you have essentially cloned the configuration for the other role.

Summary

Using a custom condition and the out of the box operational behavior for Required Attributes For Condition and Read-Only Attributes For Condition, allows to achieve the required behavior.

Keep in mind this is by no means production code. You might want to do more testing.

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

Only Owner Can Close WorkItem Advisor


I always wanted to do a Server Work Item Save advisor, so here is a simple example. This advisor will prevent closing work items for any user that is not the owner of the work item. Since the code turned out to be very simple, I will try to emphasize some other useful details about creating it. Please note, that this code can be easily changed to do more complex things e.g. only work for certain work item types or workflows, look at roles of the user that does the save to act on that and much more. There are various code examples in this blog that could be used to extend the code below to achieve those goals. The API code in this post is Server and Common API.

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!

You can download the final code here.

Just Starting With Extending RTC?

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

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

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

Creating the Plug-in(s)

The first step is, as usual, to create a plug-in. This is easily enough using the New Project wizard and choosing the Plug-in Project. All you need to provide is a name for the project and some settings.

Using a name like “OnlyOwnerCanSaveAdvisor” looks great and a lot of people do this. Its a trap!

When choosing the name of the project I have learned over the years, it is a good idea to have a certain naming convention. The most important point here is to be able to easily find your plug-ins and features again, once they are deployed. If you deploy into the Eclipse Client, your extension might end up between 1500 other extensions. If you don’t know an easy way to locate your code, you have a problem. It can take a lot of time to find it. If you don’t know where stuff is going to end on disk and if something is deployed or not, being able to search for a file name helps a lot.

Tip: Name your extensions in a way that allows you to easily search and identify them. Create a namespace pattern to support this.

I use a Java namespace structure to name my projects. I always use com.ibm.js as prefix in the name. So I can easily search for files named com.ibm.js. The rest of the name usually has something to do with the purpose. In this case I chose com.ibm.js.team.workitem.extension.advisor.statechange.service as name for the plug-in project. I chose service, because this plug-in runs on the server.

If I have to develop more complex extensions, I often end up having several plug-in projects that belong together. To be able to easily locate them in my workspace I ended up to have a common name part and a special suffix for each of the projects. The common name here would be com.ibm.js.team.workitem.extension.advisor.statechange and suffixes would be service, component, feature, updatesite and potentially others. This makes it so much easier to find around in the Eclipse UI.

The other choices here are trivial. We don’t need an activator class for this. Keep the other defaults. There is no template to choose from, so finish the wizard.

Once the plug-in project is created, give it a useful name and leave the other information as it is. Especially leave the .qualifier suffix in the version. This allows Eclipse to create a unique version extension. Your plug-in overview tab should look like below.

Plug-in Overview

Tip: Keep the Version number structure with the .qualifier suffix as provided by the default. This allows Eclipse to create a unique version extension.

The next step is typically adding the dependencies. I usually start with some I have from other extensions. org.eclipse.core.runtime is almost always needed.

Tip: Start with dependencies used in other extensions. It is easy to remove dependencies later.

You can add dependencies easily by following the namespace pattern used in RTC. The pattern starts with com.ibm.team then there is a domain such as workitem and the suffix is typically service, common, client where

  • service is API that is only available on the server
  • common is API that is available to the server and (Java/Eclipse) clients
  • client is API only available to (Java/Eclipse) clients

The domains are

  • process for API related to the process specification for process areas (project areas/team areas)
  • repository for API related to accessing data in the repository
  • workitem for work item related API
  • filesystem for the SCM API
  • interop for API to develop work item synchronizers

amongst others.

Tip:Use the namespace pattern provided by the RTC SDK to find the API plug-ins you have dependencies to.

To add dependencies, use the add button and the namespace pattern to find interesting plug-ins.

Search and add dependencies

Please be aware that there is a domain reports that continues with the usual pattern, including the domain names in the suffix as subdomains. Avoid to accidentally pick one of those if you don’t work in the reports API. If you want to use API and the classes can not be found, although the dependency was meant to be added, check if you accidentally picked the reports domain and fix the dependency. This happened to me many, many times.

Tip: The reporting API can sneak in because its namespace space includes the other domains as subdomain. Make sure to pick the right plug-ins.

It is a good idea to start with adding the usual suspects as dependencies. If you need additional API later, you can always add it on the fly and save the plugin.xml to be able to access the API.

This is a typical first iteration of dependencies:

Typical dependencies When adding the dependency, there is compatibility information added. This information would require at least a certain version of the dependency to be available. It is possible to remove this information to make the extension more compatible e.g. to earlier versions of RTC. In the dependencies above, I removed the minimal version. Since this extension was developed with RTC 4.0.5, but would work with other, earlier versions, as well this would now be deployable e.g. in RTC 4.0 or even 3.0.

Tip: Manage the required versions in the dependencies, if you want to be able to deploy in RTC versions with lower version numbers than the version you use to develop your plug-in.

The next step is to add the extension you want to hook up to. In our case we want an operationAdvisor. It can be found the same way we found the dependencies. If you can’t find your extension point, please uncheck the option Show only extension points from the required plug-ins, to make sure you can see all the extension points, even if you have not yet added the required dependency.

The full ID of the extension point is com.ibm.team.process.service.operationAdvisors  from the list of Extension Points and Operation ID’s.

Tip: Make sure to pick all extension points and use the namespace patterns already described to find extension points.

Tip: Look in the list of Extension Points and Operation ID’s to learn more about what is available.

Find and add extension pointsSelect the extension point and add it.

Once the extension point is added, provide the required information. You need to provide data for the operation advisor.

Specify basic informationThere are several mandatory fields here. I stick to my namespace pattern and provide the following information:

  • id – com.ibm.js.team.workitem.extension.advisor.statechange.RestrictClosingToOwner
  • class – com.ibm.js.team.workitem.extension.advisor.statechange.service.RestrictClosingToOwner
  • name – Restrict Closing Work Item to Owner
  • operationId – we want to react on work item save so choose com.ibm.team.workitem.operation.workItemSave from the list of available Extension Points and Operation ID’s.

Clicking in the field name class* in front of the class definition allows to create a class. It also shows you what the class is required to provide to be able to conform to the specification of the extension point. When you create the class make sure to use com.ibm.team.repository.service.AbstractService as superclass and choose the com.ibm.team.process.common.advice.runtime.IOperationAdvisor interface as implemented. The class definition should look like:

public class RestrictClosingToOwner extends AbstractService implements IOperationAdvisor {

The code of the class is going to be presented later below. For now, just quickfix and let it add the methods to implement. In order for the code to run later, you need to specify an extension service to provide the component ID this extension belongs to and specify the implementation class for this service. The interface is required in the operationAdvisor specification. The AbstractService comes in in the next step.

Tip: Make sure to create the extending class with the right interface by looking at the editor.

Click on the operationAdvisor node and add the extension service. You can also add a description.

Specify Extension Service

The extension service needs to be specified.  Stay with the namespace pattern and provide a component ID. As implementationClass, select the one that was just created.

  • componentId – com.ibm.js.team.workitem.extension.advisor.statechange.common.component
  • implementationClass – com.ibm.js.team.workitem.extension.advisor.statechange.service.RestrictClosingToOwner

Tip: Server extensions usually extend AbstractService which provides capabilities needed later e.g. to get services.

This class needs to be based on AbstractService, which was already dealt with.

The Jazz compnent extension with the ID that was just chosen still needs to be defined. There is a special extension point for this. The extension point to specify the Jazz component is com.ibm.team.repository.common.components.

Tip: Create the Extension providing a jazz component in a different plug-in. This allows to use the component later if server as well as client extensions are needed, e.g. to provide an aspect editor to configure the extension in the UI.

It would be possible to define the component in the current plug-in. However, if the component is needed in server as well as client extensions, it is necessary to bundle it with both. In this case it is better to create a special plug-in for the component. Provide the same ID that was used in the definition of the extension service and provide a name for the component.

Specify a component extensionTip: To get more information about the extension point look at the description, the schema, declaration and references. You can find all kinds of information easily, including plug-ins that extend this point and the classes that implement the extension.

There are several means that allow you to find out more about the extension point and implementations. Just be curious and look at it. The image beow shows where to access this information.

Extension Point DetailsAnother place is where you add new extensions to the point.

Add more advisorsIt will be necessary later, to declare the services that are used by the extension. Unfortunately the schema does not contain the node to do so. This is a manual that needs to be done in the plugin.xml.

Implement the Extension

The class that is to be called by the extension point has already been created. However, the implementation is still missing and needs to be provided. It is easy to open the class from the plug-in editor.

The entry point into the class called by the extension point is the run() method. The implementation code is provided below. As all advisors (preconditions)  and participants (follow-up actions) the run() method gets the information about the work item and other information about the context it is running.

This information is checked first and the work item is extracted. The advisor code contains a section that is commented out right now.This code could later be used to limit the restriction only to specific work item types.

The advisor shall not limit saving the work item in closed stats, it should only preventing to change the state to a state in the closed group. So the next check is looking at the workflow action to determine if there is a state change. If not, the advisor does not prevent the operation.  So a user could still update attributes, while it is closed,however, only the owner can change states to states in the closed group.

The last section checks if the new state of the work item is in the closed group. If not, nothing needs to be done.

If there is a workflow action, the next step is to check the state the work item will enter. If the new state is not in the closed group, nothing needs to be done.

If the new state is in the close groups, the final check is comparing the owner and the current user. If the ID’s match, nothing needs to be done. Note, this is also the case if the work item is already closed and someone wants to move it to another closed state. Only the owner will be able to do this, provided the advisor is configured for all roles.

Finally, this is a state change into a closed state and the current user is not the owner. The advisor provides a problem info and returns it. This will block the save. Please note, this is also true if the owner is unassigned.

Here is the code:

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

import org.eclipse.core.runtime.IProgressMonitor;

import com.ibm.team.process.common.IProcessConfigurationElement;
import com.ibm.team.process.common.advice.AdvisableOperation;
import com.ibm.team.process.common.advice.IAdvisorInfo;
import com.ibm.team.process.common.advice.IAdvisorInfoCollector;
import com.ibm.team.process.common.advice.runtime.IOperationAdvisor;
import com.ibm.team.repository.common.IAuditable;
import com.ibm.team.repository.common.IContributorHandle;
import com.ibm.team.repository.common.TeamRepositoryException;
import com.ibm.team.repository.service.AbstractService;
import com.ibm.team.workitem.common.ISaveParameter;
import com.ibm.team.workitem.common.model.IWorkItem;
import com.ibm.team.workitem.common.workflow.IWorkflowInfo;
import com.ibm.team.workitem.service.IWorkItemServer;

public class RestrictClosingToOwner extends AbstractService implements IOperationAdvisor {

	@Override
	public void run(AdvisableOperation operation,
			IProcessConfigurationElement advisorConfiguration,
			IAdvisorInfoCollector collector, IProgressMonitor monitor)
			throws TeamRepositoryException  {
		Object data = operation.getOperationData();
		if (data instanceof ISaveParameter) {
			IAuditable auditable = ((ISaveParameter) data).getNewState();
			if (auditable instanceof IWorkItem) {
				IWorkItem workItem = (IWorkItem) auditable;

//				// If this needs to be limited to a special type
//				if (workItem.getWorkItemType() != "Enter Type ID Here")
//					return;
				
				// We want to allow saving the work item, if there is no state change happening.				
				String action = ((ISaveParameter) data).getWorkflowAction();
				if(action==null)
					return;
				
				// Get the workflow info and check if the new state is in the closed group.
				IWorkItemServer iWorkItemServer = getService(IWorkItemServer.class);
				IWorkflowInfo workflowInfo = iWorkItemServer.findWorkflowInfo(workItem,
						monitor);
				if (!(workflowInfo.getStateGroup(workItem.getState2()) == IWorkflowInfo.CLOSED_STATES)) {
					return; // nothing to check if the new state is not closed.
				}

				// work item is going to a state in the closed group.
				// Check if the current user is owner of the work item.
				IContributorHandle loggedIn = this
						.getAuthenticatedContributor();
				IContributorHandle owner = workItem.getOwner();
				if ((owner != null && owner.getItemId().equals(
						loggedIn.getItemId())))
					return;
				
				IAdvisorInfo info = collector.createProblemInfo(
						"The work item can only closed by its owner!",
						"The work item can only closed by its owner! If the owner is unassigned and it can also not be closed.",
						"error");
				collector.addInfo(info);
			}
		}
	}
}

Before we can do the debugging the last thing we need to do is to require the service IWorkItemServer we use to be available to the server in the plugin.xml. The plugin.xml needs to be changed to reflect that.

Add the prerequisite section with the service(s) required as presented below.

Prerequisite for the required serviceTip: Since the extension point schema does not have the prerequisite added, this is something you simply have to know how to do it.

Please also see Creating Custom Link Types for Rational Team Concert, especially the sections Prepare to Deploy and the following 2 sections describing the additional project to make deployment a little easier.

Download

You can download the final code here.

Summary

The advisor presented above will prevent anyone, except the owner, to use any action that changes the state of a work item to a closed state. Users, other than the owner can still update work item attributes, but if it is configured for a user, state changes to a closed state are only possible for the owner of a work item.

As always, I hope the code above helps someone out there with extending RTC.

I will blog about the next steps, like debugging and deploying the advisor in one of the next posts.

Do not Modify the Triggering Element in an OperationAdvisor


Don’t modify the element that triggers your OperationAdvisor! The API is not designed to support that. You can do this in a participant.

Get a comprehensive overview of the rules and how this works in the Team Process Developer Guide.

I met with the work item team in Zurich two days before and we had some discussion about automation around work items in several areas. This lead to looking into the API for the extension point com.ibm.team.process.service.operationAdvisors and we found that the class providing this actually states that you should not modify the object that triggers the operation advisor.

The Wiki topic Process Execution of Operations also explains that, and why client side advisors and participants must not modify information in the repository.

See the documentation of com.ibm.team.process.common.advice.runtime.IOperationAdvisor below:

/**

* Operation advisors are run before an operation is invoked to decide whether

* or not the operation should be executed. Advisors check conditions and

* report problems via the collector they are given. If an advisor reports a

* serious problem with an operation, the operation will not be executed (though

* subsequent advisors may still be checked).

*

* Advisors may not make modifications to data which is persisted in the

* repository. Clients wishing to participate in operations by modifying data

* should implement an {@link IOperationParticipant} instead.

*

* Operation advisors are contributed on the client and server via the

* com.ibm.team.process.client.operationAdvisors extension-point and the

* com.ibm.team.process.service.operationAdvisors extension-point

* respectively.

*

Why is that important? For example imagine you have the intention to check for a work item state change, and you want to add approvals for specific state changes. You could use an operation participant for that as described in this post.

You could however also have the idea to do that in an operation adviser. If you do that, it violates the contract of the extension point. The extension point does not check that and it might actually work, but there might be ill effects or, if the extension point starts checking in the future, your extension might stop working.

One example where you might have ill effects is that the manipulation and save of the work item will not cause a second save and thus also bypass any other Advisor you might have in place. If you had an advisor that prevented approvals from being added to work items, it would not detect that change.

In general, if you want to modify data, use an operation participant.

Related Posts

RTC Update Parent Duration Estimation and Effort Participant

Resolve Parent If All Children Are Resolved Participant

A Create Approval Work Item Save Participant

Jazz.net article about creating operation Advisors

Restrict Delivery of Changesets Associated to Wrong Work Item Types Advisor


How can I access work items associated to a change set in an Advisor or precondition on the server? Here is an Advisor example that prevents delivery of change sets if they are associated to the wrong work item type.

I tried to answer some questions in the forum. They were related to how to get the work item from a change set in an Advisor. I thought it would be simple because I had done a lot with the work item link API on client and server already. This is called hubris, I guess, and I found out the hard way, that the API from the Jazz SCM side is very different from what I was used to so far. Since it was so tough, I thought I should publish what I found, so that others can use it to jump start solving their own requirements.

License and how to get started with the RTC API’S

As always, our lawyers reminded me to state that the code in this post is derived from examples from Jazz.net as well as the RTC SDK. The usage of code from that example source code is governed by this license. Therefore this code is governed by this license, which basically means you can use it for internal usage, but not sell. Please also remember, as stated in the disclaimer, that this code comes with the usual lack of promise or guarantee. Enjoy!

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

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

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

Download

The code for the example can be found here. The code uses the RTC 4.0 SDK and should work with lter versions of the SDK.

Advisors and Participants

I have already blogged about the RTC Update Parent Duration Estimation and Effort Participant. This blog is about an Advisor, so what is the difference?

A Participant is basically operational behavior that happens after a certain operation. It can fail and roll back the save, but does not have to. Participants are also called follow-up actions. An Advisor or precondition would be used If it is necessary to check certain conditions before an operation is performed. A good example for this is the Descriptive Change Sets Advisor that requires a comment or a work item associated to a change set before it can be delivered.

In general Advisors and Participants are very similar. Even most of the required API code can be used in both extension types. However there are some differences.

Participants

  • Extend the extension point com.ibm.team.process.service.operationParticipants
  • Implement the interface com.ibm.team.process.common.advice.runtime.IOperationParticipant
  • Are allowed to modify the changes they are triggered for

Advisors

They get different information when called but other than that most of the code is similar. It is also important for both implementations to extend com.ibm.team.repository.service.AbstractService in order to be able to get services and other vital information from the API.

Participants as well as Advisors need to provide an operation ID for the operation they want to be triggered and they need to specify a component and required services.

You can find a list of extension points and operation ID’s here in the Jazz Wiki. The Rational Team Concert 4.0 Extensions Workshop shows all the steps required to create a complete participant. The techniques used there have been used to create the Advisor in this post. Please note that all code below is for a Server extension.

How does that API Work?

The implementation started like any other Advisor or Participant. I looked at this question that originally started my effort and tried to figure out how to use the link API.

And I completely failed. I was able to locate similar code in the SDK but I could not figure out how to get the ProviderFactory needed to get the links. Now I did some really serious “search for references” in the SDK. I searched for plugins that extend the same extension point and use the same classes I was using, especially the link API.

As a side note, that is the only technique, besides using a search engine and scanning the SDK documentation for the RTC versions 3.x, 2.x and 1.x in the Jazz Team Wiki looking for examples, that I use to find around in the API.

The Eclipse Plugin Development Environment allows to search for references, declarations of classes, methods and extension points. It allows to use the Java search for classes and methods. It can also restrict to what kind of results one is looking for. This is really powerful and helped solving almost all my questions so far.

Robins blog about using the Plugin Spy to find Client API is also a great resource if looking for API. In most of the cases, the Client PAI works similar in the Server, only the Client Libraries need to be replaced by a Service.

Finally I found two classes that helped me solving the issue. Only, at first, I did not notice it was two classes. The Class com.ibm.team.filesystem.service.internal.ServerRequireWorkItemAdvisor uses the link code. It is the Descriptive Change Sets Advisor that requires a comment or a work item associated to a change set before it can be delivered. However, it gets the required factory in its constructor which does not match the way my implementation gets created by the extension point. I figured after a lot of confusion that this class is actually created by com.ibm.team.filesystem.service.internal.ServerRequireWorkItemAdvisorRunner which is the class that is called in the Advisor implementation. And it is able to get the factory I was looking for.

The Advisor Implementation Explained

So I tried the code and .. it did not compile. Another close look revealed that ServerRequireWorkItemAdvisorRunner extends AbstractScmService and not only AbstractService which allows it to get the data needed to get the factory. This finally led to my code below.

/*******************************************************************************
 * Licensed Materials - Property of IBM (c) Copyright IBM Corporation 2005-20012.
 * All Rights Reserved.
 * 
 * Note to U.S. Government Users Restricted Rights: Use, duplication or
 * disclosure restricted by GSA ADP Schedule Contract with IBM Corp.
 ******************************************************************************/
package com.ibm.js.team.scm.extension.restrictdelivery.advisor;

import java.util.List;

import org.eclipse.core.runtime.IProgressMonitor;

import com.ibm.team.filesystem.common.workitems.ILinkConstants;
import com.ibm.team.links.common.ILink;
import com.ibm.team.process.common.IProcessConfigurationElement;
import com.ibm.team.process.common.advice.AdvisableOperation;
import com.ibm.team.process.common.advice.IAdvisorInfo;
import com.ibm.team.process.common.advice.IAdvisorInfoCollector;
import com.ibm.team.process.common.advice.runtime.IOperationAdvisor;
import com.ibm.team.repository.common.TeamRepositoryException;
import com.ibm.team.repository.service.IRepositoryItemService;
import com.ibm.team.scm.common.IChangeSetHandle;
import com.ibm.team.scm.common.links.ChangeSetLinks;
import com.ibm.team.scm.common.process.DeliverOperationData;
import com.ibm.team.scm.common.providers.ProviderFactory;
import com.ibm.team.scm.service.internal.AbstractScmService;
import com.ibm.team.scm.service.internal.utils.ServerProviderFactory;
import com.ibm.team.workitem.common.model.IWorkItem;
import com.ibm.team.workitem.common.model.IWorkItemHandle;
import com.ibm.team.workitem.service.IWorkItemServer;

/**
 * Finds the work items a changeset is related to and checks if they are od a
 * specific type. Prevents from delivery if the type iswrong.
 * 
 */
@SuppressWarnings("restriction")
public class RestrictDeliveryToWorkitemType extends AbstractScmService
		implements IOperationAdvisor {

	public static final String COM_IBM_JS_TEAM_SCM_EXTENSION_RESTRICTDELIVERY_ADVISOR_RESTRICTWORKITEMTYPE = "com.ibm.js.team.scm.extension.restrictdelivery.advisor.restrictworkitemtype";
	// Services we need
	private IWorkItemServer workItemServer;
	IRepositoryItemService itemService;

	@Override
	public void run(AdvisableOperation operation,
			IProcessConfigurationElement advisorConfiguration,
			IAdvisorInfoCollector collector, IProgressMonitor monitor)
			throws TeamRepositoryException {
		Object operationData = operation.getOperationData();
		if (!(operationData instanceof DeliverOperationData)) {
			return;
		}
		DeliverOperationData data = (DeliverOperationData) operationData;
		List changeSetHandles = data.getChangeSetHandles();

		// Get the required service interfaces
		workItemServer = getService(IWorkItemServer.class);
		// @see com.ibm.team.filesystem.service.internal.ServerRequireWorkItemAdvisorRunner
		// @see com.ibm.team.filesystem.service.internal.ServerRequireWorkItemAdvisor
		itemService = getService(IRepositoryItemService.class);

		// Get the ProviderFactory to be able to find the links
		ProviderFactory providerFactory = new ServerProviderFactory(this, itemService);

		// Check all relationships
		for (IChangeSetHandle iChangeSetHandle : changeSetHandles) {
			List links = ChangeSetLinks.findLinks(providerFactory,iChangeSetHandle,
					new String[] { ILinkConstants.CHANGESET_WORKITEM_LINKTYPE_ID },
					monitor);
			for (ILink link : links) {
				Object resolved = link.getTargetRef().resolve();
				if (resolved instanceof IWorkItemHandle) {
					checkWorkITemType(resolved, collector, monitor);
				}
			}
		}
	}
}

This part of the code basically gets the data, gets the Services and the ProviderFactory. Then it uses the ProviderFactory and iterates all links to find if it is associated to a work item. If it is it checks for a valid work item type.

To allow better display in this blog, I pulled checking the work item type into a method.

public void checkWorkITemType(Object resolved,
		IAdvisorInfoCollector collector, IProgressMonitor monitor)
		throws TeamRepositoryException {
	// We found a work item.
	IWorkItemHandle wiHandle = (IWorkItemHandle) resolved;
	IWorkItem workItem = (IWorkItem) workItemServer
		.getAuditableCommon().resolveAuditable(wiHandle,
		IWorkItem.FULL_PROFILE, monitor);
	String workitemType = workItem.getWorkItemType();

	if (!(workitemType.equalsIgnoreCase("defect") || workitemType.equalsIgnoreCase("task"))) {
		IAdvisorInfo info = collector.createProblemInfo(
		"Change Set associated to wrong work item type. Delivery denied ",
		"The change set is assotiated to work item " + workItem.getId()
		+ " of type " + workitemType
		+ ". Changesets are allowed only for work items of type Defect or Task. Please remove this assotiation",
		COM_IBM_JS_TEAM_SCM_EXTENSION_RESTRICTDELIVERY_ADVISOR_RESTRICTWORKITEMTYPE);
		collector.addInfo(info);
	}
}

If the code detects a wrong work item type, it creates a problem info and that provides the failure information.

If you are looking for the Client API, here are some code hints in this forum question. More information about the client SCM API can be found in this and  the related posts.

As usual the exception handling is very basic and you might want to improve that if using this code. In addition you probably want to add a schema to make the code configurable in the process configuration. However, I hope this is helpful and saves some cycles searching through the RTC SDK.