Tech behind Tech

Raw information. No finesse :)

Introduction to Clojure Web Development using Ring, Compojure and Sandbar

with 14 comments


I gave an introduction to clojure web development presentation to bay area clojure user group. As my slides do not have much information, I am writing this blog so everyone can follow and get started with a sample clojure web application using compojure.

Setting Up:

 lein new address_book 

Add Compojure to Project:

Edit project.clj

(defproject address_book "1.0.0-SNAPSHOT"
  :description "Runnering log"
  :dependencies [[org.clojure/clojure "1.2.0"]
                 [org.clojure/clojure-contrib "1.2.0"]
                 [ring/ring-jetty-adapter "0.2.5"]
                 [compojure "0.4.1"]
                 [hiccup "0.2.6"]
                 [sandbar "0.3.0-SNAPSHOT"]
                 [clj-json "0.3.1"]]
  :dev-dependencies [[swank-clojure "1.3.0-SNAPSHOT"]])

Run

lein deps

Test whether our setup works:

Edit src/address_book/core.clj

(ns address-book.core
  (:use [compojure.core]
        [ring.adapter.jetty])
  (:require [compojure.route :as route]))

(defroutes rts
  (GET "/" [] "Address Book!!")
  (route/not-found "Page not found"))

(def application-routes
     rts)

(defn start []
  (run-jetty application-routes {:port 8080
                                 :join? false}))

Goto http://localhost:8080 and you should see “Address Book”

Interactive Development:

In core.clj, change start function to

(defn start []
  (run-jetty application-routes {:port 8080
                                 :join? false}))

Add Static Folders:

mkdir -p public/{css,js}

Create public/index.html

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"	"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">	
 <head>		
   <meta http-equiv="content-type" content="text/html; charset=utf-8" />		
   <title>My Address book</title>		
   <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>	
 </head>
 <body id="main">    
    <p>      My Address Book    </p>  
 <body>
</html>

Try to access http://localhost:8080/index.html. This will fail, as we never told compojure how to handle static files.

Edit core.clj and replace rts

(defroutes rts
  (GET "/" [] "Address Book!!")
   (route/files "/")
  (route/not-found "Page not found"))

Now when you access http://localhost:8080/index.html, it should display correctly.

Ring:

Three components

1) Handlers:
Handlers are main functions that process a request. We define handlers using defroutes macro.

2) Middleware:
Middleware are functions that could be chained together to process a request. Middleware functions can take any number of arguments, but the spec stats that first argument should be an handler and function should return an handler. An example for middleware is logging all requests that comes to your webserver. Ring and compojure comes with some standard middleware. We will see in next part how to create our own middleware.

3) Adapters:
Adapters are functions could adapt our handler to a web server. We are using jetty adapter to tie our handler to jetty server.

Lets create our Middleware:

Create src/address_book/middleware.clj

(ns address-book.middleware)

(defn- log [msg & vals]
  (let [line (apply format msg vals)]
    (locking System/out (println line))))

(defn wrap-request-logging [handler]
  (fn [{:keys [request-method uri] :as req}]
    (let [resp (handler req)]
      (log "Processing %s %s" request-method uri)
      resp)))

Edit core.clj ns form

(ns address-book.core
  (:use [compojure.core]
        [ring.adapter.jetty])
  (:require [compojure.route :as route]
			[address-book.middleware :as mdw]))

Edit core.clj application-routes def

(def application-routes
     (-> rts
	     mdw/wrap-request-logging))

Now when you access your server, you should see request log messages.

Main sourcecode:

Create address_book/address.clj

(ns address-book.address
  (:import [java.util Date])
  (:refer-clojure :exclude (find create)))

(def STORE (atom {:1 {:id :1 :name "Siva Jagadeesan" :street1 "88 7th" :street2 "#203" :city "Cupertino" :country "USA" :zipsourcecode 98802}}))

(defn to-keyword [num]
  (if-not (keyword? num)
    (keyword (str num))
    num))

(defn random-number []
  (to-keyword (.getTime (Date.))))

(defn create [attrs]
  (let [id (random-number)
        new-attrs (merge {:id id} attrs)]
    (swap! STORE merge {id new-attrs})
    new-attrs))

(defn find-all []
  (vals @STORE))

(defn find [id]
  ((to-keyword id) @STORE))

(defn update [id attrs]
  (let [updated-attrs (merge (find id) attrs)]
    (swap! STORE assoc id updated-attrs)
    updated-attrs))

(defn delete [id]
  (let [old-attrs (find id)]
    (swap! STORE dissoc id)
    old-attrs))

Edit core.clj ns form

(ns address-book.core
  (:use [compojure.core]
        [ring.adapter.jetty])
  (:require [compojure.route :as route]
			[address-book.middleware :as mdw]
            [address-book.address :as address]
            [clj-json.core :as json]))

Add this function to core.clj

(defn json-response [data & [status]]
  {:status (or status 200)
   :headers {"Content-Type" "application/json"}
   :body (json/generate-string data)})

Add these routes to core.clj

(GET "/addresses" [] (json-response (address/find-all)))
(GET "/addresses/:id" [id] (json-response (address/find id)))
(POST "/addresses" {params :params}  (json-response (address/create params)))

Replace your public folder with https://files.me.com/sivajag/sh35fq

This folder has needed css and js files.

Now go to http://localhost:8080/index.html and you should see a address book webapp. You can add and view addresses.

Authentication:

Create src/address_book/auth.clj

(ns address-book.auth
  (:use [sandbar.form-authentication ]
        [sandbar.validation]))

