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

Let's Build a Bitbucket Add-on in Clojure!

DZone's Guide to

Let's Build a Bitbucket Add-on in Clojure!

Check out this tutorial on building a Bitbucket add-on in Clojure, with a look at the anatomy of a connect add-on, automatic versions, and more!

· Java Zone
Free Resource

Learn how our document data model can map directly to how you program your app, and native database features like secondary indexes, geospatial and text search give you full access to your data. Brought to you in partnership with MongoDB.

One the most exciting things about the Atlassian Connect add-on framework, for me at least, is that it removes the need to create add-ons in the language of the hosting application. With the recent release of Bitbucket support for Connect we now have the ability to not only extend Bitbucket in any way we see fit, but to also do it in whatever language or framework we desire. This opens us up to developing for Atlassian products in Haskell, Scala, Node.js, or anything else that supports the basic protocols of the web.

Personally I'm rather fond of Clojure, so when we wanted to produce a Docker Hub add-on for Bitbucket that was the technology I chose. But as this was the first Bitbucket Connect add-on written in Clojure I chose to port Tim Pettersen'sexample NodeJS Connect project first to work out any issues. This turned out to be a very useful process, so I thought I'd share it here in a series of posts.

Note: To streamline things I'm going to assume you already have basic knowledge of Clojure development. In particular I'll assume you already have a development environment of choice; this allows me to skip the details of working with Emacs/Cider, Vim/Fireplace, La Clojure, Cursive, etc. I'll leave that up to you. But other than that I'll introduce the fundamental components of a Clojure web-stack from the ground up.

Anatomy of a Connect Add-On

I won't reiterate all the details of how Connect works, but in essence the diagram shows that a Connect add-on is a web application written in any language you like, running on any stack you choose, in any location you want. Once registered with the Atlassian application, your web app can progressively enhance ours with new screens, features, and functions that appear directly embedded as if a part of our cloud. It can also enhance our app with new behind-the-scenes logic, all via REST APIs and Webhooks. All of Connect's components consist of standard web protocols and conventions, and, apart from a minor extension to the JWT spec, are supported by most languages and libraries out of the box. This is part of the power of Connect, and what gives it a true cross-platform experience. You can read more details of the Connect architecture here.

Let's Get Started

The first thing we need to define is our project properties, such as versioning, build structure, and dependencies. Clojure now has two competing yet complementary project tools: Leiningen and Boot. They take fundamentally different approaches to defining a project, and both (to me) have their attractions and use-cases. I'm going to use Leiningen for this project as it's the one I'm most familiar with, and it has better support for Maven-style project versioning, but Boot works just as well if you prefer it.

I'll assume you have the latest JDK installed along with the lein command on your path. If not, follow the appropriateinstructions for getting these on your platform of choice.

The first step is to have Leiningen create our project for us. Leiningen has a number of pre-made templates for web-based projects that will setup all the necessary dependencies and structures, the most popular being Luminus. However, full-featured "frameworks" that exist in other languages (such as Ruby on Rails, or Django) are not really part of the Clojure experience. The Clojure community prefers to build small, composable components that can be combined to provide framework-like functionality as needed. Even Luminus works this way; it's really just a ready-made combination of the current best-of-breed web components from elsewhere.

So in the true Clojure fashion, let's create a default project and construct our Connect add-on from the bottom-up...

[ssmith:~/projects] $ lein new app hello-connect
Generating a project called hello-connect based on the 'app' template.
[ssmith:~/projects] $ cd hello-connect/
[ssmith:~/projects/hello-connect] $ ls -al
total 32
drwxr-xr-x 12 ssmith staff   408 Nov 18 13:57 .
drwxr-xr-x  9 ssmith staff   306 Nov 18 13:57 ..
-rw-r--r--  1 ssmith staff    99 Nov 18 13:57 .gitignore
-rw-r--r--  1 ssmith staff   122 Nov 18 13:57 .hgignore
-rw-r--r--  1 ssmith staff   792 Nov 18 13:57 CHANGELOG.md
-rw-r--r--  1 ssmith staff 11218 Nov 18 13:57 LICENSE
-rw-r--r--  1 ssmith staff   239 Nov 18 13:57 README.md
drwxr-xr-x  3 ssmith staff   102 Nov 18 13:57 doc
-rw-r--r--  1 ssmith staff   273 Nov 18 13:57 project.clj
drwxr-xr-x  2 ssmith staff    68 Nov 18 13:57 resources
drwxr-xr-x  3 ssmith staff   102 Nov 18 13:57 src
drwxr-xr-x  3 ssmith staff   102 Nov 18 13:57 test
[ssmith:~/projects/hello-connect] $

We should also add version-control to our project by doing git init and doing an initial add/commit. It is also good practice to create a remote repository for backup; obviously, my preferred one is Bitbucket.

Setting Up project.clj

Leiningen produces a lot of boilerplate files for us. The main one of interest at this point is project.clj; this is the Leiningen project definition file. This is pretty basic at the moment:

(defproject hello-connect "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "Eclipse Public License"
            :url "http://www.eclipse.org/legal/epl-v10.html"}
   :dependencies [[org.clojure/clojure "1.7.0"]]
   :main ^:skip-aot hello-connect.core
  :target-path "target/%s"
  :profiles {:uberjar {:aot :all}})

Feel free to fill in the metadata as you see fit. And then we can start adding dependencies.

Quick Tip: Automatic Versions

As you can see from the org.clojure/clojure entry above Leiningen dependencies use Maven-style group/artifact/version specifications (in-fact Leiningen and Boot both reuse Maven's existing infrastructure). Normally we would now need to go off and find out the required versions of all our libraries and add them in here. However as we will generally use the latest version, we can short-cut this using lein ancient, a plugin to check your project for outdated dependencies and plugins. So the first thing we'll do is add this to our project:

:plugins [[lein-ancient "0.6.8"]]

Once we've added our dependencies we'll use this to fill in the blanks.

Adding HTTP/REST Support

The first dependency we'll add is clj-connect. This is a wrapper around clj-jwt that supports the Atlassian JWT extensions and defines some common operations for Connect add-ons, such as token retrieval:

[clj-connect]

As Connect add-ons operate as HTTP REST servers we'll also add dependencies on the standard ring-*/compojure Clojure HTTP stack. Compojure and Ring have some REST add-ons that support advanced tooling such as Swagger, but as we're not defining public API this is overkill, so we'll stick to the core APIs and add the following to our dependency list:

[ring]
[ring/ring-defaults]
[ring/ring-json]
[ring.middleware.logger]
[compojure]

Web Server

The preferred way to build small scalable web applications is to follow the 12 Factor guidelines. This means we'd like our add-on to be self-contained and serve its own content, which means embedding a HTTP server into the app. As Clojure is built on top of Java there are a number of embedded HTTP engines available, not least the venerable Aussie workhorse Jetty. However the Clojure web community is increasingly coalescing around the Immutant web module (which is build on top of Undertow), especially since version 2 when it became much more modular:

[org.immutant/web]

As a bonus the latest versions of Immutant/Undertow support HTTP/2. Immutant will require some manual invocation, which we will set-up in a bit.

Our First Build

Our project.clj now looks like:

(defproject hello-connect "0.1.0-SNAPSHOT"
  :description "A demo Connect Addon"
  :url "https://bitbucket.org/ssmith/hello-connect/"
  :license {:name "Eclipse Public License"
            :url "http://www.eclipse.org/legal/epl-v10.html"}
  :plugins [[lein-ancient "0.6.8"]]

  :dependencies [[org.clojure/clojure "1.7.0"]

                 [clj-connect]

                 [ring]
                 [ring/ring-defaults]
                 [ring/ring-json]
                 [ring.middleware.logger]
                 [compojure]
                 [org.immutant/web]]

  :main ^:skip-aot hello-connect.core
  :target-path "target/%s"
  :profiles {:uberjar {:aot :all}})

We can now get lein ancient to fill-in the latest versions for us. Ancient is quite conservative and will run your tests after updating the dependencies and revert if they fail. However Leiningen drops a dummy failing test in the test directory, so this will never work. It's possible to tell ancient to ignore the tests, but for now we can just delete the fake test:

[ssmith:~/projects/hello-connect] $ rm test/hello_connect/core_test.clj

Now we run ancient:

[ssmith:~/projects/hello-connect] $ lein ancient upgrade
[clj-connect "0.2.1"] is available but we use ""
[ring "1.4.0"] is available but we use ""
[ring/ring-defaults "0.1.5"] is available but we use ""
[ring/ring-json "0.4.0"] is available but we use ""
[ring.middleware.logger "0.5.0"] is available but we use ""
[compojure "1.4.0"] is available but we use ""
[org.immutant/web "2.1.1"] is available but we use ""

running test task ["test"] ...

lein test user

Ran 0 tests containing 0 assertions.
0 failures, 0 errors.
6/6 artifacts were upgraded.

And now our project.clj looks like:

(defproject hello-connect "0.1.0-SNAPSHOT"
  :description "A demo Connect Addon"
  :url "https://bitbucket.org/ssmith/hello-connect/"
  :license {:name "Eclipse Public License"
            :url "http://www.eclipse.org/legal/epl-v10.html"}

  :plugins [[lein-ancient "0.6.8"]]

  :dependencies [[org.clojure/clojure "1.7.0"]

                 [clj-connect "0.2.1"]

                 [ring "1.4.0"]
                 [ring/ring-defaults "0.1.5"]
                 [ring/ring-json "0.4.0"]
                 [ring.middleware.logger "0.5.0"]
                 [compojure "1.4.0"]

                 [org.immutant/web "2.1.1"]]

  :main ^:skip-aot hello-connect.core
  :target-path "target/%s"
  :profiles {:uberjar {:aot :all}})

You can also run ancient in interactive mode with lein ancient :interactive, which allows you to cherry-pick the updates you want.

Now we can have Leiningen fetch the dependencies for us:

[ssmith:~/projects/hello-connect] $ lein deps
Retrieving clj-connect/clj-connect/0.2.1/clj-connect-0.2.1.pom from clojars
Retrieving ring/ring-codec/1.0.0/ring-codec-1.0.0.pom from clojars
.
.
.
Retrieving org/immutant/web/2.1.1/web-2.1.1.jar from clojars
Retrieving org/immutant/core/2.1.1/core-2.1.1.jar from clojars

Creating a Runnable App

Now that we have our necessary dependencies we can get started on making our project into an actual working app. The first thing we'll need is a set of skeleton Ring routes. We'll add this into a new namespace called handler in src/hello_connect/handler.clj:

(ns hello-connect.handler
  (:require [clojure.tools.logging :as log]

            [ring.util.response :as response]
            [ring.middleware.defaults :refer [wrap-defaults site-defaults]]
            [compojure.core :refer :all]
            [compojure.route :as route]))

(defroutes app-routes
  (GET  "/" [] "Hello Connect!")
  (route/not-found {:status 404 :body "Not Found"}))


(def app
  ;; Disable anti-forgery as it interferes with Connect POSTs
  (let [connect-defaults (-> site-defaults
                             (assoc-in [:security :anti-forgery] false)
                             (assoc-in [:security :frame-options] false)) ]

    (-> app-routes
        (wrap-defaults connect-defaults))))

(defn init []
  (log/info "Initialising application"))

(defn shutdown []
  (log/info "Shutting down application"))

This defines three things; a set of routes called app, and two hooks to be called on startup and shutdown. Currently the only route setup is the root "/" one, which just returns dummy text. We'll expand on these later, primarily by adding entries to the defroutes section.

While this adds the skeleton app structure we still need an Immutant HTTP server for it to live in. To do this we'll use the existing core.clj namespace that was created by Leiningen; it contains a placeholder (-main) function but we'll replace that with the necessary Immutant/Undertow invocations:

(ns bb-example.core
  (:require [clojure.tools.logging :as log]

            [bb-example.handler :as handler]

            [immutant.web :as web]
            [immutant.web.middleware :refer [wrap-development]]

            [environ.core :refer [env]]))

(defonce server (atom nil))
(defonce port (env :port 3000))

(defn start-server [port]
  (handler/init)
  (reset! server (web/run (wrap-development handler/app) :port port)))

(defn stop-server []
  (when @server
    (handler/shutdown)
    (web/stop @server)
    (reset! server nil)))

(defn start-app []
  (.addShutdownHook (Runtime/getRuntime) (Thread. stop-server))
  (start-server port)
  (log/info "server started on port:" (:port @server)))

(defn -main [& args]
  (start-app))

We use the wrap-development middleware here as this gives us useful things such as auto-reloading of our code which helps with dynamic development.

One new library I'm introducing here is environ, which gives us convenient access to environment variables, in this case PORT. The practice of defining all our runtime configuration in the system environment is part of the 12 Factor philosophy. We'll be using this technique extensively in the following installments.

Note that I haven't explicitly added this to the dependencies; from this point I'll take updating these as a given. If in any doubt about where a library comes from please refer the final version in Bitbucket:

https://bitbucket.org/ssmith/hello-connect/src/HEAD/project.clj

So we can now take our server for spin; we can do this by invoking (-main) from our development REPL if we're using one, or via Leiningen:

[ssmith:~/projects/hello-connect] $ lein run
<snip logging info>
13:38:00.602 INFO  [org.projectodd.wunderboss.web.Web] (main) Registered web context /
13:38:00.607 INFO  [hello-connect.core] (main) server started on port: 3000

Now if you visit http://localhost:3000 you should see our "Hello Connect!" message.

That's a Wrap on Part 1

Now we have the basic server up and running that's probably a good point to take a break. So far nothing we've done is specific to Atlassian Connect. That will change in the next installment when we introduce the Connect descriptor and look at some of the options for templating and routing requests.

This section of the tutorial has introduced a lot of Clojure technologies. While I've sought to justify my decisions I'm always interested in hearing about how others use Clojure in practice. Feel free to add your tips in the comments.

See you next time.

Originally written by Steve Smith

Discover when your data grows or your application performance demands increase, MongoDB Atlas allows you to scale out your deployment with an automated sharding process that ensures zero application downtime. Brought to you in partnership with MongoDB.

Topics:
closure ,bitbucket

Published at DZone with permission of Ian Buchanan, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

THE DZONE NEWSLETTER

Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

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

X

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

{{ parent.tldr }}

{{ parent.urlSource.name }}