Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Let's Build a Bitbucket Add-on in Clojure! - Part 4: Talking to Bitbucket

DZone's Guide to

Let's Build a Bitbucket Add-on in Clojure! - Part 4: Talking to Bitbucket

Check out this tutorial on building a Bitbucket add-on in Clojure, concentrating on talkint to Bitbucket, browser vs. server-side calls, and more!

· Java Zone
Free Resource

Learn how to troubleshoot and diagnose some of the most common performance issues in Java today. Brought to you in partnership with AppDynamics.

In part 3 of this series we added REST and JSON capabilities to our add-on. However most Atlassian Connect add-ons will want to add some user-interface elements to the Bitbucket repository too, usually by working with data from the repository. To get this data, the add-on will need to talk to Bitbucket directly. In this installment, we'll look at a couple of ways to do this, including how to authenticate using the handshake information we received in the previous blog post.

Talking to Bitbucket

The real power of Atlassian Connect is the ability to modify Bitbucket's UI to expand its functionality. However for most add-ons this will entail communicating with Bitbucket via the API to interact with a user's repository and other information. This is why in the previous installment when we implemented the calls that Bitbucket will make back to our add-on we left out the /connect-example endpoint which serves up the HTML component that will be embedded into Bitbucket. Before we can fill out this endpoint we need to be able to fetch some metadata about the repository from Bitbucket.

Two Methods: Browser or Server-side Calls

The Bitbucket API can be accessed via two methods; server-side calls, or from the browser UI component itself via JavaScript. We'll use both in our example, so let's define our UI component template in resources/views/connect-example.selmer:

<!doctype html>
<html>
  <head>
    <title>Atlassian Connect</title>

    <link rel="stylesheet" href="//aui-cdn.atlassian.com/aui-adg/5.6.11/css/aui.css" media="all">
    <link rel="stylesheet" href="//aui-cdn.atlassian.com/aui-adg/5.6.11/css/aui-experimental.css" media="all">
    <!--[if IE 9]><link rel="stylesheet" href="//aui-cdn.atlassian.com/aui-adg/5.6.11/css/aui-ie9.css" media="all"><![endif]-->
    <link rel="stylesheet" href="/css/addon.css" type="text/css" />

    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
    <script src="//aui-cdn.atlassian.com/aui-adg/5.6.11/js/aui-soy.js" type="text/javascript"></script>
    <script src="//aui-cdn.atlassian.com/aui-adg/5.6.11/js/aui.js" type="text/javascript"></script>
    <script src="//aui-cdn.atlassian.com/aui-adg/5.6.11/js/aui-datepicker.js"></script>
    <script src="//aui-cdn.atlassian.com/aui-adg/5.6.11/js/aui-experimental.js"></script>

    <script src="https://bitbucket.org/atlassian-connect/all-debug.js" type="text/javascript"></script>
  </head>

  <body class="aui-page-hybrid">
    <section id="content" role="main">

      <img src="/img/100px-clojure.png" style="float:left;"/>

      <table>
        <tr>
          <td>Add-on user (retrieved via server-to-server REST):</td>
          <td>{{displayname}}</td>
        </tr>
        <tr>
          <td>Your name (retrieved via <code>AP.request()</code>):</td>
          <td>
            <span id="displayName"></span>
            <span class="aui-icon aui-icon-wait loading">Please wait</span>
          </td>
        </tr>
        <tr>
          <td>This repository (retrieved from callback parameters):</td>
          <td>{{repopath}}</td>
        </tr>
        <tr>
          <td>Page visits:</td>
          <td>
            <span id="pageVisits"></span>
            <span class="aui-icon aui-icon-wait loading">Please wait</span>
          </td>
        </tr>
      </table>

    </section>

    <script src="/js/addon.js"></script>

  </body>
</html>

This is pretty much a straight port of Tim Pettersen's NodeJS version Connect example, but usingSelmer markup and adding a Clojure logo. The JavaScript Bitbucket callbacks are implemented in/js/addon.js, which is ripped straight out of Tim's project:

// Bitbucket Connect also supports a client side library - AP (for "Atlassian Plugins") - that
// allows you to interact with the host application. For example, you can make authenticated
// requests to the Bitbucket REST API ...

AP.require('request', function(request) {
    request({
        url: '/1.0/user/',
        success: function(data) {
            $('#displayName')
                .text(data.user.display_name)
                .next('.loading').hide();
        }
    });
});

// ... and set cookies (browser security policies can prevent this from being done in iframes).

var COOKIE_NAME = 'example-visits';

