The plugin created in this tutorial sends each of your commit messages to your Twitter account.
In this tutorial you will:
The tutorial teaches you how to:
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
For more detail on the initial setup of the SDK, and the first steps below, see Developing with the Atlassian Plugin SDK.
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
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>
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
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>
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.
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.
The Atlassian Plugin SDK allows you to reload your plugin without restarting Fisheye/Crucible, which will speed up our work in the following sections:
cd
to fecrutwitter
atlas-cli
pi
at the atlas-cli
maven2>
promptKeep your atlas-cli
console open.
At this point open the project in your IDE.
Create a new class TwitterSettingServlet which will let you set Twitter credentials from the admin panel.
TwitterSettingsServlet.java
1 2package com.example.ampstutorial; import javax.servlet.http.HttpServlet; public class TwitterSettingsServlet extends HttpServlet { }
Let's start with very simple output. Override method doGet:
TwitterSettingsServlet.java
1 2import 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!"); }
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.
Add the following lines at the beginning of the servlet's doGet
method:
TwitterSettingsServlet.java
1 2request.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 2response.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.
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 2import 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 2import com.google.common.collect.ImmutableMap; templateRenderer.render("/templates/twitterSettings.vm", ImmutableMap.of(), response.getWriter());
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.
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 2import 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); } }
We'd like to store the 4 authentication strings as one object:
TwitterLoginRecord.java
1 2public 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; } }
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.
To accept the data post
ed from the form, override the doPost
method:
TwitterSettingsServlet.java
1 2import 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.
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 2public interface TwitterLoginRecordStore { TwitterLoginRecord get(String userName); void put(String userName, TwitterLoginRecord record); }
TwitterLoginRecordStoreImpl.java
1 2import 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); } }
atlassian-plugin.xml
1 2<component key="twitterLoginRecordStore" class="com.example.ampstutorial.TwitterLoginRecordStoreImpl" public="true"> <interface>com.example.ampstutorial.TwitterLoginRecordStore</interface> </component>
TwitterSettingsServlet.java
1 2private 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)); }
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 2import 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(); }
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()); }
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/
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).
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)"
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 2import 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>
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 2import org.slf4j.Logger; import org.slf4j.LoggerFactory; private static final Logger LOGGER = LoggerFactory.getLogger(CommitListener.class);
We need two components to work with CommitEvent
s: 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 2import 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 2import 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); } }
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 2import 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); } }
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: