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.

Advertisement

A Create Approval Work Item Save Participant

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

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

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

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

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

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

Download

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

Solution Overview

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Summary

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

Subscribing To a Work Item Using the Java API

Another thing I have been asked for several times and that is asked in the Jazz Forum right now is how to use the API to subscribe to a work item. Lets look at it, it is really easy.

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

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

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

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

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

Solution Overview

The example in this blog shows RTC Client and Common API. If you are not sure what this means, please read this and the additional blog posts mentioned in this article. Code in the server would be very similar, except that the Client Library would be replaced by a service. See the Server side code at the end of the post.

For more details on the API, suggested readings and if you want to update a work item, see the last post on uploading attachments to work items. The code below is used to create a work item and subscribe users.

private static class CreateAndSubscribeWorkItem extends WorkItemOperation {

	private String fSummary;
	private ICategoryHandle fCategory;
	private ITeamRepository fTeamRepository;
	private String fContributorUserID;

	public CreateAndSubscribeWorkItem (String summary, ICategoryHandle category, 
			ITeamRepository teamRepository, String contributorUserID) {
		super("Initializing Work Item");
		fSummary = summary;
		fCategory = category;
		fTeamRepository=teamRepository;
		fContributorUserID=contributorUserID;
	}

	@Override
	protected void execute(WorkItemWorkingCopy workingCopy,	IProgressMonitor monitor) throws TeamRepositoryException {
		IWorkItem workItem = workingCopy.getWorkItem();
		workItem.setHTMLSummary(XMLString.createFromPlainText(fSummary));
		workItem.setCategory(fCategory);

		IContributor aUser = fTeamRepository.contributorManager().fetchContributorByUserId(fContributorUserID, null);
		IContributor loggedIn = fTeamRepository.loggedInContributor();
		ISubscriptions subscriptions = workItem.getSubscriptions();
		subscriptions.add(aUser);
		subscriptions.add(loggedIn);
	}
}

The operation can be called like below. The example is again based on the wiki entry on Programmatic Work Item Creation where the missing details can be found.

	CreateAndSubscribeWorkItem operation = new CreateAndSubscribeWorkItem(summary,category, teamRepository, approverUserID);
	IWorkItemHandle handle = operation.run(workItemType, null);

The core part is shown below. First a contributor is searched using the ContributorManager and the contributor ID. The next statement gets the user that is currently logged in – running the java class. The code then retrieves the subscription list and adds both users to it. Saving (or updating) is done for us by the operation.

	IContributor aUser = fTeamRepository.contributorManager().fetchContributorByUserId(fContributorUserID, null);
	IContributor loggedIn = fTeamRepository.loggedInContributor();
	ISubscriptions subscriptions = workItem.getSubscriptions();
	subscriptions.add(aUser);
	subscriptions.add(loggedIn);

Server Code

The code above is client code and only runs in the client. However, the code to get the subscription and to add (or remove) users is common API that would run on the client and on the server. So to make this work on the server see RTC Update Parent Duration Estimation and Effort Participant for how to save a work item in the server.

In addition, the client library call to get the user would be replaced by using a service as presented below, assuming your class extends AbstractService and thus has access to the getService() method.

	IContributorService contributorService = getService(IContributorService.class);
	IContributorHandle approver = contributorService.fetchContributorByUserId(approverId);