AP.require('cookie', function(cookie) {
    cookie.read(COOKIE_NAME, function(visits) {
        visits = (visits ? parseInt(visits) : 0) + 1;
        cookie.save(COOKIE_NAME, visits, 30);
        $('#pageVisits')
            .text(visits)
            .next('.loading').hide();
    });
});

This code in turn utilises the all-debug.js library loaded from Bitbucket that sets up the web messaging between the browser and the server-side.

(At this point, some people are probably asking pointedly why I'm not using ClojureScript. Don't worry, we'll get to that in a later installment, and maybe some bonus ClojureScript+core.async fun too.)

Server-side API Calls

But more interesting for the time being are the server-side calls, so let's get those working, too. We've already created template markers for variables in the template above, but we need to retrieve the information. We'll create a new namespace in bitbucket.clj for this. As we're going to be making REST calls and authenticating we'll pull the HTTP client as a dependency:

(ns hello-connect.bitbucket
  (:require [clojure.tools.logging :as log]
            [clj-http.client :as http]
            [clj-connect.jwt :as jwt]
            [environ.core :refer [env]]
            [hello-connect.storage :as storage]))

The JavaScript calls to Bitbucket are automatically authenticated by the browser, but for the server-side call we'll need to authenticate ourselves. The recommended method to acquire an OAuth key in Connect is to use the shared-secret we received during the add-on installation (via the /installed endpoint) torequest an OAuth key from Bitbucket. This is done by making a JWT-authenticated call to Bitbucket:

(defn fetch-oauth-token []
  (let [token (jwt/gen-jwt-token "POST"
                                 "/site/oauth2/access_token"
                                 {}
                                 (env :project-key)
                                 (storage/client-key)
                                 (storage/shared-secret))

        resp (http/post "https://bitbucket.org/site/oauth2/access_token"
                        {:headers {"Authorization" (str "JWT " token)}
                         :form-params {:grant_type "urn:bitbucket:oauth2:jwt"}
                         :as :json})]

    (-> resp
        :body
        :access_token)))

(Note that these tokens last 90 minutes so should be cached for a production setup. However as this is just a demo we can cut corners.)

It is also possible to use the JWT token to authenticate with the API directly, however this is not fully documented at this point so we'll use the officially supported OAuth method.

We'll also specify a function to fetch the current user's display-name by requesting the full information and extracting it with a Clojure threading macro:

(defn bb-display-name [oauth]
  (let [data (http/get "https://bitbucket.org/api/1.0/user/" {:oauth-token oauth
                                                                :as :json})]
    (log/info "Received user data" data)
    (-> data
        :body
        :user
        :display_name)))

Serving the UI Component Up

Now that we have our calls and our template, we can pull it all together by defining a calling function and route in handler.clj:

(defn get-repo-page [request]
  (let [{jwt-token "jwt"
         repo-path "repoPath"} (request :query-params)

         oauth (bitbucket/fetch-oauth-token)
         ctx {:repopath repo-path
              :displayname (bitbucket/bb-display-name oauth)}]
    (log/info "Getting repo information for " repo-path)

    {:status 200
     :headers {"Content-Type" "text/html; charset=utf-8"}
     :body (selmer/render-file "views/connect-example.selmer" ctx)}))

(defroutes app-routes
  (GET  "/" [] (response/redirect "/atlassian-connect.json"))
  (GET  "/atlassian-connect.json" []
        (gen-descriptor-reply))

  (POST "/installed" {params :query-params body :body}
        (process-installed params body))

  (POST "/uninstalled" request
        (wrap-jwt-auth request process-uninstalled))

  (POST "/webhook" request
        (wrap-jwt-auth request process-webhook))

  (GET  "/connect-example" request
        (wrap-jwt-auth request get-repo-page))

  (route/not-found {:status 404 :body "Not Found"}))

Next Time

In the previous installments we've created our Connect project, configured the descriptor and created a REST API. With the addition of a valid UI component to serve-up in Bitbucket we're ready to take it for a spin. In the next installment we'll review some of the practicalities of doing that, from both the Clojure and Bitbucket Connect points of view.

The Code

The code for this part of the tutorial series is available in the part-4 tag in the accompanying hello-connect Bitbucket repository. There will also be code appearing there for the later parts as I work on them if you want to skip ahead.

originally written by Steve Smith

Understand the needs and benefits around implementing the right monitoring solution for a growing containerized market. Brought to you in partnership with AppDynamics.

Topics:
clojure ,bitbucket

Opinions expressed by DZone contributors are their own.

The best of DZone straight to your inbox.

SEE AN EXAMPLE
Please provide a valid email address.

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.
Subscribe

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}