Last updated Dec 8, 2017

Fisheye Twitter integration plugin tutorial

About the tutorial

The plugin created in this tutorial sends each of your commit messages to your Twitter account.

In this tutorial you will:

  1. Create a Fisheye/Crucible plugin.
  2. Add an entry in settings menu for users to set properties.
  3. Set up a git repository.
  4. Create a listener that will post to Twitter on every commit.

 The tutorial teaches you how to:

  • Get the output of your servlet 'decorated' so that it has the correct headers and footers.
  • Use a servlet to add a new pane to the user settings dialog.
  • Render your servlet output using template engine.
  • Write an event listener to listen for commit events.
  • Use a Spring Component module to provide a service to several of your plugin modules.
  • Depend on a 3rd party library.

Source

We encourage you to work through this tutorial. If you want to skip ahead or check your work when you are done, 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, run:

1
2
$ git clone https://bitbucket.org/atlassian_tutorial/fisheye-twitter-plugin-tutorial.git

Alternatively, you can download the source using the 'Downloads' page here: bitbucket.org/atlassian_tutorial/fisheye-twitter-plugin-tutorial/overview

Stage 1: Create plugin structure

For more detail on the initial setup of the SDK, and the first steps below, see Developing with the Atlassian Plugin SDK.

Skeleton

First, create your plugin skeleton:

1
2
$ atlas-create-fecru-plugin
...
Define value for groupId: : com.example.ampstutorial
Define value for artifactId: : fecrutwitter
Define value for version:  1.0-SNAPSHOT: :               # just accept the default
Define value for package:  com.example.ampstutorial: :   # again, just press enter for the default

Fisheye/Crucible version

atlas-create-fecru-plugin might create a skeleton with a different version of Fisheye/Crucible than you want to use. Update the properties in pom.xml

1
2
<properties>
    <fecru.version>3.9.0-20150804094455</fecru.version>
    <fecru.data.version>3.9.0-20150804094455</fecru.data.version>
    <amps.version>5.1.18</amps.version>
    ...
</properties>

Java version

Update to use Java 8  (also in  pom.xml ) :

1
2
<plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
        <source>1.8</source>
        <target>1.8</target>
    </configuration>
</plugin>

To make sure your maven uses the correct Java version run:

1
2
$ atlas-mvn -version | grep Java

slf4j dependency

Fix the following dependency (also in pom.xml):

1
2
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.5.8</version>
    <scope>provided</scope>
</dependency>

Stage 2: Run Fisheye/Crucible

Run Fisheye/Crucible

Now run Fisheye/Crucible with the skeleton plugin:

1
2
$ cd fecrutwitter
$ atlas-run

The first time you run it, you'll need to wait a while for files to download. When the application is ready, you'll see in the console:

1
2
[INFO] Type Ctrl-D to shutdown gracefully
[INFO] Type Ctrl-C to exit

You can use atlas-debug to enable remote debugging.

Add a user in the Fisheye/Crucible admin panel

Go to localhost:3990/fecru/admin/, giving the administrator password 'password'. You should see the administrative panel. Go to User Settings > Users on the left, then click Add user on the right. Add yourself as a user to your Fisheye/Crucible instance. The e-mail address must match the one you set in Git, so that we can match commits with Fisheye/Crucible users.

Now click Log in on the right to access your account.

Reload the plugin

The Atlassian Plugin SDK allows you to reload your plugin without restarting Fisheye/Crucible, which will speed up our work in the following sections:

  1. Open a new terminal window and cd to fecrutwitter
  2. Run atlas-cli
  3. Type pi at the atlas-cli maven2> prompt
  4. Refresh your browser.

Keep your atlas-cli console open.

Stage 3: Profile settings servlet setup

At this point open the project in your IDE.

Servlet skeleton

Create a new class TwitterSettingServlet which will let you set Twitter credentials from the admin panel.

TwitterSettingsServlet.java

1
2
package com.example.ampstutorial;

import javax.servlet.http.HttpServlet;

public class TwitterSettingsServlet extends HttpServlet {
    
}

Http GET: Hello world

Let's start with very simple output. Override method doGet:

TwitterSettingsServlet.java

1
2
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.getWriter().append("Hello world!");
    }

New entry in Profile settings

To make your servlet visible in the Profile settings menu, add it to atlassian-plugin.xml

atlassian-plugin.xml

1
2
<!-- Load TwitterSettingsServlet -->
<servlet key="twitterSettingsServlet" class="com.example.ampstutorial.TwitterSettingsServlet">
    <url-pattern>/twitter-settings</url-pattern>
</servlet>