(defrecord AuthAdapter []
  FormAuthAdapter
  (load-user [this username password]
             (cond (= username "example")
                   {:username "example" :password "password" :roles #{:user}}))
  (validate-password [this]
                     (fn [m]
                       (if (= (:password m) "password")
                         m
                         (add-validation-error m "Unable to authenticate user.")))))

(defn form-authentication-adapter []
  (merge
   (AuthAdapter.)
   {:username "Username"
    :password "Password"
    :username-validation-error "You must supply a valid username."
    :password-validation-error "You must supply a password."
    :logout-page "/"}))

Edit core.clj ns form


(ns address-book.core
  (:use [compojure.core]
        [ring.adapter.jetty]
        [sandbar.auth]
        [sandbar.form-authentication ]
        [sandbar.validation ])
  (:require [compojure.route :as route]
			[address-book.middleware :as mdw]
            [address-book.address :as address]
            [address-book.auth :as auth]
            [clj-json.core :as json]))

Add this def to core.clj

(def security-policy
  [#".*\.(css|js|png|jpg|gif|ico)$" :any
   #"/login.*" :any
   #"/logout.*" :any
   #"/permission-denied.*" :any
   #"/addresses" :user
   #"/index.html" :user
   #"/" #{:user}])

Add this route to core.clj

 (form-authentication-routes (fn [_ c] (html c)) (auth/form-authentication-adapter))

Change application routes function in core.clj

(def application-routes
     (-> rts
        (with-security security-policy form-authentication)  		 
        wrap-stateful-session
	mdw/wrap-request-logging))

Now when you access http://localhost:8080/index.html it will take you to login page. You can login using “example” and “password”.

That is it folks. A simple web app using clojure.

I am sure this blog could be improved a lot. Please leave comments I will update this blog with your feedback.

Written by Siva Jagadeesan

January 19, 2011 at 12:36 am

Posted in Clojure

Tagged with , , , ,

14 Responses

Subscribe to comments with RSS.

  1. Hey Siva
    yet another great post, thanks!

    — Ben

    Ben

    January 21, 2011 at 4:45 am

  2. You haven’t mentioned anywhere how we run the server. I tried ./compojure src/address_book/core.clj, but that just quits without any error

    ScriptDevil

    March 20, 2011 at 12:48 am

  3. I noticed that you don’t direct anything to port 8080. How is data sent to the port? What sends it?

    lunamystry

    March 24, 2011 at 1:00 am

  4. It’s more succint to use:

    (run-jetty #’application-routes

    or

    (run-jetty (var application-routes)

    instead of

    10 (def application-routes
    11 rts)

    14 (run-jetty application-routes

    Sergey

    June 9, 2011 at 5:50 am

  5. Oop, I mean:

    14 (run-jetty #’rts

    Sergey

    June 9, 2011 at 5:51 am

  6. Sorry, now I see your intentions to place middleware in application-routes.

    But then you miss the ability to reload handlers on the fly because you don’t do “(var rts)”, don’t you?

    Sergey

    June 9, 2011 at 5:56 am

  7. In following this post through, I found I needed to modify the final ‘ns’ form to:

    (ns address-book.core
    (:use [compojure.core]
    [ring.adapter.jetty]
    [hiccup.core] ;; note: added
    [sandbar.auth]
    [sandbar.form-authentication]
    [sandbar.stateful-session] ;; note: added
    [sandbar.validation])
    (:require [compojure.route :as route]
    [address-book.middleware :as mdw]
    [address-book.address :as address]
    [address-book.auth :as auth]
    [clj-json.core :as json]))

    The post also doesn’t specify where the form-authentication-routes form goes; in fact it needs to go in the defroutes form, thus:

    (defroutes rts
    (GET “/” [] “Address Book!!”)
    (GET “/addresses” [] (json-response (address/find-all)))
    (GET “/addresses/:id” [id] (json-response (address/find id)))
    (POST “/addresses” {params :params} (json-response (address/create params)))
    (form-authentication-routes (fn [_ c] (html c)) (auth/form-authentication-adapter))
    (route/files “/”)
    (route/not-found “Page not found”))

    Altogether an excellent and most useful tutorial, many thanks!

    Simon Brooke

    June 28, 2011 at 4:03 am

  8. Two more minor things: the tutorial security-policy allows access to ‘addresses’ but not to ‘addresses.*'; consequently an attempt to edit an address results in ‘Processing :get /permission-denied’ appearing in the log (no error shown to the user).

    When that change is made, an attempt to edit an address results in ‘Processing :get /addresses/1′ appearing in the log but no change appears on the screen – the address is /not/ loaded into the form. However as browsing to ‘http://localhost:8080/addresses/1′ results in

    ‘{“id”:”1″,”name”:”Siva Jagadeesan”,”street1″:”88 7th”,”street2″:”#203″,”city”:”Cupertino”,”country”:”USA”,”zipsourcecode”:98802}’

    appearing in the browser, I suspect this is an issue either of templating or JavaScript rather than of authentication.

    Still investigating, will post a fix when I find it.

    Simon Brooke

    June 28, 2011 at 4:41 am

  9. OK, the problem is, there is no ‘edit-address-form’ so that the anonymous function defined at line 48 of address_list.js never gets called. If in both address_book.js and address_list.js, you replace all instances of ‘edit-address-form’ with ‘address-form’, the address to be edited gets loaded into the form on RHS of the index.html page. However, when the address is edited and the ‘create’ button pressed, although the log shows ‘Processing :put /addresses/1′, the address does not get updated.

    Still investigating.

    Simon Brooke

    June 28, 2011 at 5:32 am

  10. I try to run “http://localhost:8080″ and I get an error. I run Vista OS What makes the server active?
    Thanks

    jvandal (@jvandal)

    August 14, 2011 at 3:50 pm

    • In src/address_book/core.clj there is a start function. You can call that in a repl to start the server.

      Siva Jagadeesan

      August 19, 2011 at 2:56 pm

  11. Siva, great post!

    When I run “lein run address_book.core start”, lein reports the following error.

    Exception in thread “main” java.lang.NoSuchMethodError: clojure.lang.RestFn.(I)V (params.clj:1)
    at clojure.lang.Compiler.eval(Compiler.java:5440)
    at clojure.lang.Compiler.eval(Compiler.java:5415)

    Stack trimmed. Any thoughts on this error?

    Thanks!

    Mark

    October 18, 2011 at 9:50 pm


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 146 other followers

%d bloggers like this: