Skip to end of metadata
Go to start of metadata

Applicable:

This tutorial applies to JIRA 5.0 and later.

Level of experience:

This is an intermediate tutorial. You should have completed at least one beginner tutorial before working through this tutorial. See the list of developer tutorials.

Time estimate:

It should take you approximately half an hour to complete this tutorial.

 

On this page:

Overview of the tutorial

JIRA has supported a simple listener API for a long time, but that API has several problems. Installation and configuration are complex. Also, it's impossible (as of JIRA 4.0) to share code between listeners and plugins.

Fortunately, you can use the atlassian-event library to implement the same functionality and avoid these problems. This tutorial shows you how.

In this tutorial, you will create a plugin that listens for events generated when an issue is created or resolved. When the events occur, the plugin simply logs the event. Your completed plugin will consist of the following components:

  • Java classes encapsulating the plugin logic.
  • A plugin descriptor (XML file) to enable the plugin module in the Atlassian application.

When you have finished, all these components will be packaged in a single JAR file.

About these Instructions

Icon

You can use any supported combination of OS and IDE to create this plugin. These instructions were written using IntelliJ IDEA on Ubuntu Linux. If you are using another OS or IDE combination, you should use the equivalent operations for your specific environment.

This tutorial was last tested with JIRA 6.0.4.

Required knowledge

To complete this tutorial, you need to know the following: 

  • The basics of Java development: classes, interfaces, methods, how to use the compiler, and so on.
  • How to create an Atlassian plugin project using the Atlassian Plugin SDK.
  • The basics of using and administering JIRA.

Plugin source

We encourage you to work through this tutorial. If you want to skip ahead or check your work when you have finished, you can find the plugin source code on Atlassian Bitbucket. Bitbucket serves a public Git repository containing the tutorial's code. To clone the repository, issue the following command:

git clone https://bitbucket.org/atlassian_tutorial/jira-event-listener

Alternatively, you can download the source as a ZIP archive by choosing download here: https://bitbucket.org/atlassian_tutorial/jira-event-listener. 

Step 1. Create the plugin project

In this step, you'll use an atlas command to generate stub code for your plugin. The atlas commands are part of the Atlassian Plugin SDK, and automate much of the work of plugin development for you.

  1. If you have not already set up the Atlassian Plugin SDK, do that now: Set up the Atlassian Plugin SDK and Build a Project.
  2. Open a terminal and navigate to your project home directory.
  3. Enter the following command to create a plugin skeleton:

    atlas-create-jira-plugin
    
  4. When prompted for the version to create the plugin for, choose 1 for JIRA 5. 
  5. As prompted, enter the following information to identify your plugin:

    group-id

    com.example.tutorial.plugins

    artifact-id

    new-listener-plugin

    version

    1.0-SNAPSHOT

    package

    com.example.tutorial.plugins

  6. Confirm your entries when prompted.

Step 2. Modify the POM metadata and add a dependency

The POM (Project Object Model definition file) declares your plugin's dependencies, build settings, and metadata (information about your plugin). Modify the POM as follows:

  1. Change to the new-listener-plugin directory created by the SDK.
  2. Open the pom.xml file for editing.
  3. Add your company or organisation name and your website to the organization element:

  4. Update the description element:

  5. Add the following dependency as a child of the dependencies element:

    The org.springframework version should match the version used by the UPM.

  6. We're going to use a custom log4j.properties file for this plugin. Add the following element as a child of the project.build.plugins.plugin.configuration element:

    The complete plugin element should look like this:

  7. Save the file.

Step 3. Register the plugin module in the plugin descriptor

The SDK can generate the code for many types of modules you can use in plugins. However, in the case of the event listener module, we'll need to add it by hand.

Let's start with the plugin descriptor. The plugin descriptor is an XML file that identifies the plugin to JIRA and defines the functionality that the plugin requires. Here's a basic plugin descriptor:

Notice that the SDK adds some ingredients that we won't pursue in this tutorial, such as plugin icons for the add-on management page. It doesn't hurt to leave these in, but this tutorial will leave icon customization as an exercise for you to explore later. Also note that some of the information from the POM is transferred to the plugin descriptor using variable names such as ${project.artifactId}.

Update the descriptor for the plugin as follows:

  1. Open the plugin descriptor file, atlassian-plugin.xml for editing. It is located at src/main/resources/.
  2. Add the following component declarations (as children of the atlassian-plugin element):

The first line imports the EventPublisher instance used to register for events. The second line instantiates the listener class that we will write. To be eligible for dependency injection, your classes must be registered as component plugin modules. We'll talk about this more later.

Now you are ready to write some code to make your plugin do something.

Step 4. Write the Java class

The recipe for creating an event listener is very simple:

  • Inject the EventPublisher implementation into your class.
  • Add an @EventListener annotation to any method that should receive events.
  • Handle events as they come in.

To demonstrate this, we'll create a listener that will log a notification whenever an issue is created, resolved, or closed. It's easy to imagine how this same approach could be used to send notifications, say to email or IRC, but we'll leave those as exercises for you to try later.

In the following steps, you'll create the class file and build on it little by little.

  1.  Create a file named IssueCreatedResolvedListener.java in the directory src/main/java/com/example/tutorial/plugins/, and give it the following contents:

    So far, all we've done is declared a few import statement and instantiated a logger. We'll use the logger to record our events as they happen. 

  2. Add a constructor that injects an EventPublisher instance into our plugin, which registers our IssueCreatedResolvedListener class with the EventPublisher:

    You'll recall that we added a component-import statement to our plugin descriptor for EventPublisher, the entry point to the atlassian-event library. The EventPublisher object handles publication of events and registration of event listeners. While this works for a demonstration, it isn't what you want to do in your actual plugin. We'll explain why in a moment.

  3. Add a method to handle the event:

    Notice that the method is annotated with EventListener. You can apply the EventListener annotation to any public method. The method must take a parameter corresponding to the event it should handle, IssueEvent in this case. JIRA provides several events, as described on JIRA-Specific Atlassian Events.

  4. Now create the custom log4j.properties file. This is the custom resource we added to the plugin's configuration in the POM. We need a custom log4j properties file because the JIRA log4j implementation doesn't know anything about our plugin's logger. And since our package name is com.example.tutorial.plugins, we don't inherit any of the com.atlassian hierarchical loggers.
    Add the following log4j.properties file in a new directory under your project home, src/aps.

    The key lines are the last two. Instead of adding this file as an alternative build resource for your plugin, you can simply add the last two lines to the log4j.properties file that comes with the version of JIRA you are using.

  5. Save any changed files, if you haven't already, since you'll try out your plugin next.

Step 5. Try it out

Now you're ready to try out the plugin:

  1. Open a terminal and change to the project root directory (where the pom.xml file is located).
  2. Enter the following SDK command:

  3. When JIRA finishes starting up, open the JIRA home page in a browser.
  4. Create a new project based on the Software Development project type. This gives us the workflow we need to try out each event type, that is, one that includes transitions for resolving and closing issues.  
  5. While keeping an eye on the terminal where you started JIRA, try creating an issue and then resolving it. You should see an event logged to the terminal output, like this:

    Icon

    If you miss the log message in the terminal output, you can check the log file on disk in the following directory: the target/container/tomcat6x/cargo-jira-home/logs.

It works! However, it's not yet ready for prime time. We have a little more to do to make sure that our event listener plays well given the normal plugin lifecycle in a production deployment, as described next. You can keep JIRA running while you continue your work. 

Step 6. Mind the plugin lifecycle

Icon

More information about the plugin lifecycle is available in our guide to the JIRA Plugin Lifecycle

 

The code so far makes two naive assumptions about the plugin lifecycle:

  1. The constructor will only ever be called once, and
  2. The plugin will only shut down when the system itself shuts down

Neither is true. Plugins should expect to be enabled or disabled at any time. Since our plugin registers with an external service, this needs to be accounted for.

In the plugin system itself, Atlassian plugins are implemented as Spring dynamic modules, and the atlassian-plugin.xml is transformed into a Spring XML bean configuration before it is loaded by the product. In most cases, plugin developers don't need to worry about this. In our case, we do. As a <component>, IssueCreatedResolvedListener will become a Spring bean, so we can apply the Spring interfaces InitializingBean and DisposableBean. Spring guarantees that the bean – in this case, our listener class – will have a chance to get its act together before it's put into active service, or retired from it.

To coordinate our sample event listener with the plugin lifecyle, replace the IssueCreatedResolvedListener.java contents with the following:

Here, we've moved the EventPublisher reference to a member variable and bound the registration to the afterPropertiesSet()/destroy() methods, which come from InitializingBean and DisposableBean respectively. This guarantees the correct behaviour even when our plugin is disabled and later re-enabled, perhaps by an administrator using the Universal Plugin Manager.

Re-install the plugin using FastDev, the pi command, or by restarting JIRA, and try it out again. As before, make sure that your plugin logs issue created, resolved, and closed events as expected.

Next Steps

Although writing a listener as explained here has the advantage of working inside a fully-fledged plugin, it doesn't support the properties feature of legacy (pre-4.0) JIRA listeners. In those listeners, administrators could set properties to configure a listener. For example, a listener that forwarded issue events to IRC might have properties for the IRC server, username and password to use. Atlassian event listeners don't automatically integrate with the JIRA listener configuration screen.

To make your listener configurable by a JIRA administrator, implement your own administration page that can configure not only your listener but any other information your users might need to input. For guidance on how to do this, see Writing an Admin Configuration Screen.

 

 

22 Comments

  1. I followed this tutorial, but I didn't succeed in making my listener automatically register, while my plugin enabling.

    A thing you may know in order to help me finding what didn't work : I couldn't put the annotations @Override on the methods afterPropertiesSet and destroy because the compilation unit didn't allow it to me.

    I also tried to automatically register my listener, simply injecting the event publisher, and registering in the constructor (not in afterPropertiesSet method), and it didn't work either.

    Have you got any clue ?

    Cheers,

    Clément

    1. To use @Override on methods implemented from an interface, you have to be using JDK 1.6. Sorry about that.

      Can you attach your listener class and atlassian-plugin.xml? Are they different from the example code linked above?

    2. to me it seems the <component key="issueCreatedResolvedListener" name="Listener" class="com.example.tutorial.plugins.IssueCreatedResolvedListener" /> definition is missing in atlassian-plugin.xml

      see http://confluence.atlassian.com/display/CONFDEV/Event+Listener+Module Option 1

      You must also register the class with the EventPublisher, which can be injected to the listener.

      As an annotation-based event listener is not required to implement the com.atlassian.event.EventListener interface, you cannot use the listener module descriptor.

      In order to use the annotation-based event listeners you must register your listener as a component.

      1. Did you try this Ulrich ? Does it work ?

        1. Ben, you said : "To be eligible for dependency injection, your classes must be registered as <component> plugin modules"

          Is it only for dependency injection or could it be a necessary condition to declare my listener automatically ?

          1. Some plugin modules automatically register as components; some don't. In this case, since you're declaring your own standalone class and you want dependencies injected, it needs to be a component. Listeners of this style must always be components.

  2. What version(s) of Jira does this work?

    1. Ben - does this work with JIRA 4.2?

        1. Hey, Ben (I am Ben too, ;>),

          I modified this sample code to write my first listener, but kept on getting "Failed to retrieve Plugin Version from PAC", any idea what is wrong?

          Also, in here: 

          public/contrib/jira/jira-issue-listener/trunk/src/java

          this class GetIssueDetailsListener in package com.atlassian.jira.event.listeners does not have those annotation @EventListener, how does this listener work here?

          thanks.

          1. I am running jira 4.2. I have been successful doing the following.

            First, Install the Jira sdk. In the folder where you want your new project, use the command atlas-create-jira-project. You will be prompted for 'groupID', 'artifactId', 'version' and 'package'. As I understand things, this will create a maven based project skeleton. Change directory into the folder where the pom.xml file is. Run the following three commands in order atlas-clean atlas-compile atlas-package. If each command is successful, you will have a target directory with a new jar file. At this point all you have done is proven to yourself that the skeleton compiles.

            Second, Since I was not successful following the code from the above example I decided to follow the jira sample debuglistener. I believe this is the code that one of the built in listeners in the admin listeners section of jira is created from. Go to the following link and click on DegbugListener source example DebugListener

            Third, I opened the project created in first step above in my ide (I use netbeans). The java file for me was named MyPlugin.java, so I renamed it to JiraEventListenerExample.java. Cut and paste the code, all of it from package to the bottom, into your java file. I left everything the same except for the following

            This lets me know that I am seeing output from my event listener instead of the example.

            Fourth, Go back and run the three commands mentioned above in first step. Put the jar from the project target folder into the [jira_home]\Application Data\JIRA\plugins\installed-plugins folder.

            Fifth, Go into the jira admin listeners screen and add the new listener. Give it a name like 'Jira Event Listener Example' and then enter the package name for the class. Restart jira.

            Sixth, I like to use the linux tail command to watch log information as it is added to a log file. The log file I ran tail over was stdout... in the [jira_home]\logs directory.

            Last, Add a project and you should see the log file display the comments from the event listener. Make changes, add and edit comments... and you should see messages added to the log file.

            You will probably run into some null pointer errors. I changed the code to check for nulls and got rid of all my errors.

            I am not sure why I could never get the example from Ben Speakmon to work. I believe that I got an error when added his listener, that is was not the right type, it was looking for a class that referenced JiraListener.

            1. Hey, Jim,

              thanks for sharing your experience, I will give it a try.

  3. This is nicely written tutorial!

    If you define your own Logger object in the class, and configure it in the standard log4j.properties, doesn't the output just appear as usual?

    Also, am I right in assuming that only component plugin module types need to remember about the lifecycle?

  4. Nice tutorial!. I want to add a listener to a gadget but when i add the component and component import to the gadget i get this error:
    2011-07-27 22:43:12,762 http-2990-1 WARN admin 81792x193x1 1vekjza /secure/Dashboard.jspa [gadgets.embedded.internal.GadgetUrlBuilder] GadgetUrlBuilder: could not parse spec at rest/gadgets/1.0/g/com.atlassian.plugins.tarceability-cehcker.jira-gadget-tarceability-cehcker-plugin:tarceability-cehcker-gadget/jira/traceabilityCheckerGadget/traceabilityChecker.xml

    when i make this tutorial run it catches the issue_created event correctly but gots a similar error:

    2011-07-28 08:48:09,252 http-2990-4 WARN admin 528x150x1 huuiu6 0:0:0:0:0:0:0:1 /secure/Dashboard.jspa [gadgets.embedded.internal.GadgetUrlBuilder] GadgetUrlBuilder: could not parse spec at rest/gadgets/1.0/g/com.atlassian.streams.streams-jira-plugin:activitystream-gadget/gadgets/activitystream-gadget.xml
    2011-07-28 08:51:18,084 http-2990-6 INFO admin 531x1096x1 huuiu6 0:0:0:0:0:0:0:1 /secure/CreateIssueDetails.jspa [example.tutorial.plugins.IssueCreatedResolvedListener] Issue AAAAA-1 has been created at 2011-07-28 08:51:17.421.
    2011-07-28 08:48:09,252 http-2990-4 WARN admin 528x150x1 huuiu6 0:0:0:0:0:0:0:1 /secure/Dashboard.jspa [gadgets.embedded.internal.GadgetUrlBuilder] GadgetUrlBuilder: could not parse spec at rest/gadgets/1.0/g/com.atlassian.streams.streams-jira-plugin:activitystream-gadget/gadgets/activitystream-gadget.xml

    2011-07-28 08:51:18,084 http-2990-6 INFO admin 531x1096x1 huuiu6 0:0:0:0:0:0:0:1 /secure/CreateIssueDetails.jspa [example.tutorial.plugins.IssueCreatedResolvedListener] Issue AAAAA-1 has been created at 2011-07-28 08:51:17.421.

    How can i solve this?

    1. Looks like there are some typos is the name of the gadget: "tarceability-cehcker"? But it looks like the code that creates the object to receive the event is not able to create the gadget for some reason.

      1. Sorry the typo, the name is gadget-traceability checker, i misspelled it on this page. This gadget used to work correctly and actually does, but when I import the component to handle events or add the class to which I want the component to be injected, the gadget stops working and the resource that seems to be missing is the one with the i18n(rest/gadgets/1.0/g/com.atlassian.plugins.tarceability-cehcker.jira-gadget-tarceability-cehcker-plugin:tarceability-cehcker-gadget/jira/traceabilityCheckerGadget/traceabilityChecker.xml). Another question. Is this tutorial compatible with jira 4.0.1?. I'm having some problems on 4.3 and i  would need to  make it work on 4.0.1.

  5. I use the Atlassian Plugin SDK 3.4 und follow the tutorial. When i create a new issue, i get the following exception

    Caused by: java.lang.ClassNotFoundException: com.atlassian.jira.event.type.EventType

    What's wrong?

  6. Thanks for providing the tutorial. I am using the 3.4 version of Plugin SDK. My version of Jira is 4.2.1.

    I used the Universal Plugin Manager to install my plugin. When I open an issue and edit it I don't find any output from my log statements. I looked in the logs directory and noticed that there are several different files and I went through them all and could not find my log statements.

    I added code in the event handler to log a comment when an issue is created, updated or commented.

    What am I missing?

    1. Check you've updated atlassian-jira/WEB-INF/classes/log4j.properties to refer to your log object

  7. Nice tutorial (even works with remote SOAP service calls), but why delete issue event is not fired when I delete all project? I suppose, that delete project action execute delete operation on every issue in deleted project.

    Exists something like project event listener?

  8. I now understand why you made the comment you did at the top of the tutorial in the level of experience section... I have found writing my first jira event listener a bit difficult.

    I was not successful in getting your tutorial to work. My version of Jira is 4.2 and does not have the universal plugin manager installed, and I have been unsuccessful in installing it. So I copied the tutorial jar file to the [jira_home]\ApplicationData\Jira\plugins\installed-plugins folder. When I tried registering the listener an error message said it was not a JiraListener class. It appears that substituting the import via universal plugin manager with simply copying the jar file was not enough. At that point I tried a different approach. I was able to create a plugin skeleton, insert the example DebugListener code, build, deploy the jar to the jira plugins folder by copying to the folder mentioned earlier, register the listener and get messages to display in the stdout* log.

    Going back over the tutorial code I can see the advantage of the style of plugin you demonstrated.

    In the notes section you talk about a 'full fledged' plugin. I am not sure I understand what you mean by that? Are talking about a plugin that is a spring dynamic module? What advantage does a full fledged plugin have compared to what I was able to get working?

    Thanks for your time and effort in writing the tutorial and answering questions. Without this tutorial I don't know where I would be at this point.