Pure Danger Tech


navigation
home

Learning Clojure #7: Compojure

16 Feb 2010

I had the need to whip up a little app last week to test some SPARQL queries and I took the opportunity to try out the Compojure web framework for Clojure (and the Leiningen build system also written in Clojure).

All of the functionality for this app is already available in the Jena semantic web framework and I’m merely exposing it in probably the ugliest web app you’ve ever seen.

Compojure is pretty raw from a framework perspective and that’s really part of the charm – there’s not a mountain of libraries or a ton of abstractions. There’s just enough structure that you can get http requests and responses and do what you want. In a language like Clojure, this is less daunting than might be in other languages.

You can find my entire web app (named Sparcl = SPARQL queries in Clojure) in one source file which is just 74 lines long. The file has a main in it that starts a server (Jetty in this case) and maps all urls to the “sparcl” servlet.

(defn -main [& args]
	(run-server {:port 8080}
		"/*" (servlet sparcl)))

Then almost the only requirement here from a Compojure point of view is to define your routes which are matched in order:

(defroutes sparcl
	(GET "/" (exec-page nil nil))
	(POST "/" (exec-page (params :rdf) (params :sparql)))
	(ANY "*" 404))

Here I’ve got the root mapped to both a GET (which returns an empty form) and a POST (where the query is executed and the results are also displayed. So, this is a super-simple one-page web app. Anything else gets dumped into a 404 error. There are lots of ways to customize all this as well.

In both cases here I’m just calling the exec-page function:

(defn exec-page [rdf sparql]
	(exec-html rdf sparql
		(if (strs-exist? [rdf sparql]) 
			(results-html (exec-sparql sparql (create-model rdf)))
			[:p "No query executed."])))

This function is first executing the SPARQL query (if there is one) in exec-sparql, formatting it into a chunk of html with results-html and passing the RDF, query, and results to the page function exec-html.

Breaking that down further, exec-sparql and create-model are just exercising various Jena APIs to build models, convert text to RDF, and execute the SPARQL:

(defn exec-sparql [sparql model] 
	(.execSelect 
		(. QueryExecutionFactory create 
			(. QueryFactory create sparql) 
			model) ))

(defn create-model [rdf]
	(let [model (ModelFactory/createDefaultModel)]	
		(.read model (StringReader. rdf) nil)
		model))

Nothing too interesting here, just Java interop (although it’s maybe a good example of a few different styles of that). I’m still trying to work out where’s idiomatic to use the different forms of .method, .. macro, etc.

Finally, there are the functions to format the results (which are delivered in an iterator-style JDBC ResultSet like API) and the final page. These are all doing effectively the same thing which is to produce a vector of vectors that can be passed to the html function (part of Clojure) that will convert the vectors into HTML for return. Each vector contains a tag at the beginning (like :html, :head, etc), optionally a map of attributes and the content (either text or nested vectors as needed).

This looks kind of like this:

(defn data-html [results]
	(for [solution (iterator-seq results)]
		[:tr
			(for [var-name (iterator-seq (.varNames solution))]
				[:td (.toString (.get solution var-name))])]))
	
(defn vars-html [results] 
	[:tr
		(for [binding (.getResultVars results)] 
			[:th binding])])
		
(defn results-html [results]
	[:table {:border "1"}
		(vars-html results) 
		(data-html results)])

(defn exec-html [rdf sparql results]
	(html [:html
			[:head
				[:title "SPARCL: SPARQL Query Tester"]]
			[:body
				(form-to [:post "/"]
					[:h2 "RDF:"]
					[:textarea {:name "rdf" :rows 10 :cols 80} rdf]
					[:h2 "SPARQL Query:"]
					[:textarea {:name "sparql" :rows 10 :cols 80} sparql]
					[:p (submit-button "Execute")]
					[:h2 "Results:"]
					results)]]))

Not pretty, for sure. There may be a templating engine included in Clojure or available separately but I have no idea and haven’t looked. That seems like an obvious thing to add. For me, this was enough. [UPDATE: @wmacgyver recommended Enlive for this.]

That’s pretty much all of the code other than a helper function or two. To build this sucker I’m using Leiningen, which in my case means using the provided “lein” script and a project.clj file:

(defproject sparcl "0.1.0"
  :description "SPARQL query tester"
  :dependencies [[com.hp.hpl.jena/jena "2.6.2"]
                 [com.hp.hpl.jena/iri "0.7"]
                 [com.hp.hpl.jena/arq "2.8.1"]
                 [org.slf4j/slf4j-api "1.5.6"]
                 [org.slf4j/slf4j-log4j12 "1.5.6"]
                 [log4j/log4j "1.2.13"]
                 [com.ibm.icu/icu4j "3.4.4"] 
                 [xerces/xercesImpl "2.7.1"] 
                 [compojure "0.3.2"]
                ]
  :main sparcl
)

This is somewhat Maven-esque and in fact the dependencies will be pulled from your local Maven repository or from the standard Clojure jar repo Clojars. The main tells Leiningen the namespace to look for the main to put in the created jar. So, those two files (the project.clj and the source in sparcl.jar) are effectively the total content of the project.

To build it just do ./lein uberjar which will download dependencies, build the clj files into classes, and package everything together into one self-contained web app jar file. You can run that jar with java -jar sparcl-standalone.jar. That starts a server listening on port 8080 and you’re ready to rock.

I hope this helped give a pretty concrete example of how to get started with Compojure and Leiningen. I’m NOT a web app kind of guy but I had a lot of fun with this because everything was so simple to put together. I can certainly see many ways to build something more complicated without too much more work. You can find the whole project here.