Question

I'm using Clojure to build a sample web app. I'm using the lein ring plugin and compojure for routing.

When I run the app using lein ring server everything works fine. I can browse to localhost:3000/items and everything looks good all css & js is loaded.

However when I build an uberwar (lein ring uberwar webdev.war) and deploy that to Tomcat the routing is wrong.

Once deployed I browse to localhost:8080/webdev/items

The css/js files don't get loaded because the path to them does not include the context "webdev".

Similarly my forms defined in Hiccup are trying to post back to /items which again does not include the context webdev.

Here's my project.clj, core.clj and views.clj

Project.clj:

(defproject webdev "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.5.1"]
                 [ring "1.2.2"]
                 [hiccup "1.0.5"]
                 [compojure "1.1.6"]
                 [org.clojure/java.jdbc "0.3.3"]
                 [postgresql/postgresql "9.1-901.jdbc4"]]

  :plugins [[lein-ring "0.8.10"]]

  :ring {:init webdev.core/create-db-schema
         :handler webdev.core/app
         :servlet-path-info? true
        }
  )

Core.clj

(ns webdev.core
  (:require [webdev.item.model :as items])
  (:require [webdev.item.handler :refer [handle-index-items
                                         handle-create-item
                                         handle-delete-item
                                         handle-update-item]])
  (:require
            [hiccup.middleware :refer [wrap-base-url]]
            [ring.middleware.reload :refer [wrap-reload]]
            [ring.middleware.params :refer [wrap-params]]
            [ring.middleware.resource :refer [wrap-resource]]
            [ring.middleware.file-info :refer [wrap-file-info]]
            [compojure.core :refer [defroutes ANY GET POST PUT DELETE]]
            [compojure.route :as route]
            [compojure.handler :as handler]
            [ring.handler.dump :refer [handle-dump]]))

(defn greet [req]
  {:status 200
   :body "Hello, World!!!!!!!!!!!!!"
   :headers {}})

(defroutes routes
  (GET "/" [] greet)

  (ANY "/request" [] handle-dump)

  (GET "/items" [] handle-index-items)
  (POST "/items" [& params] (handle-create-item params))
  (DELETE "/items/:item-id" [item-id] (handle-delete-item item-id))
  (PUT "/items/:item-id" [& params] (handle-update-item params))

  (route/resources "/" {:root "static"})

  (route/not-found "Page not found."))


(defn create-db-schema []
  (items/create-table))


(defn wrap-server-response [hndlr]
  (fn [req]
    (let [response (hndlr req)]
      (assoc-in response [:headers "Server:"] "my-server"))))


(def sim-methods {"PUT"     :put
                  "DELETE"  :delete})

(defn wrap-simulated-methods [hndlr]
  (fn [req]
    (if-let [method (and (= :post (:request-method req))
                         (sim-methods (get-in req [:params "_method"])))]
      (hndlr (assoc req :request-method method))
      (hndlr req))))

(def app-routes
  (wrap-base-url
    (wrap-file-info
      (wrap-server-response
       (wrap-params
        (wrap-simulated-methods routes))))))

(def app
  (handler/site app-routes))

View.clj

(ns webdev.item.view
  (:require [hiccup.page :refer [html5]]
            [hiccup.core :refer [html h]]))


(defn new-item []
  (html
   [:form.form-horizontal
    {:method "POST" :action "/items"}
     [:div.form-group
      [:label.control-label.col-sm-2 {:for :name-input}
       "Name"]
      [:div.col-sm-10
       [:input#name-input.form-control
        {:name :name :placeholder "Name"}]]]
   [:div.form-group
    [:label.control-label.col-sm-2 {:for :desc-input}
     "Description"]
    [:div.col-sm-10
     [:input#desc-input.form-control
      {:name :description :placeholder "Description"}]]]
   [:div.form-group
    [:div.col-sm-offset-2.col-sm-10
     [:input.btn.btn-primary
      {:type :submit
       :value "New item"}]]]]))


(defn delete-item-form [id]
  (html
   [:form.form-horizontal
    {:method "POST" :action (str "/items/" id)}
    [:div.form-group
     [:input {:type :hidden
              :name "_method"
              :value "DELETE"}]
     [:div.col-sm-offset-2.col-sm-10
      [:input.btn.btn-danger.btn-xs
       {:type :submit
        :value "Delete" }]]]]))