<!-- Make twitter-settings servlet visible in profile settings menu -->
<web-item key="config-link" section="system.userprofile.tab">
    <link>/plugins/servlet/twitter-settings</link>
    <label key="Twitter Configuration"/>
</web-item>

Install the servlet with pi and go to localhost:3990/fecru/. Expand the top-rightmost menu and choose Profile settings. You should now see Twitter Configuration on the list. Click it to get your "Hello world!" message. Notice that the menu disappeared.

Decorate servlet with userprofile.tab decorator

Add the following lines at the beginning of the servlet's  doGet  method:

TwitterSettingsServlet.java

1
2
request.setAttribute("decorator", "fisheye.userprofile.tab");
response.setContentType("text/html");

Remember to use pi each time to reload the servlet.

Refresh the page and check that after clicking Twitter Configuration the menu on the left no longer disappears.

The Twitter Configuration item is not highlighted correctly when selected. That's because the decorator can't identify the link belonging to the active tab. We need to set the  profile.tab.key  attribute to the key of the Web Item. Replace the plain "Hello world!" string with:

TwitterSettingsServlet.java

1
2
response.getWriter().print(
    "<html><head>" +
    "<meta name='profile.tab.key' content='com.example.ampstutorial.fecrutwitter:config-link'/>" +
    "</head><body><div>Hello world!</div></body></html>");

com.example.ampstutorial.fecrutwitter:config-link  is the plugin key (from the  <atlassian-plugin>  element) and the key of the Web Item, separated by a colon.

Now the correct text should be highlighted when you click on the Twitter Configuration link.

Templates

Your servlet can use a templating library to produce HTML. Fisheye/Crucible provide some utility classes to help you use  Velocity , but you could use other libraries too.

Add TemplateRenderer dependency

pom.xml

1
2
<dependency>
    <groupId>com.atlassian.templaterenderer</groupId>
    <artifactId>atlassian-template-renderer-api</artifactId>
    <version>1.5.4</version>
    <scope>provided</scope>
</dependency>

Add TemplateRenderer component

atlassian-plugin.xml

1
2
<!-- Import the template renderer -->
<component-import key="templateRenderer" interface="com.atlassian.templaterenderer.TemplateRenderer" />

Create basic template

In the resources directory create the "Hello world!" template:

templates/twitterSettings.vm

1
2
<html>
<head>
    <meta name='profile.tab.key' content='com.example.ampstutorial.fecrutwitter:config-link'/>
</head>
<body>
Hello world!
</body>
</html>

Inject template renderer

TwitterSettingsServlet.java

1
2
import com.atlassian.templaterenderer.TemplateRenderer;
    private final TemplateRenderer templateRenderer;
    public TwitterSettingsServlet(TemplateRenderer templateRenderer) {
        this.templateRenderer = templateRenderer;
    }

Use template

In doGet replace the response you had so far with:

TwitterSettingsServlet.java

1
2
import com.google.common.collect.ImmutableMap;
 
    templateRenderer.render("/templates/twitterSettings.vm", ImmutableMap.of(), response.getWriter());

Stage 4 Twitter access configuration

Register the app in Twitter

To access the Twitter Platform, you need to obtain a Consumer Key/Secret pair and an Access Token/Secret pair. Every user who wants his commits to be tweeted needs to do this.

Go to apps.twitter.com/ and select Create new app. Once done, open the Keys and Access Tokens tab, then click Create my access token.

Debug: check Twitter access

Once you have the 4 authentication strings, you might want to check if they are correct and if they truly let you post to Twitter. You can do so with this debug code:

TwitterStatusUpdater.java

1
2
import twitter4j.Twitter;
import twitter4j.TwitterFactory;
import twitter4j.conf.PropertyConfiguration;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.Properties;

/**
 * Little app to ensure authorisation works and you can publish tweets to Twitter.
 * First use {@link TwitterTokenGenerator} to obtain access token/secret pair. Then provide this script with the consumer
 * key/secret and access token/secret. Lastly, type in the twitter message you want to post.
 */
public class TwitterStatusUpdater {

    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        // Consumer key + consumer secret
        print("Enter consumer key:");
        String consumerKey = br.readLine();
        print("Enter consumer secret:");
        String consumerSecret = br.readLine();

        // Access token + access token secret
        print("Enter access token:");
        String token = br.readLine();
        print("Enter access token secret:");
        String tokenSecret = br.readLine();

        TwitterFactory twitterFactory = new TwitterFactory(new PropertyConfiguration(new Properties() {{
            put("oauth.consumerKey", consumerKey);
            put("oauth.consumerSecret", consumerSecret);
            put("oauth.accessToken", token);
            put("oauth.accessTokenSecret", tokenSecret);
        }}));

        Twitter twitter = twitterFactory.getInstance();
        twitter.verifyCredentials();

        print("Enter your Twitter status update:");
        String twitterUpdate = br.readLine();
        twitter.updateStatus(twitterUpdate);
        print("No exceptions, so your update seems successful");
    }

    private static void print(String x) {
        System.out.println(x);
    }
}

Bean of Twitter data

We'd like to store the 4 authentication strings as one object:

TwitterLoginRecord.java

1
2
public class TwitterLoginRecord {

    private final String consumerKey;
    private final String consumerSecret;
    private final String token;
    private final String tokenSecret;

    public TwitterLoginRecord(String consumerKey,
                              String consumerSecret,
                              String token,
                              String tokenSecret) {
        this.consumerKey = consumerKey;
        this.consumerSecret = consumerSecret;
        this.token = token;
        this.tokenSecret = tokenSecret;
    }

    public String getConsumerKey() {
        return consumerKey;
    }

    public String getConsumerSecret() {
        return consumerSecret;
    }

    public String getTokenSecret() {
        return tokenSecret;
    }

    public String getToken() {
        return token;
    }
}

Servlet for Twitter data input

We need to change the "Hello world!" servlet to become a form for typing in the data that will become TwitterLoginRecord. First declare the variable-class mapping on the first line of the template:

twitterSettings.vm

1
2
#* @vtlvariable name="loginRecord" type="com.example.ampstutorial.TwitterAccessTokenRecord" *#

Then change the body to become a form:

twitterSettings.vm

1
2
<body>
<div>
    <form action="./twitter-settings" method="post">
        <table class="dialog-prefs" cellspacing="0">
            <thead>
            <tr>
                <th colspan="2"><h3>Twitter Settings</h3></th>
            </tr>
            </thead>
            <tbody>
            <tr>
                <td class="tdLabel"><label for="consumerKey" class="label">Consumer key:</label></td>
                <td><input type="text" name="consumerKey" value="${loginRecord.consumerKey}" id="consumerKey"/></td>
            </tr>
            <tr>
                <td class="tdLabel"><label for="consumerSecret" class="label">Consumer secret:</label></td>
                <td><input type="password" name="consumerSecret" value="${loginRecord.consumerSecret}" id="consumerSecret"/></td>
            </tr>
            <tr>
                <td class="tdLabel"><label for="token" class="label">Token:</label></td>
                <td><input type="text" name="token" value="${loginRecord.token}" id="token"/></td>
            </tr>
            <tr>
                <td class="tdLabel"><label for="tokenSecret" class="label">Token secret:</label></td>
                <td><input type="password" name="tokenSecret" value="${loginRecord.tokenSecret}" id="tokenSecret"/></td>
            </tr>
            <tr>
                <td></td>
                <td class="action"><input type="submit" value="Save"/></td>
            </tr>
            </tbody>
        </table>
    </form>
</div>
</body>

Note that action="./token-servlet" specifies where the form will post the collected data.

Save data from the servlet post

To accept the data posted from the form, override the doPost method:

TwitterSettingsServlet.java

1
2
import static com.google.common.base.Strings.isNullOrEmpty;
 
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String consumerKey = request.getParameter("consumerKey");
        String consumerSecret = request.getParameter("consumerSecret");
        String token = request.getParameter("token");
        String tokenSecret = request.getParameter("tokenSecret");

        if (       !isNullOrEmpty(consumerKey)
                && !isNullOrEmpty(consumerSecret)
                && !isNullOrEmpty(token)
                && !isNullOrEmpty(tokenSecret)) {
            storeLoginRecord(consumerKey, consumerSecret, token, tokenSecret);
        }
        response.sendRedirect("./twitter-settings");
    }

Note how the parameter names match those in the twitterSettings.vm form.

Store TwitterLoginRecords

For the purposes of this tutorial, the username and password are just stored in a  Map  in a field of the servlet - this is not persistent. To learn how to save configuration data for your plugins see  Storing Plugin Settings for Fisheye .

We'll use TwitterLoginRecordStore interface, so that we can easily switch to a persistent store in the future.

TwitterLoginRecordStore.java

1
2
public interface TwitterLoginRecordStore {

    TwitterLoginRecord get(String userName);
    
    void put(String userName, TwitterLoginRecord record);
}

TwitterLoginRecordStoreImpl.java

1
2
import java.util.HashMap;
import java.util.Map;

public class TwitterLoginRecordStoreImpl implements TwitterLoginRecordStore {

