You can create custom workflows in TeamForge using custom event handlers.

How TeamForge Custom Event Handlers Work?

The TeamForge custom event handling framework allows third-party event handlers to register for TeamForge-specific application events and notifies them whenever such an event occurs.

The event handling framework implements an extended flavor of the observer pattern. The TeamForge application events are triggered whenever a property of a TeamForge object (e.g. tracker item, discussion item, wiki page) has been changed or is going to be changed, if no event handler objects (i.e. blocks the event).

For example, you can block deletions of projects for all users, add a comment to a tracker item whenever an association has been modified, or design your own tracker workflow engine.

Writing custom event handlers requires at least some basic knowledge in the Java programming language or (if you use the examples shipped with this post) a script language that is installed on the TeamForge server, such as shell, Python, or Perl. For these instructions, we’ll assume that you are familiar with basic programming techniques.

The event handler framework differentiates between two types of events:

  • Asynchronous - If a handler registers for asynchronous events, it is informed that a change has just happened. The handler can decide to trigger further changes by calling TeamForge web services, but it cannot block the change because it has already happened.

Asynchronous event handlers are good for triggering system events, such as changing an artifact status or sending an email. See Using an Asynchronous Event Handler: Trigger Follow-up Events.

  • Synchronous - If a handler has registered for synchronous events, it gets informed whenever a change has been anticipated by a user. It can examine the properties that should be changed and decide whether to accept the change or block it. A synchronous event handler cannot trigger further changes on the currently processed object, since other handlers in the event handler chain must also have the chance to block the anticipated change.

A synchronous event handler is the appropriate way to show an alert directly in the TeamForge UI, for example. See Using a synchronous event handler: Send event handler output to the TeamForge UI.

Technically, all event handlers have to be part of a Java archive (JAR) file with a TeamForge specific deployment descriptor that describes which events should be intercepted. This JAR file then has to be uploaded to the TeamForge application server. No restart is necessary, but the event handling cache has to be refreshed.

In practice, you can customize a TeamForge site’s behavior without any knowledge of Java if you can write scripts in a language that can deal with environment variables, write to standard out/error (to control what will be displayed in the TeamForge UI as result of the handler’s execution) and control the return code (to decide whether to block the event or not).

We will show you how to come up with your own custom event handlers based on two examples.

Event Handler Example - Comment on Associations

This event handler adds a comment to a tracker item whenever an association is added to or deleted from this tracker item.

This example illustrates how to intercept a specific event, trigger a follow-up action by calling TeamForge web services, and add a comment based on the formatting template which is specified as part of a property file.

The code for example one can be found here.

  1. When you extract the JAR file you’ll find a structure like this: com/vasoftware/sf/plugin. This directory contains the class file of your event handler. There may be additional directories containing Java class files. If you like to include Java libraries, you have to unpack their JAR files and add their class files (including directory structure) in the event handler JAR.

    The META-INF/event.xml file contains the events and operations your handler class will intercept. It is also common to have additional files in the META-INF directory, such as property files to control the behavior of the event handler.

    This example event handler is provided as-is (i.e. not supported as part of any TeamForge release). As with all event handlers, use it at your own risk. CollabNet cannot guarantee any SLAs on third-party code.

  2. In the event.xml file, you will find some lines like this:

    <event api="6.1" mode="asynchronous">
     <type>relationship</type>
     <operation>*</operation>
     <handler>com.vasoftware.sf.plugin.AsynchronousRelationshipEventListener</handler>
    </event>
    

This tells the event handling framework that the class AsynchronousRelationshipEventListener is responsible for intercepting events of type “relationship” (aka associations) for every possible operation. The handler will be called after the event has happened (asynchronous mode) and the passed data structures will be compatible with the events format defined in TeamForge 6.0 (in other words, the event handler extends the EventHandler60 class). If you only want to intercept certain operations, you can specify those instead of the wildcard character (*). Supported operations are usually create, update, move and delete, but every event can define its own operations.

  1. The config.properties file is used to control the formatting of the comment that gets added when an association has been modified. The initialize-method of the handler class (AsynchronousRelationshipEventListener) shows how property files can be parsed within a custom event handler.

  2. By default, the user triggering the event is also the user executing the event handler. If you want to run your event handler with a different user account, specify it in the user element, like this:

    <event api="6.1" mode="synchronous">
      <type>*</type>
      <operation>*</operation>
      <handler>com.vasoftware.sf.plugin.AsynchronousRelationshipEventListener</handler>
      <user>foo</user>
    </event>
    

Event Handler Example - Execute a Hook Script

When a TeamForge event arrives, this event handler looks to see whether there is a script in the TeamForge file system with the name/operation of the event, and then calls that script with all information from the event contained within environment variables.

This example illustrates how to intercept arbitrary TeamForge events, examine the event’s properties, map them to system environment variables and call a script in the file system with a name corresponding to the intercepted event.

You can use this event handler to customize your TeamForge site’s behavior without any knowledge of the Java programming language as long as you can write scripts in a language that can deal with environment variables, write to standard out/error (to influence what will be displayed in TeamForge’s UI as result of the handler’s execution) and influence the return code (to decide whether to block the event or not).

The code for this example can be found here.

  1. When you extract the JAR file you’ll find a structure like this: com/collabnet/ctf/events. This directory contains the class file of your event handler. There may be additional directories containing Java class files. If you like to include Java libraries, you have to unpack their JAR files and add their class files (including directory structure) in the event handler JAR.

The META-INF/event.xml file contains the events and operations your handler class will intercept. It is also common to have additional files in the META-INF directory, such as property files to control the behavior of the event handler.

This example event handler is provided as-is (i.e. not supported as part of any TeamForge release). As with all event handlers, use it at your own risk. CollabNet cannot guarantee any SLAs on third-party code.

  1. In the event.xml file, you will find some lines like this:

    <event api="6.1" mode="asynchronous">
        <type>*</type>
        <operation>*</operation>
        <handler>com.collabnet.ctf.events.AsynchronousHookScriptEventListener</handler>
    </event>
    
    <event api="6.1" mode="synchronous">
        <type>*</type>
        <operation>*</operation>
        <handler>com.collabnet.ctf.events.SynchronousHookScriptEventListener</handler>
    </event>
    

    These lines tell TeamForge to register two event handlers, one asynchronous (AsynchronousHookScriptEventListener) and one synchronous (SynchronousHookScriptEventListener) for arbitrary events (wildcard *). It also tells TeamForge that the handler classes will extend the EventHandler60 interface.

  2. By default, the user triggering the event is also the user executing the event handler. If you want to run your event handler with a different user account, specify it in the user element, like this:

    <event api="6.1" mode="synchronous">
        <type>*</type>
        <operation>*</operation>
        <handler>com.collabnet.ctf.events.SynchronousHookScriptEventListener</handler>
        <user>foo</user>
    </event>
    

It is possible to register multiple handlers for different events, but you can also use one handler to intercept both synchronous and asynchronous events.

Event Handler Example: Hook Scripts

These sample hook scripts should give you an idea how custom event handlers can be written. Feel free to adjust them to your own needs.

  • Hooks must be owned by sf-admin for security, and must have the executable bit set.

  • To configure a site to prevent projects being deleted, we could create this file: /opt/collabnet/teamforge/hooks/synchronous/project_delete

 #!/bin/sh
 echo Sorry, projects cannot be deleted on this site 1>&2
 exit 1
  • To automatically create an initial directory structure in an SVN repository when the repository is created, you might create this file: /opt/collabnet/teamforge/hooks/asynchronous/repository_create
 #!/bin/sh
 /usr/bin/svn
 mkdirhttp://localhost/svn/repos/${tf_original_RepositoryDirectory:9:999}/trunk
 http://localhost/svn/repos/${tf_original_RepositoryDirectory:9:999}/tags
 http://localhost/svn/repos/${tf_original_RepositoryDirectory:9:999}/branches
 -m "Inital Structure"
 --username admin --password mypassword --non-interactive --no-auth-cache
 exit 0

Extend EventHandler60 for Your TeamForge Event Handler Classes

All event handlers (synchronous and asynchronous) have to extend com.vasoftware.sf.events.EventHandler60.

This is how our example “Comment on associations” event handler does it:

package com.vasoftware.sf.plugin;
...
public class AsynchronousRelationshipEventListener extends EventHandler60 {
...
}

The folloiwng table lists the JAR file location for different versions of TeamForge that contains EventHandler60.

TeamForge Version Jar File
6.1 /opt/collabnet/teamforge/dist/deployments/saturn.ear/sfevents.jar
6.1.1 /opt/collabnet/teamforge/dist/deployments/saturn.ear/lib/sfevents.jar
6.2 /opt/collabnet/teamforge/dist/deployments/saturn.ear/lib/sfevents.jar
6.2.0.1 /opt/collabnet/teamforge/dist/deployments/saturn.ear/lib/sfevents.jar
7.0 /opt/collabnet/teamforge/dist/deployments/saturn.ear/lib/sfevents.jar
7.1 /opt/collabnet/teamforge/dist/deployments/saturn.ear/lib/sfevents.jar
8.0 /opt/collabnet/teamforge/dist/deployments/saturn.ear/lib/events-api.jar
8.0.0.1 /opt/collabnet/teamforge/dist/deployments/saturn.ear/lib/events-api.jar
8.1 /opt/collabnet/teamforge/dist/deployments/saturn.ear/lib/events-api.jar
8.1.0.1 /opt/collabnet/teamforge/dist/deployments/saturn.ear/lib/events-api.jar
16.3 /opt/collabnet/teamforge/dist/deployments/lib/events-sdk.jar
16.7 and later /opt/collabnet/teamforge/dist/deployments/lib/event.jar
  • EventHandler60 has one abstract method you have to override:

     @Override
     public void processEvent() throws Exception {
     ...
    }
    
  • This method does not take any parameters, so how do we access the relevant information?

    There are some interesting methods of the base class (EventHandler60) you can call:

    • getSessionKey - Returns a session id of the user who is going to (synchronous handler) / has triggered (asynchronous handler) the event we just intercepted. If you used the user element in event.xml, it will contain a session id for the user you specified there.

    • getEventContext - Returns a data structure containing the event type, operation, project, comment and user name.

    • getOriginalData - In case of a synchronous event handler, this will return a representation of the object the event is going to change. In case of an asynchronous event handler, this will return the representation of the object before it was changed by the event. The data structure used to represent the object is the same that would have been used in CollabNet’s SOAP API.

    • getUpdatedData - In case of a synchronous event handler, this will return a representation of the object how it will look after the event has happened (you can still block it). In case of an asynchronous event handler, this will return the representation of the object after it was changed by the event.

    Let’s assume a user wants to change the priority of a tracker item from 3 to 4. If you have registered a synchronous event handler, this one is triggered before the change can actually be performed. getOriginalData returns an ArtifactSoapDO object of the tracker item with the priority field set to 3. getUpdatedData contains an ArtifactSoapDO object of the tracker item with the priority field set to 4.

    • If you block the event (by throwing an exception), the change does not happen and the user is presented with an error message. (See next section for how to influence this error message.)

    • If you do not block the event (by just returning from the processEvent method), all registered asynchronous handlers are called. getOriginalData and getUpdatedData contain exactly the same objects as in the synchronous case. However, the semantic is different: They are no longer representing the current and anticipated state, but the previous and current state of the object in question.

  • The following code snippet (taken from our “Hook script” event handler example) shows how to retrieve all information available to an event handler.

 String type = getEventContext().getType();
 String operation = getEventContext().getOperation();
 String projectId = getEventContext().getProjectId();
 String comment = getEventContext().getComment();
 String userName = getEventContext().getUsername();
 String originalDataClassName = getOriginalData().getClass().toString();
 String updatedDataClassName = getUpdatedData().getClass().toString();
 Object originalData = getOriginalData();
 Object updatedData = getUpdatedData();

Using a Synchronous Event Handler: Send Event Handler Output to the TeamForge UI

When we have extracted all data available to the event handler, how do we interact with the user interface?

  • You use three independent actions to interact with the TeamForge UI:

    1. Add a success message to the UI that gets displayed as the result of the action just triggered by the user. This can be done by calling the addSuccessMessage method of the EventHandler base class (see SynchronousHookScriptEventListener.java of example two for details).

    2. Add an error message to the UI that gets displayed as the result of the action just triggered by the user. This can be done by calling the addErrorMessage method of the EventHandler base class.

    3. Block the event you intercepted. This can be done by throwing an exception in the processEvent method. The payload of your exception will be displayed in the UI.

    All three forms of UI feedback can be used in combination. For example, it is possible to display an error message even if you did not block the event, and it is possible to show many error and success messages together.

  • What happens if the event in question was not triggered by a user logged into the TeamForge Web UI but by a client using the TeamForge web services?

In this case, error and success messages do not reach the SOAP client. However, the payload of the exception object thrown when the event was blocked is delivered as part of the SOAP fault element.

  • While synchronous event handlers enable you to block events and/or to provide additional feedback to the currently logged in user, they should not be used to trigger follow-up actions (like changing TeamForge artifacts or interacting with external systems).

Remember that these handlers are running in the main TeamForge event loop and nothing else will happen until you return from the processEvent method, so return as fast as you can.

Using an Asynchronous Event Handler: Trigger Follow-up Events

Use an asynchronous event handler to communicate with TeamForge, external systems, processes or system resources.

Interacting with TeamForge is done as you would do it if you had to write a Java program to interact with TeamForge using its web services API. The only difference is that you will connect to localhost (since your handler is running locally) and that you already have a valid session ID.

  • You do not have to include the SOAP SDK classes in your event JAR file, because they are already in the TeamForge class path. This code snippet extracted from our association converter example ([Event Handler example: Comment on Associations][/teamforgecustomeventhandlers.html#commentonassociations]) shows how to do it:
 ITrackerAppSoap trackerClient = (ITrackerAppSoap) ClientSoapStubFactory.getSoapStub(
 ITrackerAppSoap.class, "http://localhost:8080");
 ...
 ArtifactSoapDO artifact = trackerClient.getArtifactData(getSessionKey(), originId);
 trackerClient.setArtifactData(getSessionKey(), artifact, finalComment, null, null, null); 
  • Using the session key provided by the event handler is actually only going to work if the SOAP call you are using is not throwing an exception. The session ID passed into your handler is associated with an already running transaction that will be aborted if an exception is thrown as part of this session. Part of rolling back the transaction is rolling back the JVM’s call stack which contains your event handling code, so you will not be able to catch the web services exception. If you like to to handle web service exceptions, you have to create your own session id by logging into TeamForge again by calling ICollabNetSoap.login with some credentials stored as part of your handler. (You can store them in a property file in the META-INF directory.)

Best Practices for Working with Custom TeamForge Event Handlers

In general, watch out for deadlocks and favor asynchronous over synchronous event handlers.

Beware of Deadlocks

Having custom event handlers that modify other objects can be dangerous if there it is possible for that handler or another handler to chain in the opposite direction. An example of this is an event handler that updates a task when an associated artifact is updated and updates the artifact when the associated task is updated. It is possible for two users to modify each object at the same time causing the two event handlers to wait on each other. The task handler would have a lock on the task bean in the application server while the artifact handler would have a lock on the artifact bean. When the custom event handlers fired, they would wait for the locks to be released but since the two threads have the locks each other needs and are waiting on the opposite objects, a deadlock would occur.

Asynchronous is Safer

Custom event handlers will be the least worrisome when they are responsible for data validation or secondary object creation (or association creation). Object modification is possible but adds greater complexity due to the risks involved with locking multiple objects across many threads. If you are unsure, use asynchronous handlers to modify objects instead since the lock on the original object will be gone by the time the asynchronous handler is executed.

Calling and waiting for synchronous hooks currently doesnt have a timeout. As long as your synchronous hook is running, the whole TeamForge site will be blocked for all users accessing the site. Some events trigger other events. For example creating a project actually calls the create project hook, wiki page hooks, and so on. Badly written or slow hooks can cripple a site.

Write to a File

Write your diagnostics messages in a file and not on stdout/stderr, since TeamForge does not read from stdout/stderr before the script completes. In the case of synchronous hooks, this could lead to a situation where the script blocks because the pipes buffer between the script process and the TF process is completely filled.

No Cascading

Due to the nature of custom event handling, custom events cannot cascade. This means that if a custom event handler catches an event and creates an object that it or another custom event handler would normally process, the event bypasses the custom event handlers. This is to prevent looping and infinite object creation. While there are ways for event handlers to avoid this, it would be a fairly difficult task since all of your event handlers would have to use a circular event detection algorithm. Rather than adding that complexity, we just eliminated the possibility.

Event Handler Life Cycle

For every single call to the processEvent method, a new object of your class will be instantiated. A best practice to avoid costly reinitialization every time (remember that the TeamForge event loop thread is blocked while you are doing this) is to delegate all synchronization work to a method you always call in your constructor which checks a static variable whether the initialization has already been done and if not, just returns without any further action (code snippet from example one):

private static boolean initialized = false;
public  AsynchronousRelationshipEventListener() {
initialize();
}

private synchronized void initialize() {
if (initialized ) {
return;
}
initialized = true;
// proceed with initialization
...
}

Logging in into TeamForge and initializing network connections file resources are costly operations that should be handled in such a method instead of doing it all over again.

Event Spooling

While it is true that asynchronous handlers may consume considerably more time than synchronous ones, there is only one thread for those handlers, so events may queue up if you do expensive operations. A best practice is to capture the event in your asynchronous event handler, write all necessary information to the local file system (comparable to a mail spooling directory) and return. At the same time, you can have a separate application reading from the spooling directory. This way, you never get into a situation where you miss TeamForge events, or things queue up just because you run into a blocking operation.

Incremental Changes

The event handler parser is really picky on the exact format of your JAR file. A best practice is to base your work on an already existing event handler and then adapt it to your own needs by doing incremental changes while checking whether it still works.

Watch out for Loops

Your follow-up actions may trigger your handler to be called again. You have to protect your handler from an infinite update loop if that happens. A best practice is to add a check to your event handler to see whether the user initiating the event is the same user you are using to perform follow-up actions.

Roll Back Sparingly

Throwing an exception in a synchronous event handler blocks the intercepted event and rolls back the transaction associated with the change. Rolling back transactions also means that the data the user entered is not saved. If this happens accidentally due to a wrongly programmed event handler, it can be frustrating to your users, so make sure that you only throw exceptions in your handler code when you really want to enforce the rollback.

Catch Errors Generically

It is quite easy to miss an exception you did not expect (like a null pointer exception, parsing exception, time out exception, any other malfunction in your own code). A best practice is to introduce a generic catch block in your handler and only rethrow the exception if it was an intended exception (see SynchronousHookScriptEventListener):

} catch (Exception e) {
 if (!intendedException) {
 log.error("Exception occured: " + e.getMessage(), e);
 } else {
...
 throw e;
 }
 }
Tags for this page: extend_teamforge customize