(defn update-checked-form [id checked]
  (html
   [:form.form-horizontal
    {:method "POST" :action (str "/items/" id)}
    [:div.form-group
     [:input {:type :hidden
              :name "_method"
              :value "PUT"}]
     [:input {:type :hidden
              :name "checked"
              :value (str (not checked))}]
     [:div.col-sm-offset-2.col-sm-10
      [:input.btn.btn-primary.btn-xs
       {:type :submit
        :value (if checked "Done" "To-do")}]]]]))


(defn items-page [items]
  (html5 {:lang :en}
         [:head
           [:title "CLOJURE Web Dev Tutorial"]
           [:meta {:name :viewport :content "width=device-width, initial-scale=1.0"}]
           [:link {:href "/bootstrap/css/bootstrap.min.css"
                   :rel :stylesheet}]]
         [:body
          [:div.container
            [:h1 "My Items"]
            [:div.row
             (if (seq items)
               [:table.table.table-striped
                [:thead
                 [:tr
                  [:th "Name"]
                  [:th "Description"]
                  [:th.col-sm-2]
                  [:th.col-sm-2]]]
                 [:tbody
                  (for [i items]
                    [:tr
                     [:td (h (:name i))]
                     [:td (h (:description i))]
                     [:td (update-checked-form (:id i) (:checked i))]
                     [:td (delete-item-form (:id i))]])]]
               [:div.col-sm-offset-1 "There are no items."])]
          [:div.col-sm-6
           [:h2 "Create a new item"]
           (new-item)]]
          [:script {:src "http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"}]
          [:script {:src "/bootstrap/js/bootstrap.min.js"}]]))

I saw this related question but I didn't seem to help

How can I fix this problem?

Was it helpful?

Solution

Ok, after much 'googling' and experimentation I seem to have come up with some reasonable solutions to my problems.

I'm very new to Clojure and I followed along with the example from this excellent video tutorial.

Then I amended the example because I wanted to try and deploy the app to Tomcat.

That's when the trouble started. As I stated in my question, if I ran the app via lein ring server whilst I was developing, then all was good. But I wanted to deploy my app to Tomcat and in order to do that I needed to build an uberwar.

The lein ring uberwar webdev.war command is just what I need.

Once I deployed the app to Tomcat the following problems occurred

  1. The css and js files stopped being served.
  2. The forms were not posting back to the correct URL
  3. The redirects after form posts were also attempting to redirect to the incorrect URL

What I hadn't noticed before was that when an uberwar is built then a :context key is added to the request map. (I think you also have to use the wrap-base-url middleware in order for the key to get added to the request map)

In order to get the css and js files served I needed to use include-css and include-js functions from the hiccup.page namespace.

So instead of using

[:script {:src "http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"}]
          [:script {:src "/bootstrap/js/bootstrap.min.js"}]

in my views.clj. I used

(include-css "/bootstrap/css/bootstrap.min.css")
(include-js "http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"
                       "/bootstrap/js/bootstrap.min.js")

These functions obviously respect the :context key and my css and js files get served correctly.

The next issue was my forms were not posting back to the correct URL. So once again I needed to change the code in my views. I now needed to define my forms using the form/form-to from the hiccup.form namespace.

(form/form-to [:post (str "/items/" id)

These functions are also obviously :context aware.

Finally I needed to fix the redirects after the form posts. To achieve this I needed to change the handler signatures of handle-create-item, handle-delete-item and handle-update-item and the appropriate route definitions so that they passed the request map in addition to the other parameters the handlers required. Then I just created a little helper function

defn items-list [req]
  (str (:context req) "/items"))

that plucked out the :context from the request map and prepended it to the route.

Once I made those alterations my app now runs correctly locally via lein ring server when I'm developing and also when its deployed to a Tomcat server.

I hope this might help others faced with similar issues. The complete source code can be found here at my github account

OTHER TIPS

Thanks for publishing your findings - very helpful as I'm at the same sort of stage as you. I would comment other than for my current StackExchange level, but just to add there are also the Hiccup functions link-to and image in the element name space rather than using the naked html for these.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top