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.

42 thoughts on “A Create Approval Work Item Save Participant

  1. Pingback: Analyzing a Aroject Areas Members and Roles Using The Plain Java Client Libraries | rsjazz

  2. Pingback: Do Not Modify The Triggering Element In An OperationAdvisor | rsjazz

  3. Thanks Ralph! Great job! but I have a problem with the method getContributorsWithRole is not defined in IProcessServerService is posible?

      • Ralph I prove:
        IProcessService fProcessService = getService(IProcessService.class);
        IContributorHandle[] matchingContributors = fProcessService.getContributorsWithRole(members, processArea,new String[] { roleName });

        and the method is not defined for the type IProcessService, is restricted for any version?

  4. I have created this originally for RTC 3.0.1.1 and I have just now looked into RTC 4.0.1 and this code works for me in these versions. I can not tell when theis was introduced, but the code works for me. You can use Search as described here https://rsjazz.wordpress.com/2013/03/20/understanding-and-using-the-rtc-java-client-api/ for the client API for the server API as well and search for operations. You can also navigate to the interface (and the extended Interface). I am not sure which version you use, but this works for me in the versions I mentioned.

    • IProcessService aprocessService = (IProcessService)getService(IProcessService.class);
      IContributorHandle[] amatchingContributors = aprocessService.getContributorsWithRole(members, processArea,new String[] { roleName });

      resolves in my code without errors. The same with my original code.

  5. Hi Ralph,

    I just published a question in Jazz.net (https://jazz.net/forum/questions/131741/rtc-plugin-for-sending-notification-email). I’d appriciate if you could go over it and see whether you can assist.
    =============
    The question says:

    I need to write RTC participant plugin that will send, under some circumstances, notification email to several recipients. Obviously, those circumstances are more complex than those that can be configured in the “Mail Configurations” supplied out-of-the-box in RTC.

    Now, of course I can use a Java emails class to create this email message, but I’d rather use the built-in emails notifications mechanism of the Jazz system (the same configurable mechanism I mentioned).

    Is this possible? Can I approach this service and use it to send emails myself using RTC plugins? How can it be done?
    ==============
    Thanks,

    Regards,
    Idan Shanny, Application Engineer
    idan@manageware.co.il

    • In the server API you can use the built in mail service of the server. No need to use external classes. There is an API available and this has actually been answered on the forum several times already.

      You can use this in a server side follow up action. You can also create your own extension listening to work item change events and sending mail from them. This would be asynchronous. Several customers have done this.
      You can not extend the notification we provide. You would create your own handler.

      I have not published any API examples for this here yet.

  6. Hi Ralph,

    First, Thank you for this post, …. I have a question about the java code of the plugin, in the class UpdateParentState.java…. there are 3 methods for closing parent’s state (resolveWithResolveActionID, resolveDF and resolveBF), you have used “resolveBF” method because it provides the shortest path, but really I don’t understand the difference between the 3 methods, you have used “resolveActionId” in the 3 methods for closing the parent’s state, but in methods resolveDF and resolveBF we search the resolve transition (in the case when there is none), can you give me an example about this please, when we haven’t the resolve transition ?
    Houssem.

    /**
    * Resolve using the resolve action
    *
    * @param workItemHandle
    * @param monitor
    * @throws TeamRepositoryException
    */
    @SuppressWarnings(“unused”)
    private boolean resolveWithResolveActionID(IWorkItemHandle workItemHandle,
    IProgressMonitor monitor) throws TeamRepositoryException {
    fWorkItemServer = getService(IWorkItemServer.class);
    IWorkItem workItem = (IWorkItem) fWorkItemServer
    .getAuditableCommon()
    .resolveAuditable(workItemHandle, IWorkItem.FULL_PROFILE,
    monitor).getWorkingCopy();

    IWorkflowInfo workflowInfo = fWorkItemServer.findWorkflowInfo(workItem,
    monitor);
    String actionId = null;
    if (workflowInfo != null) {
    // check if already closed
    if (workflowInfo.getStateGroup(workItem.getState2()) == IWorkflowInfo.CLOSED_STATES) {
    // nothing to do.
    return true;
    }
    if (workflowInfo.getStateGroup(workItem.getState2()) != IWorkflowInfo.CLOSED_STATES) {
    // Get the resolve action
    Identifier resolveActionId = workflowInfo
    .getResolveActionId();
    // If there is a resolve action
    if (resolveActionId != null) {
    Identifier availableActions[] = workflowInfo
    .getActionIds(workItem.getState2());
    // Find the resolve action in the actions available for this
    // state
    for (int i = 0; i < availableActions.length; i++) {
    if (resolveActionId == availableActions[i]) {
    actionId = resolveActionId.getStringIdentifier();
    break;
    }
    }
    }
    }
    if (actionId == null) {
    // can't use an action from the current state
    // Do nothing or throw some error
    return false;
    }
    return saveWorkitemWithAction(workItem, actionId);
    }
    return false;
    }

    /**
    * Find a way to the resolve transition, in case there is none.
    *
    * @param workItemHandle
    * @param monitor
    * @return
    * @throws TeamRepositoryException
    */
    @SuppressWarnings("unused")
    private boolean resolveDF(IWorkItemHandle workItemHandle,
    IProgressMonitor monitor) throws TeamRepositoryException {
    fWorkItemServer = getService(IWorkItemServer.class);
    IWorkItem workItem = (IWorkItem) fWorkItemServer
    .getAuditableCommon()
    .resolveAuditable(workItemHandle, IWorkItem.FULL_PROFILE,
    monitor).getWorkingCopy();

    IWorkflowInfo workflowInfo = fWorkItemServer.findWorkflowInfo(workItem,
    monitor);
    if (workflowInfo != null) {
    // check if already closed
    if (workflowInfo.getStateGroup(workItem.getState2()) != IWorkflowInfo.CLOSED_STATES) {
    Identifier resolveActionId = workflowInfo
    .getResolveActionId();
    if (resolveActionId != null) {
    WorkflowPathFinder tManager = new WorkflowPathFinder(
    workflowInfo, monitor);
    Path path = tManager.findPathToAction_DF(
    workItem.getState2(), resolveActionId);
    ArrayList transitions = path.getElements();
    for (Iterator iterator = transitions
    .iterator(); iterator.hasNext();) {
    PathElement pathElement = (PathElement) iterator.next();

    return saveWorkItemStateTransition(workItem,
    pathElement, workflowInfo, monitor);
    }
    }
    }
    }
    return false;
    }

    /**
    * Find a way to the resolve transition, in case there is none.
    *
    * @param workItemHandle
    * @param monitor
    * @throws TeamRepositoryException
    */
    private boolean resolveBF(IWorkItemHandle workItemHandle,
    IProgressMonitor monitor) throws TeamRepositoryException {
    fWorkItemServer = getService(IWorkItemServer.class);
    IWorkItem workItem = (IWorkItem) fWorkItemServer
    .getAuditableCommon()
    .resolveAuditable(workItemHandle, IWorkItem.FULL_PROFILE,
    monitor).getWorkingCopy();

    IWorkflowInfo workflowInfo = fWorkItemServer.findWorkflowInfo(workItem,
    monitor);
    if (workflowInfo != null) {
    // check if already closed
    if (workflowInfo.getStateGroup(workItem.getState2()) != IWorkflowInfo.CLOSED_STATES) {
    Identifier resolveActionId = workflowInfo
    .getResolveActionId();
    if (resolveActionId != null) {
    WorkflowPathFinder tManager = new WorkflowPathFinder(
    workflowInfo, monitor);
    Path path = tManager.findPathToAction_BF(
    workItem.getState2(), resolveActionId, true);
    ArrayList transitions = path.getElements();
    for (Iterator iterator = transitions
    .iterator(); iterator.hasNext();) {
    PathElement pathElement = (PathElement) iterator.next();

    return saveWorkItemStateTransition(workItem,
    pathElement, workflowInfo, monitor);
    }
    }
    }
    }
    return false;
    }

    • Refering to https://rsjazz.wordpress.com/2012/11/27/resolve-parent-if-all-children-are-resolved-participant/

      You have to have something to search for, an action to the desired state or a desired state. Given either of that you can find a path, if there is no action that ends in the state you want to end in, or if there is no action that you search for, no search algorithm can be successful.

      Provided you have one of the two, you can create an algorithm to find it, if there is a path through the graph from where you are to where you want. The algorithms I designed look for a resolve transition, because you can (and should) configure that in the workflow in RTC. If you don’t you would have to redesign the algorithms to search for a state rather than an action and provide the state you want as findPathToState… in the download allows to do.

      In general, if you for example want to move a dependent work item into the state the current item is, you would use the approach of trying to find a path to the state, from the current state.

      Either way, the download on the post provides tools for both approaches. If there is no way from where you are to where you want, you need to enhance the workflow. Avoid dead states (where there is no outgoing transition) or unreachable states (where there is no ingoing transition) in your workflows.

      • Maybe I misinterpreted the question. To have a resolve action, you need to define which action that is in the RTC workflow editor. If you don’t set that, there won’t be a resolve action and the algorithm would fail.

  7. Hi Ralph .. this is great and I hope to modify it to handle a bunch of automated approval generations I’ve been asked for.
    I assume that the steps to deploy this outside of Jetty are:
    1) create a Feature project
    2) create Update Site project
    3) modify attributes, create update site directory on your dev machine and then build site.xml
    4) copy contents of the update site directory to the server under ccm/sites/
    5) create the .ini file with the URL and feature ID
    6) requestReset on server (to reload plugins)
    7) restart server
    8) enable follow-up action in the operational behaviors of the team area configuration

    Is this correct?

      • I still am unable to get the Approval Participant to load. The log says it failed to resolve the bundle and before it there are tons of exceptions about services not loading, like The “com.ibm.team.jfs.app.distributed.IDistributedDataService” service was not registered. I don’t remember having this before attempting to load this Participant.

        Perhaps my Dependencies tab is incorrect? I have common and service plugins for team.process, team.repository, and workitem, along with workitem.api.common.

        Susan

      • Susan, if it runs with Jetty, the only thing I can imagine is that some kind of client API sneaked in. You won’t see errors in Jetty, but in a rel deployment in this case. I am not sure how I could help you. You could send me your code and the error messages and I can have a look. Also, please check with the Extension workshop how the packaging is done, especially the dependencies in the feature. Also, try to check if you have specific version numbers in the dependencies. Try to remove them, if that is the case.

    • What does “I can’t seem to be able to use…” mean? In what context? I don’t use it in this participant.

      com.ibm.team.process.internal.common.service.IProcessService

      should be in com.ibm.team.process.common or, more likely in
      should be in com.ibm.team.process.service .

      It is a common pattern to look for com.ibm.team.*.service and com.ibm.team.*.common to find Server API. For client API the name pattern are com.ibm.team.*.client and com.ibm.team.*.common.

  8. What I meant is when I am trying to use ‘IProcessServerService’, I am getting an error: ‘IProcessServerService’ cannot be resolved.
    You do use it in the “Find contributors by role on a process area” code snippet in this post. Here is the line of code: “fProcessServerService = getService(IProcessServerService.class);”
    I used the server api to get this, but my next question is where does the “getService” come from in the line above? Is this a custom method? “getSerivce” gives me an error and says I need to create a method with this name.

  9. Hi Ralph,

    I just tried this extension, but i could not add precondition “Create Approval” extension on Operation Behaviour – Save Work Item (server)
    Let me know where i can add “Create Approval” extension on a project area?

    • The work item command line has code for that and some participants do a work item save. So this information is in code downloadable from this blog. If you just spend the time to find it, you should be successful.

    • The dropbox link has always worked for me. It worked a minute ago. I was never able to reproduce any of the claims that Dropbox was not working.

      I must assume your issues are on your end.

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.