Clojure is a Lisp-like programming language that runs on the JVM. Ring is a web application library that provides a simple framework for serving HTTP content with clojure. It is similar to Rack for Ruby or WSGI for Python.
In this post we will be creating a web application that serves up a 90s style hit-counter GIF image. Skip ahead to the demo (hosted on Heroku) or check out the source.
Leiningen
Leiningen is a tool for managing Clojure projects. It is highly recommend to use this for new clojure projects.
If you are on Mac OS X run the following command to install the lein tool or click here if you are on another platform.
brew install --devel leiningen
Counters from the 90s!
Remember those old sites with a hit-counter at the bottom of the page?
That is what we will be creating in clojure – a dynamic counter image serving web app. If you do not remember those hit-counters, maybe you can find some examples with the Wayback Machine!
Creating the project
The following command creates a new Clojure project and a source file at src/web_counter/core.clj.
lein new web-counter
The project.clj allows you to configure your Clojure project. It is similar in function (but not in syntax!) to a pom.xml (AHHH) or build.gradle.
(defproject web-counter "1.0.0-SNAPSHOT"
:description "web counters are so cool"
:dependencies [[org.clojure/clojure "1.3.0"]
[ring "1.1.8"]
[spy/spymemcached "2.8.1"]]
:plugins [[lein-ring "0.8.2"]]
:min-lein-version "2.0.0"
:repositories [["couchbase" {:url "http://files.couchbase.com/maven2/" :checksum :warn}]]
:ring {:handler web-counter.core/app :init web-counter.core/init})
We have added a few dependencies, notably ring and spymemcached.
The lein-ring plugin is important as it gives the lein command extra tasks specific to ring.
The ring key configures our web app:
- handler - Function that handles requests
- init - Function that will be called during servlet initialization
Request handler
The core of a ring application is the request handler. It takes a single argument, request, which contains all of the request attributes. In our handler we are only concerned with the uri, or the path of the request.
(defn app [request]
(case (:uri request)
"/counter"
(let [n (.incr memcached-client counter-key 1)]
{:status 200
:headers {"Content-Type" "image/gif"}
:body (ByteArrayInputStream. (make-gif-counter n))})
"/"
{:status 200
:headers {"Content-Type" "text/html"}
:body (str "<html><body><p>You are visitor number:</p><img src=\"/counter\"/></body></html>")}
{:status 404}))
The return of a request handler is a map containing information about the response. Above are some common response attributes:
- status - The HTTP response status code
- headers - A map of HTTP headers for the response
- body - Ring supports a few different types for this, such as: String, File, InputStream, etc,.
Our handler does the following:
- / - Returns a simple HTML page that has an tag pointing to the counter image
- /counter - Increments the counter and generates a GIF image
- _ - Returns 404 for any other requests
Counter Storage
The counter is stored in memcached (rather than in-memory) so that the application can be deployed on multiple instances (any shared storage will work, though).
First we make a forward declaration for a var named memcached-client. When the web application starts we will set the root binding for this var so that it can be used in our request handler.
(declare memcached-client)
This function creates a memcached client using the spymemcached library. The memcached server information is retrieved from environment variables. (The environment variables were used to work with the Heroku memcache addon)
(defn make-memcached-client []
(let [servers (map (fn [^String s]
(let [sp (.split s ":")
^String host (aget sp 0)
^Integer port (if (> (alength sp) 1) (Integer/parseInt (aget sp 1)) 11211)]
(InetSocketAddress. host port)))
(.split (System/getenv "MEMCACHE_SERVERS") ";"))
username (System/getenv "MEMCACHE_USERNAME")
password (System/getenv "MEMCACHE_PASSWORD")
callback-handler (PlainCallbackHandler. username password)
auth-descriptor (AuthDescriptor. (into-array ["PLAIN"]) callback-handler)
builder (-> (ConnectionFactoryBuilder.)
(.setAuthDescriptor auth-descriptor)
(.setProtocol (ConnectionFactoryBuilder$Protocol/BINARY)))
conn-factory (.build builder)]
(let [^MemcachedClient client (MemcachedClient. conn-factory servers)]
(if (nil? (.get client counter-key)) (.set client counter-key 0 0))
client)))
Below is the initialization function. In our project.clj we set :init to web-counter.core/init so that Ring knows which function to call. The function below just sets the binding for the var memcached-client to a valid memcached client instance.
(defn init []
(alter-var-root (var memcached-client) (fn [x] (make-memcached-client))))
Generating the GIF images
The counter image is generated by piecing together a series of digit images. The code below loads digit images from a folder in the resource path. By default lein expects resources to be in the resources directory. In our app we have a folder named odometer that contains 10 digit images named 0.gif, 1.gif…
(def counter-images
(reduce (fn [cache n]
(let [res (clojure.java.io/resource (str "odometer/" n ".gif"))]
(assoc cache (format "%d" n) (ImageIO/read res)))) {} (range 10)))
The var counter-images is bound to a map of digit -> counter image.
The function below generates the counter image by extracting the individual digits from the number and putting them into a single GIF.
(defn make-gif-counter [n]
(let [numStr (format "%d" n)
images (map #(get counter-images (String/valueOf %1)) (.toCharArray numStr))
width (reduce (fn [w ^BufferedImage img] (+ w (.getWidth img))) 0 images)
height (reduce (fn [h ^BufferedImage img] (max h (.getHeight img))) 0 images)
output (ByteArrayOutputStream.)
img (BufferedImage. width height BufferedImage/TYPE_BYTE_INDEXED)
^Graphics graphics (.createGraphics img)]
(loop [xOffset 0 digit-images images]
(when-let [^BufferedImage digit-image (first digit-images)]
(.drawImage graphics digit-image xOffset 0 nil)
(recur (+ xOffset (.getWidth digit-image)) (rest digit-images))))
(.dispose graphics)
(ImageIO/write img "GIF" output)
(.toByteArray output)))
The end
Clojure is a great language and Ring makes writing web applications extremely easy. If you have not tried clojure yet, I highly recommend you do!
Check out the counter in action here and grab the full source code here.