    private final Map<String, TwitterLoginRecord> store = new HashMap<>();

    @Override
    public TwitterLoginRecord get(String userName) {
        return store.get(userName);
    }

    @Override
    public void put(String userName, TwitterLoginRecord record) {
        store.put(userName, record);
    }
}

Add the TwitterLoginRecordStore component

atlassian-plugin.xml

1
2
<component key="twitterLoginRecordStore" class="com.example.ampstutorial.TwitterLoginRecordStoreImpl" public="true">
    <interface>com.example.ampstutorial.TwitterLoginRecordStore</interface>
</component>

Inject and use TwitterLoginRecordStore

TwitterSettingsServlet.java

1
2
private final TwitterLoginRecordStore twitterLoginRecordStore;
private final TemplateRenderer templateRenderer;

public TwitterSettingsServlet(TwitterLoginRecordStore twitterLoginRecordStore, TemplateRenderer templateRenderer) {
    this.twitterLoginRecordStore = twitterLoginRecordStore;
    this.templateRenderer = templateRenderer;
}
 
private void storeLoginRecord(String consumerKey, String consumerSecret, String token, String tokenSecret) {
    twitterLoginRecordStore.put(getCurrentUser(),
    new TwitterLoginRecord(consumerKey, consumerSecret, token, tokenSecret));
}

Get the current user login

We use the user login as the key in the store. To get the current user login, we use EffectiveUserProvider. Add a new dependency:

pom.xml

1
2
<dependency>
    <groupId>com.atlassian.fisheye</groupId>
    <artifactId>fisheye-jar</artifactId>
    <version>3.10.0-SNAPSHOT</version>
    <scope>provided</scope>
</dependency>

Inject the user provider to the servlet and use it:

TwitterSettingsServlet.java

1
2
import com.atlassian.fecru.user.EffectiveUserProvider;

    private final EffectiveUserProvider effectiveUserProvider;

    public TwitterSettingsServlet(EffectiveUserProvider effectiveUserProvider,
                                  TwitterLoginRecordStore twitterLoginRecordStore,
                                  TemplateRenderer templateRenderer) {
        this.effectiveUserProvider = effectiveUserProvider;
        this.twitterLoginRecordStore = twitterLoginRecordStore;
        this.templateRenderer = templateRenderer;
    }
 
    private String getCurrentUser() {
        return effectiveUserProvider.getEffectiveUserLogin().getUserName();
    }

Load TwitterLoginRecord to fill the form

Once saved, we want to show the data in the form. To fill the form template, we need to modify the get method:

TwitterSettingsServlet.java

1
2
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    request.setAttribute("decorator", "fisheye.userprofile.tab");
    response.setContentType("text/html");

    TwitterLoginRecord loginRecord = twitterLoginRecordStore.get(getCurrentUser());
            
    templateRenderer.render("/templates/twitterSettings.vm",
        loginRecord != null ? ImmutableMap.of("loginRecord", loginRecord) : ImmutableMap.of(),
        response.getWriter());
}

Stage 5: Set up the Git repository

New Git repository

The goal of this tutorial is to send tweets for every commit. To do that, we need a repository to commit to. We will use Git for this tutorial. We only need a local repository.

1
2
$ mkdir repo_for_tutorial
$ cd repo_for_tutorial
$ git init
Initialized empty Git repository in .../repo_for_tutorial/.git/

Add a repo to Fisheye/Crucible

Open the admin panel (localhost:3990/fecru/admin), go to Repository Settings > Repositories. Click Add repository, select Git and provide the local path to your Git repository. Once added, select the new repository and click Browse repository. You should see your activity, such as changes to the a.txt file (see below).

Hint: quick command to generate a commit

To speed up the flow of your testing, you can use this command to modify your file and commit in one go:

1
2
$ echo $(date) >> a.txt && git add a.txt && git commit -m "tweet $(date)"

Stage 6: Listen for commits and post to Twitter

CommitListener skeleton

To listen for commits, you need to register yourself at EventPublisher and annotate at least one method with @EventListener. Choose the type of events you are interested in by specifying the type of the annotated method parameter.

Use InitializingBean and DisposableBean interfaces to register/unregister your listener at the right point of application startup/shut down.

CommitListener.java

1
2
import com.atlassian.event.api.EventListener;
import com.atlassian.event.api.EventPublisher;
import com.atlassian.fisheye.event.CommitEvent;
 
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;

public class CommitListener implements InitializingBean, DisposableBean {

    public CommitListener(EventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }
 
    @Override
    public void afterPropertiesSet() throws Exception {
        eventPublisher.register(this);
    }

    @Override
    public void destroy() throws Exception {
        eventPublisher.unregister(this);
    }

    @EventListener
    public void handleEvent(CommitEvent ce) {
        
    }
}

To install the listener, configure a new component:

atlassian-plugin.xml

1
2
<component key="commit-listener" class="com.example.ampstutorial.CommitListener" public="true">
    <interface>com.example.ampstutorial.CommitListener</interface>
</component>

Use logger

When something goes wrong, it's a good idea to log, so that it's easier to investigate the root cause. Let's add a logger:

CommitListener.java

1
2
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
    private static final Logger LOGGER = LoggerFactory.getLogger(CommitListener.class); 

Work with CommitEvent

New components for working with commits

We need two components to work with CommitEvents: CommitterUserMappingManager to map from git committer to Fisheye/Crucible user and RevisionDataService to get changeset data from changeset id. Both of these are available by default and you don't need to install them in atlassian-plugin.xml. We also need a third component - TwitterLoginRecordStore that we already have.

CommitListener.java

1
2
import com.atlassian.fisheye.spi.services.RevisionDataService;
import com.cenqua.fisheye.model.manager.CommitterUserMappingManager;
 
    private final EventPublisher eventPublisher;
    private final CommitterUserMappingManager committerUserMappingManager;
    private final RevisionDataService revisionDataService;
    private final TwitterLoginRecordStore twitterLoginRecordStore;

    public CommitListener(EventPublisher eventPublisher,
                          CommitterUserMappingManager committerUserMappingManager,
                          RevisionDataService revisionDataService,
                          TwitterLoginRecordStore twitterLoginRecordStore) {
        this.eventPublisher = eventPublisher;
        this.committerUserMappingManager = committerUserMappingManager;
        this.revisionDataService = revisionDataService;
        this.twitterLoginRecordStore = twitterLoginRecordStore;
    }

Handle events

Update handleEvent method to get user login from (repository, committer) tuple. Check if user has configured Twitter access. If yes, post to Twitter:

CommitListener.java

1
2
import com.atlassian.fisheye.spi.data.ChangesetDataFE;
 
    @EventListener
    public void handleEvent(CommitEvent ce) {
        String repositoryName = ce.getRepositoryName();
        ChangesetDataFE csData = revisionDataService.getChangeset(repositoryName, ce.getChangeSetId());
        if (csData != null) {
            String commitAuthor = csData.getAuthor();
            User user = committerUserMappingManager.getUserForCommitter(repositoryName, commitAuthor);
            if (user != null) {
                TwitterLoginRecord loginRecord = twitterLoginRecordStore.getForUser(user.getUsername());
                if (loginRecord != null) {
                    postToTwitter(loginRecord, csData);
                } else {
                    LOGGER.info("Twitter account not set for user {}", user);
                }
            } else {
                LOGGER.info("Can't find a user name for commit author {} in repository {}", commitAuthor, repositoryName);
            }
        } else {
            LOGGER.warn("Failed to find changeset data for {} in repository {}", changeSetId, repositoryName);
        }
    }

Post to Twitter

Add a dependency on external library that provides Twitter API:

1
2
<dependency>
    <groupId>org.twitter4j</groupId>
    <artifactId>twitter4j-core</artifactId>
    <version>4.0.4</version>
    <scope>compile</scope>
</dependency>

See twitter4j.org/ for library docs.

1
2
import twitter4j.Twitter;
import twitter4j.TwitterException;
import twitter4j.TwitterFactory;
import twitter4j.conf.PropertyConfiguration;
 
    private void postToTwitter(TwitterLoginRecord loginRecord, ChangesetDataFE csData) {
        try {
            // TwitterFactory can be configured only via PropertyConfiguration
            Properties props = new Properties();
            props.put("oauth.consumerKey", loginRecord.getConsumerKey());
            props.put("oauth.consumerSecret", loginRecord.getConsumerSecret());
            props.put("oauth.accessToken", loginRecord.getToken());
            props.put("oauth.accessTokenSecret", loginRecord.getTokenSecret());
            TwitterFactory twitterFactory = new TwitterFactory(new PropertyConfiguration(props));

            Twitter twitter = twitterFactory.getInstance();
            twitter.verifyCredentials();
            twitter.updateStatus(csData.getComment());
        } catch (TwitterException exception) {
            LOGGER.error("Failed posting to Twitter", exception);
        }
    }

Test your plugin

Commit something to the test repository (you can use the short script given in the previous section) and watch your plugin post the commit message to Twitter. Congratulations, you finished the task!

Rate this page: