├── webjure ├── src │ ├── init.clj │ ├── test │ │ └── resources │ │ │ ├── message.cpt │ │ │ ├── repeat-binding.cpt │ │ │ ├── simple.cpt │ │ │ └── inbox.cpt │ ├── main │ │ └── resources │ │ │ ├── profiler │ │ │ ├── profiler.js │ │ │ ├── report.xml │ │ │ └── profiler.css │ │ │ └── crud │ │ │ ├── generic-listing-template.cpt │ │ │ └── crud-styles.cpt │ ├── webjure │ │ ├── servlet.clj │ │ ├── sql │ │ │ ├── crud │ │ │ │ └── editors.clj │ │ │ └── crud.clj │ │ ├── html.clj │ │ ├── sql.clj │ │ ├── xml │ │ │ └── feeds.clj │ │ ├── json.clj │ │ ├── csv.clj │ │ ├── profiler.clj │ │ ├── xml.clj │ │ └── cpt.clj │ └── webjure.clj ├── test │ └── webjure │ │ ├── html │ │ └── test.clj │ │ ├── cpt │ │ └── test.clj │ │ └── csv │ │ └── test.clj └── project.clj ├── demos ├── src │ └── main │ │ ├── resources │ │ ├── var.cpt │ │ ├── function.cpt │ │ ├── docs.cpt │ │ ├── page.cpt │ │ └── docs-pdf.cpt │ │ ├── webapp │ │ ├── resource │ │ │ ├── images │ │ │ │ ├── img01.gif │ │ │ │ ├── img02.gif │ │ │ │ ├── img03.gif │ │ │ │ ├── img04.gif │ │ │ │ └── spacer.gif │ │ │ ├── repl.js │ │ │ └── default.css │ │ └── WEB-INF │ │ │ └── web.xml │ │ └── clojure │ │ └── webjure │ │ ├── wiki.clj │ │ ├── demos.clj~ │ │ └── demos.clj └── pom.xml ├── .classpath ├── .gitignore ├── .project ├── LICENSE ├── pom.xml ├── README.markdown └── changes.txt /webjure/src/init.clj: -------------------------------------------------------------------------------- 1 | 2 | 3 | (require 'swank.swank) 4 | (swank.swank/start-repl 4005) -------------------------------------------------------------------------------- /demos/src/main/resources/var.cpt: -------------------------------------------------------------------------------- 1 |
2 | var: $(:name var)
3 | $(:doc var) 4 |
-------------------------------------------------------------------------------- /demos/src/main/webapp/resource/images/img01.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tatut/Webjure/HEAD/demos/src/main/webapp/resource/images/img01.gif -------------------------------------------------------------------------------- /demos/src/main/webapp/resource/images/img02.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tatut/Webjure/HEAD/demos/src/main/webapp/resource/images/img02.gif -------------------------------------------------------------------------------- /demos/src/main/webapp/resource/images/img03.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tatut/Webjure/HEAD/demos/src/main/webapp/resource/images/img03.gif -------------------------------------------------------------------------------- /demos/src/main/webapp/resource/images/img04.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tatut/Webjure/HEAD/demos/src/main/webapp/resource/images/img04.gif -------------------------------------------------------------------------------- /demos/src/main/webapp/resource/images/spacer.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tatut/Webjure/HEAD/demos/src/main/webapp/resource/images/spacer.gif -------------------------------------------------------------------------------- /webjure/src/test/resources/message.cpt: -------------------------------------------------------------------------------- 1 | 2 | 3 | Title: $(:title m)
4 | Message: $(:body m)
5 |
6 | -------------------------------------------------------------------------------- /webjure/src/test/resources/repeat-binding.cpt: -------------------------------------------------------------------------------- 1 | 2 | 3 | $value 4 | -------------------------------------------------------------------------------- /demos/src/main/resources/function.cpt: -------------------------------------------------------------------------------- 1 |
2 | $(:name var) (function)
3 | arglists: 4 | 5 |
   $(str al) 6 |
7 |
8 | $(:doc var) 9 |
-------------------------------------------------------------------------------- /.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /webjure/src/test/resources/simple.cpt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Nothign in here will be output... the whole element will be replaced by a number 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | demos/src/main/clojure/webjure/*~ 2 | demos/src/main/resources/*~ 3 | webjure/src/main/clojure/webjure/*~ 4 | webjure/src/test/clojure/webjure/cpt/*~ 5 | webjure/src/main/clojure/webjure/sql/*~ 6 | webjure/src/test/resources/*~ 7 | webjure/src/main/resources/crud/*~ 8 | webjure/src/main/resources/profiler/*~ 9 | webjure/src/*~ 10 | webjure/src/webjure/*~ 11 | webjure/*~ 12 | demos/target/ 13 | webjure/target/ 14 | .gitignore~ 15 | derby.log -------------------------------------------------------------------------------- /webjure/src/test/resources/inbox.cpt: -------------------------------------------------------------------------------- 1 | 2 | 3 | Inbox 4 | 5 |
6 |

Unnumbered list of messages

7 | 8 | There are no messages! 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /webjure/test/webjure/html/test.clj: -------------------------------------------------------------------------------- 1 | (ns webjure.html.test 2 | (:refer-clojure) 3 | (:use clojure.test) 4 | (:use webjure.html)) 5 | 6 | 7 | (deftest simple-element 8 | (is (= (html-format `(:a {:href "foo"} "something")) 9 | "something"))) 10 | 11 | (deftest empty-element 12 | (is (= (html-format `(:br)) 13 | "
"))) 14 | 15 | (deftest escaped-text 16 | (is (= (html-format `(:div "< and > are escaped here")) 17 | "
< and > are escaped here
"))) 18 | 19 | (deftest raw-text 20 | (is (= (html-format `(:div ~(fn [] "< and > are not escaped here"))) 21 | "
< and > are not escaped here
"))) -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | webjure 4 | 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | org.devzuz.q.maven.jdt.core.mavenIncrementalBuilder 15 | 16 | 17 | 18 | 19 | 20 | org.eclipse.jdt.core.javanature 21 | org.devzuz.q.maven.jdt.core.mavenNature 22 | 23 | 24 | -------------------------------------------------------------------------------- /demos/src/main/resources/docs.cpt: -------------------------------------------------------------------------------- 1 | 2 | 3 | Webjure docs 4 | 5 | 6 |

Documentation on all public vars in the webjure namespace

7 | PDF version 8 |
9 |
10 |
11 | documentation for a function 12 |
13 |
14 | documentation for other vars 15 |
16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /webjure/project.clj: -------------------------------------------------------------------------------- 1 | (defproject webjure "0.9.1-SNAPSHOT" 2 | :description "Webjure - a Clojure web framework" 3 | :dependencies [[org.clojure/clojure "1.2.0"] 4 | [org.clojure/clojure-contrib "1.2.0"]] 5 | :dev-dependencies [[leiningen/lein-swank "1.2.0-SNAPSHOT"] 6 | [javax.servlet/servlet-api "2.4"] 7 | [org.mortbay.jetty/jetty "6.1.26"]] 8 | :aot [webjure 9 | webjure.servlet 10 | webjure.sql webjure.sql.crud webjure.sql.crud.editors 11 | webjure.cpt 12 | webjure.html 13 | webjure.xml webjure.xml.feeds 14 | webjure.json 15 | webjure.csv 16 | webjure.profiler 17 | ;;webjure.websocket 18 | ] 19 | :repl-init-script "src/init.clj" 20 | :omit-source true 21 | ) -------------------------------------------------------------------------------- /webjure/src/main/resources/profiler/profiler.js: -------------------------------------------------------------------------------- 1 | var WebjureMiniProfiler = { 2 | 3 | init: function(requestId, fShowImmediately) { 4 | // Fetch profile results for any ajax calls 5 | // (see http://code.google.com/p/mvc-mini-profiler/source/browse/MvcMiniProfiler/UI/Includes.js) 6 | $(document).ajaxComplete(function (e, xhr, settings) { 7 | if (xhr) { 8 | var requestId = xhr.getResponseHeader('X-MiniProfiler-Id'); 9 | if (requestId) { 10 | WebjureMiniProfiler.fetch(requestId); 11 | } 12 | } 13 | }); 14 | 15 | $().ready(function() { WebjureMiniProfiler.fetch(requestId, fShowImmediately) }); 16 | }, 17 | 18 | fetch: function(requestId, fShowImmediately) { 19 | $.get("/_webjure_profiler/request", { "id": requestId }, 20 | function(data) { 21 | $('body').append(data); 22 | }); 23 | } 24 | }; 25 | 26 | -------------------------------------------------------------------------------- /webjure/src/webjure/servlet.clj: -------------------------------------------------------------------------------- 1 | (ns webjure.servlet 2 | (:refer-clojure) 3 | (:use webjure) 4 | (:import (javax.servlet ServletConfig ServletException UnavailableException)) 5 | (:import (javax.servlet.http 6 | HttpServlet HttpServletRequest HttpServletResponse)) 7 | (:gen-class :name webjure.servlet.WebjureServlet 8 | :extends javax.servlet.http.HttpServlet 9 | :exposes-methods {init superInit 10 | })) 11 | 12 | 13 | (defn -doGet [this ^HttpServletRequest request ^HttpServletResponse response] 14 | (dispatch "GET" request response)) 15 | 16 | (defn -doPost [this ^HttpServletRequest request ^HttpServletResponse response] 17 | (dispatch "POST" request response)) 18 | 19 | (defn -init 20 | ([this] (.log this "Webjure servlet initialized.")) 21 | ([this ^ServletConfig config] 22 | (let [start-ns (.getInitParameter config "startupNamespace")] 23 | (when start-ns 24 | (require (symbol start-ns)))) 25 | (.superInit this config))) 26 | 27 | 28 | -------------------------------------------------------------------------------- /demos/src/main/webapp/resource/repl.js: -------------------------------------------------------------------------------- 1 | 2 | var req; 3 | function replCallback(txt) { 4 | if(req.readyState == 4) { 5 | if(req.status == 200) { 6 | if(req.responseText.length > 0) appendContent('=> '+req.responseText); 7 | readRepl(); 8 | } else { 9 | alert('Unable to read repl: '+req.statusText); 10 | } 11 | } 12 | } 13 | 14 | function readRepl() { 15 | req = new XMLHttpRequest(); 16 | req.onreadystatechange = replCallback; 17 | req.open('GET', '/webjure-demos/ajaxrepl-out', true); 18 | req.send(null); 19 | } 20 | 21 | function appendContent(txt) { 22 | var elt = document.getElementById('replout'); 23 | elt.innerHTML += txt + '\n'; 24 | elt.scrollTop = elt.scrollHeight; 25 | } 26 | 27 | function keyHandler(event) { if(event.keyCode == 13) write(); } 28 | function write() { 29 | var elt = document.getElementById('replin'); 30 | var r = new XMLHttpRequest(); 31 | r.open('POST', '/webjure-demos/ajaxrepl-in', true); 32 | appendContent(elt.value); 33 | r.send(elt.value); 34 | elt.value = ''; 35 | } 36 | window.onload = readRepl; 37 | 38 | 39 | -------------------------------------------------------------------------------- /webjure/test/webjure/cpt/test.clj: -------------------------------------------------------------------------------- 1 | (ns webjure.cpt.test 2 | (:refer-clojure) 3 | (:use clojure.test) 4 | (:use webjure.cpt)) 5 | 6 | (set! *warn-on-reflection* true) 7 | 8 | ;; Very simple template 9 | (define-template simple1 "src/test/resources/simple.cpt") 10 | 11 | ;; A template that loops over messages and includes 12 | ;; a subtemplate for each item 13 | (define-template inbox "src/test/resources/inbox.cpt") 14 | 15 | (define-template repeat-binding "src/test/resources/repeat-binding.cpt") 16 | 17 | (defn t [template here] 18 | (with-out-str (template here))) 19 | 20 | (deftest simple-template 21 | (is (= (t simple1 [1 2 3]) "\n123\n"))) 22 | 23 | 24 | (deftest test-inbox 25 | (is (= (t inbox {:messages [ {:title "Welcome to CPT" :body "something or other..."} 26 | {:title "Another message" :body "lorem ipsum etc"} ]}) 27 | "\n Inbox\n \n
\n

Unnumbered list of messages

\n \n\t\n\t\n Title: Welcome to CPT
\n Message: something or other...
\n
\n Title: Another message
\n Message: lorem ipsum etc
\n
\n
\n
\n \n"))) 28 | 29 | (deftest test-repeat-binding 30 | (is (= (t repeat-binding [[1 "one"] [2 "two"]]) 31 | "\nonetwo\n"))) -------------------------------------------------------------------------------- /webjure/src/webjure/sql/crud/editors.clj: -------------------------------------------------------------------------------- 1 | ;; 2 | ;; Inline editor support implementations 3 | ;; 4 | 5 | 6 | (ns webjure.sql.crud.editors 7 | (:refer-clojure) 8 | (:use webjure.sql) 9 | (:use webjure) 10 | (:use webjure.sql.crud)) 11 | 12 | 13 | ;; Inline editor for simple short text input 14 | ;; Shows the text normally (with a hover edit icon). When the text is clicked 15 | ;; it is replaced with an input field containing the text. 16 | ;; Pressing enter in the input field will submit the replacement text. 17 | 18 | (defn text-inline-edit [] 19 | (reify InlineEdit 20 | (render-inline-edit-view [this field current-value] 21 | (let [s (gensym) 22 | post-url (crud-url "/inline-edit" {})] 23 | (str "" 24 | "" current-value "" 25 | "" 26 | 27 | "" 28 | ))) 29 | 30 | (update-value [this id field new-value] 31 | (println "Got new " field " value for row " id ": " new-value)))) 32 | 33 | 34 | -------------------------------------------------------------------------------- /demos/src/main/resources/page.cpt: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | $(or (:title here) "Webjure demos page") 6 | 7 | 8 | 9 | 10 | 14 | 15 | 21 | 22 |
23 |
24 | $@(:content here) 25 |
26 |
27 |

Recent Updates

28 |

September 2 2010 29 |
Updated to 0.7-SNAPSHOT, now works with Clojure 1.2 and has page templates.

30 |

31 |

January 11 2010 32 |
Updated to 0.5-SNAPSHOT, now works with Clojure 1.1 and does AOT compiling.

33 |
34 | 35 |
 
36 |
37 | 38 | 42 | 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008 - 2009, Tatu Tarvainen 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, 6 | are permitted provided that the following conditions are met: 7 | 8 | Redistributions of source code must retain the above copyright notice, this list 9 | of conditions and the following disclaimer. 10 | Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or other 12 | materials provided with the distribution. 13 | Neither the name of Tatu Tarvainen nor the names of its contributors may be used 14 | to endorse or promote products derived from this software without specific prior 15 | written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 21 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 22 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 23 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 24 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 25 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 26 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /webjure/src/main/resources/profiler/report.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
5 | 6 | $(:total here) ms 7 | 8 |
9 | 10 | 49 |
-------------------------------------------------------------------------------- /demos/src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Webjure 4 | 5 | 6 | shell 7 | 127.0.0.1:27272 8 | 9 | 10 | 11 | webjure 12 | webjure.servlet.WebjureServlet 13 | 14 | 26 | 27 | 28 | 29 | startupNamespace 30 | webjure.demos 31 | 32 | 33 | 34 | 35 | 36 | default 37 | org.mortbay.jetty.servlet.DefaultServlet 38 | 39 | 40 | 41 | default 42 | /resource/* 43 | 44 | 45 | 46 | 47 | webjure 48 | /* 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /webjure/test/webjure/csv/test.clj: -------------------------------------------------------------------------------- 1 | 2 | (ns webjure.csv.test 3 | (:refer-clojure) 4 | (:use clojure.test) 5 | (:use webjure.csv) 6 | (:import (java.util Date))) 7 | 8 | (defn csv [data] 9 | (with-out-str 10 | (csv-format *out* data))) 11 | 12 | (deftest simple-output-without-headers 13 | (let [result (csv {:rows [["foo" "bar"]]})] 14 | (is (= result "foo;bar\n")))) 15 | 16 | (deftest simple-output-with-headers 17 | (let [result (csv 18 | {:headers [{:label "Name"} 19 | {:label "Shoe size" 20 | :format #(if (nil? %) "shoe size unknown" (str %))}] 21 | :rows [["Tatu" 45] 22 | ["Rolf" 38] 23 | ["Tutte" nil]]}) 24 | lines (seq (.split result "\n"))] 25 | ;; we should have four lines, 1 header + 3 rows 26 | (is (= (count lines) 4)) 27 | 28 | ;; first line should be headers 29 | (is (= (first lines) "Name;Shoe size")) 30 | 31 | 32 | ;; Rolf has tiny feet 33 | (is (= (nth lines 2) "Rolf;38")) 34 | 35 | ;; formatter for the 2nd column of the last line should output 36 | ;; "shoe size unknown" 37 | (is (= (last lines) "Tutte;shoe size unknown")) 38 | )) 39 | 40 | (deftest output-user-objects-with-separator 41 | (let [result (csv {:rows [{:name "Rolf Teflon" :birthday (Date. 81 3 8)} 42 | {:name "Tutte" :birthday (Date. 99 11 2)} 43 | {:name "Foo Barsky" :birthday (Date. 70 0 1)}] 44 | :headers [{:label "Name" :accessor :name} 45 | {:label "Year born" 46 | :accessor #(+ 1900 (.getYear (:birthday %)))}] 47 | :separator \|}) 48 | lines (seq (.split result "\n"))] 49 | 50 | (is (= (count lines) 4)) 51 | 52 | (is (= (nth lines 2) "Tutte|1999")))) 53 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | webjure 5 | webjure-top 6 | pom 7 | 0.8-SNAPSHOT 8 | webjure-top 9 | http://github.com/tatut/Webjure 10 | 11 | 12 | webjure 13 | demos 14 | 15 | 16 | 17 | 18 | 19 | org.apache.maven.plugins 20 | maven-compiler-plugin 21 | 22 | 1.5 23 | 1.5 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | codehaus snapshot repository 32 | http://snapshots.repository.codehaus.org/ 33 | 34 | true 35 | 36 | 37 | 38 | 39 | 40 | javax.servlet 41 | servlet-api 42 | 2.5 43 | provided 44 | 45 | 46 | 47 | javax.portlet 48 | portlet-api 49 | 1.0 50 | 51 | 52 | 53 | org.clojure 54 | clojure 55 | 1.2.0 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # Webjure, a web programming framework for Clojure 2 | 3 | ## Overview 4 | 5 | Webjure is a simple web framework for Clojure. 6 | It provides basic routing, request and response functionality on top 7 | of Java servlets and then gets out of your way. 8 | 9 | The defh macro can be used for more automagic behaviour 10 | when defining handlers. It can be used to automatically read and parse 11 | input parameters (both parts of the URL path and GET/POST params). 12 | The defh can also automatically send the response as HTML or JSON. 13 | 14 | Webjure also provides Clojure Page Templates (see webjure.cpt namespace) 15 | can be used for dynamic HTML templates similar to ZPT/JPT. The templates 16 | are compiled to bytecode for good performance. 17 | 18 | ## Hello world 19 | 20 | A hello world in Webjure is very simple: 21 | 22 | (ns my-hello-world 23 | (:refer-clojure) 24 | (:use webjure)) 25 | 26 | (defh "/hello" [] {:output :html} 27 | `(:html 28 | (:head (:title "Hello world from Webjure")) 29 | (:body 30 | "Hello world!"))) 31 | 32 | ## Wiki 33 | 34 | For a slightly more complex example, see [in-memory wiki](http://gist.github.com/385152). 35 | 36 | 37 | ## Installation 38 | 39 | 40 | To install, you will need Apache Maven 2. 41 | 42 | Run "mvn install" in the main directory. 43 | 44 | ## Running 45 | 46 | After installation is done, you can depend on the webjure jar in your own web projects (or just copy the .jar from the target directory). To check out the demos, go to the "demos" subdirectory and run "mvn jetty:run" and point your browser to http://localhost:8080/webjure-demos/index. 47 | 48 | SLIME users: To hack on the demos, first start swank by going to http://localhost:8080/webjure-demos/start-swank?port=4005 and then use slime-connect from Emacs. 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /webjure/src/webjure/html.clj: -------------------------------------------------------------------------------- 1 | (ns webjure.html 2 | (:refer-clojure)) 3 | 4 | (defn- append [^java.lang.Appendable out & stuff] 5 | (doseq [thing stuff] 6 | (.append out (str thing)))) 7 | 8 | (defprotocol HtmlFormat 9 | (format-item [thing out])) 10 | 11 | (defn- html-format-tag [tag out] 12 | (let [tagname (name (first tag)) 13 | attrs (second tag) 14 | content (if (map? attrs) (rest (rest tag)) (rest tag))] 15 | (append out "<" tagname) 16 | (if (map? attrs) 17 | (doseq [[key value] attrs] 18 | ;; append the attribute value, replace disallowed " character with ' 19 | (append out 20 | " " (name key) "=\"" (.replace (str value) "\"" "'") "\""))) 21 | (if (empty? content) 22 | (append out " />") 23 | (do 24 | (append out ">") 25 | (doseq [c content] 26 | (format-item c out)) 27 | (append out ""))))) 28 | 29 | (extend-protocol HtmlFormat 30 | clojure.lang.Cons 31 | (format-item [tag out] (html-format-tag tag out)) 32 | 33 | clojure.lang.PersistentList 34 | (format-item [tag out] (html-format-tag tag out)) 35 | 36 | clojure.lang.Keyword 37 | (format-item [tag out] (append out "<" (name tag) " />")) 38 | 39 | clojure.lang.IFn 40 | (format-item [fun out] (append out (fun))) 41 | 42 | nil 43 | (format-item [nothing out]) ;; Nothing in, nothing out 44 | 45 | java.lang.String 46 | (format-item [string out] 47 | (append out (.replace (.replace (.replace string "&" "&") "<" "<") ">" ">"))) 48 | 49 | 50 | java.lang.Object 51 | (format-item [obj out] 52 | (format-item (str obj) out)) 53 | ) 54 | 55 | (defn html-format 56 | "Format object as HTML to output. If called without 57 | specifying output the object is formatted into a newly 58 | created StringBuilder and the value returned." 59 | ([obj] (let [out (StringBuilder.)] 60 | (html-format out obj) 61 | (str out))) 62 | ([out obj] 63 | (format-item obj out))) -------------------------------------------------------------------------------- /demos/src/main/resources/docs-pdf.cpt: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Webjure docs 16 | 17 | 18 | 19 | 20 | Page / 21 | 22 | 23 | 24 | 25 | Documentation on all public vars in the webjure namespace 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | $(:name var) 34 | 35 | 36 | 37 | $(:arglists var) 38 | 39 | $(:doc var) 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /demos/src/main/clojure/webjure/wiki.clj: -------------------------------------------------------------------------------- 1 | (ns webjure.wiki 2 | (:refer-clojure) 3 | (:use webjure) 4 | (:use webjure.html)) 5 | 6 | (def *pages* (ref {})) 7 | 8 | (defn update-page [page-name title body] 9 | (dosync 10 | (alter *pages* 11 | #(assoc % page-name {:title title :body body})))) 12 | 13 | (defn get-page [page-name] 14 | (@*pages* page-name)) 15 | 16 | (defn html-page [title & body] 17 | `(:html 18 | (:head (:title ~title)) 19 | (:body ~@body))) 20 | 21 | 22 | (defh #"/wiki/([^/]+)$" [page-name 1] 23 | {:output :html} 24 | (let [page (get-page page-name)] 25 | (if (nil? page) 26 | (html-page (str "Page does not exist: " page-name) 27 | `(:div 28 | (:h3 "Page \"" ~page-name "\" does not exist!") 29 | (:a {:href ~(url (str "/wiki/" page-name "/edit"))} 30 | "Create it"))) 31 | 32 | (html-page (:title page) 33 | `(:h3 ~(:title page)) 34 | `(:a {:href ~(url (str "/wiki/" page-name "/edit")) 35 | :style "float: right;"} "edit this page") 36 | `(:div ~#(:body page)))))) 37 | 38 | 39 | 40 | (defh #"/wiki/(.+)/edit$" [page-name 1] 41 | {:output :html} 42 | (let [page (get-page page-name)] 43 | (html-page (if (nil? page) 44 | (str "Create page: " page-name) 45 | (str "Edit page: " (:title page))) 46 | 47 | `(:form 48 | {:action ~(url (str "/wiki/" page-name "/save")) 49 | :method "POST"} 50 | 51 | "Title: " (:input {:type "text" :name "title" 52 | :value ~(or (:title page) page-name)}) 53 | (:br) 54 | 55 | "Body: " (:br) 56 | (:textarea {:name "body" :rows "20" :cols "70"} 57 | ~(or (:body page) "")) 58 | (:br) 59 | (:input {:type "submit" :value "Save"}))))) 60 | 61 | (defn redirect [url] 62 | (.sendRedirect (.getActualResponse *response*) url)) 63 | 64 | (defh #"/wiki/(.+)/save$" [page-name 1 65 | title "title" 66 | body "body"] 67 | {} 68 | (update-page page-name title body) 69 | (redirect (url (str "/wiki/" page-name)))) -------------------------------------------------------------------------------- /demos/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | webjure 5 | webjure-demos 6 | war 7 | 0.8-SNAPSHOT 8 | webjure-demos 9 | 10 | 11 | webjure 12 | webjure-top 13 | 0.8-SNAPSHOT 14 | ../pom.xml 15 | 16 | 17 | 18 | 19 | webjure 20 | webjure 21 | ${project.version} 22 | 23 | 24 | 25 | org.apache.derby 26 | derby 27 | 10.4.1.3 28 | 29 | 30 | 31 | swank-clojure 32 | swank-clojure 33 | 1.2.1 34 | 35 | 36 | 37 | org.apache.xmlgraphics 38 | fop 39 | 1.0 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | com.theoryinpractise 48 | clojure-maven-plugin 49 | 1.3.3 50 | 51 | -Dwebjure.cpt.path=${basedir}/src/main/resources 52 | 53 | 54 | 55 | compile 56 | 57 | compile 58 | 59 | 60 | 61 | 62 | 63 | org.mortbay.jetty 64 | maven-jetty-plugin 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /webjure/src/webjure/sql.clj: -------------------------------------------------------------------------------- 1 | 2 | (ns webjure.sql 3 | (:refer-clojure)) 4 | 5 | (defn register-driver [drv] 6 | (. Class (forName drv))) 7 | 8 | (defn connect 9 | ([url] (. java.sql.DriverManager (getConnection url))) 10 | ([url user pass] (. java.sql.DriverManager (getConnection url user pass)))) 11 | 12 | 13 | (defn prepare-statement [#^java.sql.Connection con query query-args] 14 | (let [stmt (.prepareStatement con query)] 15 | ;; Set query arguments 16 | (doseq [i (range 0 (count query-args))] 17 | (.setObject stmt (+ i 1) (nth query-args i))) 18 | stmt)) 19 | 20 | 21 | (defn #^{:private true} 22 | dbg [& items] 23 | (. (. System err) (println (reduce str (map str items))))) 24 | 25 | ;; Execute query and return results as a list of lists. 26 | ;; Result set metadata is attached to the returned list. 27 | (defn query [#^java.sql.Connection con query & query-args] 28 | (let [stmt (prepare-statement con query query-args) 29 | result-set (. stmt (executeQuery)) 30 | metadata (. result-set (getMetaData)) 31 | column-count (. metadata (getColumnCount)) 32 | columns (doall (map (fn [i] 33 | [(. metadata (getColumnName i)) 34 | (. metadata (getColumnClassName i))]) (range 1 (+ 1 column-count))))] 35 | (loop [acc [] 36 | row-count 0] 37 | (if (= false (. result-set (next))) 38 | (let [result-list acc] 39 | (with-meta result-list {:columns columns :rows row-count})) 40 | (recur (conj acc (doall (map (fn [i] (. result-set (getObject i))) 41 | (range 1 (+ 1 column-count))))) 42 | (+ 1 row-count)))))) 43 | 44 | (defn update [#^java.sql.Connection con update & update-args] 45 | (let [stmt (prepare-statement con update update-args)] 46 | (. stmt (executeUpdate)) 47 | (. stmt (close)))) 48 | 49 | (defn call-with-tx [#^java.sql.Connection con func] 50 | (let [autocommit (. con (getAutoCommit))] 51 | (when autocommit 52 | (. con (setAutoCommit false))) 53 | (try (func) 54 | (. con (commit)) 55 | (catch java.sql.SQLException se 56 | (. con (rollback)) 57 | false)) 58 | (when autocommit 59 | (. con (setAutoCommit true))))) 60 | 61 | -------------------------------------------------------------------------------- /webjure/src/webjure/xml/feeds.clj: -------------------------------------------------------------------------------- 1 | 2 | ;; Working with XML-based feed formats 3 | 4 | (ns webjure.xml.feeds 5 | (:refer-clojure) 6 | (:use webjure.xml)) 7 | 8 | (defn parse-atom1-link [link-node] 9 | (parse link-node 10 | {} 11 | (attr "href" (collect-as :url #(.getValue %))) 12 | (attr "rel" (collect-as :rel #(.getValue %))))) 13 | 14 | (defn parse-atom1-entry [entry-node] 15 | (parse entry-node 16 | {:links {}} 17 | (<> "title" (collect-as :title text)) 18 | (<>* "link" (modify-key :links #(let [lnk (parse-atom1-link %2)] 19 | (assoc %1 (lnk :rel) lnk)))) 20 | 21 | (<>? "author" 22 | (<>? "email" (collect-as :email text)) 23 | (<>? "name" (collect-as :author text))))) 24 | 25 | (defn load-atom1-feed [file-or-istream] 26 | (parse 27 | (. (load-dom file-or-istream) (getDocumentElement)) 28 | {:entries []} 29 | (<> "title" (collect-as :title text)) 30 | (<>? "subtitle" (collect-as :subtitle text)) 31 | (<>* "entry" 32 | (modify-key :entries #(conj %1 (parse-atom1-entry %2)))))) 33 | 34 | 35 | ;; example modified from Wikipedia: 36 | (def +atom-test-xml+ 37 | " 38 | 39 | 40 | Example Feed 41 | A subtitle. 42 | 43 | 44 | 2003-12-13T18:30:02Z 45 | 46 | John Doe 47 | johndoe@example.com 48 | 49 | urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6 50 | 51 | 52 | Atom-Powered Robots Run Amok 53 | 54 | urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a 55 | 2003-12-13T18:30:02Z 56 | Some text. 57 | 58 | 59 | 60 | Webjure gains preliminary support form feeds 61 | 62 | urn:uuid:this-is-my-story-and-im-sticking-to-it-2008118tt 63 | We can parse these. 64 | 65 | ") -------------------------------------------------------------------------------- /webjure/src/main/resources/crud/generic-listing-template.cpt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /webjure/src/webjure/profiler.clj: -------------------------------------------------------------------------------- 1 | ;; Server page generation profiling support. 2 | ;; 3 | ;; Some client side scripts, styles and templates copied from: 4 | ;; GAE Mini Profiler (https://github.com/kamens/gae_mini_profiler) 5 | ;; which is itself heavily inspired by MVC mini profiler (http://code.google.com/p/mvc-mini-profiler/) 6 | ;; 7 | 8 | (ns webjure.profiler 9 | (:refer-clojure) 10 | (:use webjure) 11 | (:use webjure.cpt)) 12 | 13 | (def *profiler* nil) 14 | 15 | (defprotocol Profiler 16 | (start-step [this name]) 17 | (end-step [this name]) 18 | (report [this]) 19 | (id [this])) 20 | 21 | ;; Empty profiler implementation to use when *profiler* is nil 22 | (extend-protocol Profiler 23 | nil 24 | (start-step [this name] nil) 25 | (end-step [this name] nil) 26 | (report [this] nil) 27 | (id [this] nil) 28 | ) 29 | 30 | (deftype DefaultProfiler [^{:volatile-mutable true } steps 31 | ^{:volatile-mutable true} current-step 32 | ^{:volatile-mutable true} profiler-id 33 | ] 34 | 35 | Profiler 36 | (start-step [this name] 37 | (set! current-step {:name name 38 | :start (System/currentTimeMillis) 39 | :parent current-step})) 40 | 41 | (end-step [this name] 42 | (if (not (= name (:name current-step))) 43 | (throw (IllegalArgumentException. "Ending an unknown step.")) 44 | (let [finished-step (assoc (dissoc current-step :parent) 45 | :end (System/currentTimeMillis)) 46 | parent (:parent current-step)] 47 | (if parent 48 | (set! current-step 49 | (assoc parent :children 50 | (conj (or (:children parent) []) finished-step))) 51 | (do (set! steps (conj steps finished-step)) 52 | (set! current-step nil)))))) 53 | 54 | (report [this] steps) 55 | 56 | (id [this] 57 | (or profiler-id 58 | (set! profiler-id (str "p-" (System/currentTimeMillis) "-" (rand-int Integer/MAX_VALUE)))))) 59 | 60 | 61 | (defn print-profiler-report 62 | ([report] (print-profiler-report report 0)) 63 | ([report indent] 64 | (doseq [{:keys [name start end children]} report] 65 | (println (str (apply str (repeat indent " ")) 66 | name 67 | " " 68 | (- end start) " ms")) 69 | (print-profiler-report children (+ indent 2))))) 70 | 71 | (defn inject-profiler-html [] 72 | (let [id (id *profiler*)] 73 | (if (nil? id) 74 | "" ;; Not profiling 75 | (str "\n" 76 | "\n" 77 | "")))) 78 | 79 | (defn duration-of [{start :start end :end}] 80 | (- end start)) 81 | 82 | (defn format-profiler-report 83 | ([steps] (format-profiler-report steps 0)) 84 | ([steps indent] 85 | (if (nil? steps) 86 | nil 87 | (loop [acc [] 88 | [step & steps] steps] 89 | (if (nil? step) 90 | acc 91 | (recur (concat acc [{:name (:name step) 92 | :indent indent 93 | :total-ms (duration-of step) 94 | :own-ms (- (duration-of step) 95 | (reduce + 0 (map duration-of (:children step))))}] 96 | (format-profiler-report (:children step) (+ 1 indent))) 97 | steps)))))) 98 | 99 | (define-template profiler-report "src/main/resources/profiler/report.xml") 100 | 101 | (defh "/_webjure_profiler/request" [id "id"] {} 102 | (let [duration #(- (:end %) (:start %)) 103 | {url :url steps :report} (webjure/session-get id) 104 | total (reduce + 0 (map duration steps))] 105 | 106 | (webjure/set-response-content-type webjure/*response* "text/html") 107 | (profiler-report 108 | (response-writer) 109 | {:total total 110 | :url url 111 | :id id 112 | :report (format-profiler-report steps) 113 | }))) 114 | 115 | 116 | (def *profiler-enabled* (ref (fn [] true))) 117 | 118 | (defmacro with-request-profiling [& body] 119 | `(binding [*profiler* (if (@*profiler-enabled*) 120 | (DefaultProfiler. [] nil nil) 121 | nil)] 122 | (let [url# (webjure/request-path)] 123 | (with-step url# 124 | (do ~@body)) 125 | (when *profiler* 126 | (webjure/session-set (id *profiler*) 127 | {:url url# 128 | :report (report *profiler*)}))))) 129 | 130 | (defmacro with-step [name & body] 131 | `(let [name# ~name] 132 | (start-step *profiler* name#) 133 | (try 134 | ~@body 135 | (finally (end-step *profiler* name#))))) 136 | 137 | (define-static-resource "src/main/resources/profiler/profiler.js" "text/javascript" 138 | "/_webjure_profiler/profiler.js") 139 | 140 | (define-static-resource "src/main/resources/profiler/profiler.css" "text/css" 141 | "/_webjure_profiler/profiler.css") 142 | 143 | -------------------------------------------------------------------------------- /webjure/src/main/resources/profiler/profiler.css: -------------------------------------------------------------------------------- 1 | 2 | .g-m-p-corner { 3 | padding: 4px; 4 | cursor: pointer; 5 | text-align: right; 6 | } 7 | 8 | .g-m-p-corner .ms, 9 | .g-m-p .ms { 10 | color: #777; 11 | } 12 | 13 | .g-m-p-corner .entry.ajax { 14 | border-top: 1px dotted #DDD; 15 | } 16 | 17 | .g-m-p-corner .entry.expanded { 18 | font-weight: bold; 19 | } 20 | 21 | .g-m-p { 22 | min-width: 410px; 23 | padding: 9px; 24 | padding-bottom: 18px; 25 | } 26 | 27 | .g-m-p, .g-m-p-corner, .g-m-p-shared { 28 | font-family: Helvetica, Arial, sans-serif; 29 | font-size: 12px; 30 | color: #444; 31 | line-height: 18px; 32 | } 33 | 34 | .g-m-p a, .g-m-p-corner a, .g-m-p-shared a { 35 | color: #069; 36 | text-decoration: none; 37 | } 38 | 39 | .g-m-p a.uses_script, .g-m-p-corner a.uses_script, .g-m-p-shared a.uses_script { 40 | border-bottom: dotted 1px #069 41 | } 42 | 43 | .g-m-p, .g-m-p-corner { 44 | position: absolute; 45 | left: 9px; 46 | top: 0px; 47 | 48 | background: #F7F7F7; 49 | 50 | border-top: 0; 51 | border-left: 1px solid #CCC; 52 | border-bottom: 1px solid #DDD; 53 | border-right: 1px solid #DDD; 54 | 55 | box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.25); 56 | -moz-box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.25); 57 | -webkit-box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.25); 58 | 59 | -webkit-border-bottom-left-radius: 12px; 60 | -moz-border-radius-bottomleft: 12px; 61 | border-bottom-left-radius: 12px; 62 | 63 | -webkit-border-bottom-right-radius: 12px; 64 | -moz-border-radius-bottomright: 12px; 65 | border-bottom-right-radius: 12px; 66 | 67 | z-index: 1000; 68 | } 69 | 70 | .g-m-p .title { 71 | font-size: 1.25em; 72 | text-align: left; 73 | font-weight: bold; 74 | border-bottom: 1px solid #CCC; 75 | padding: 0; 76 | } 77 | 78 | .g-m-p .date_and_share { 79 | text-align: right; 80 | margin-bottom: 18px; 81 | } 82 | 83 | .g-m-p .date_and_share .date { 84 | font-style: italic; 85 | color: #CCC; 86 | } 87 | 88 | .g-m-p .appstats-link { 89 | float:right; 90 | } 91 | 92 | .g-m-p .url { 93 | font-size: 125%; 94 | } 95 | 96 | .g-m-p .total { 97 | float: right; 98 | } 99 | 100 | .g-m-p .dupe { 101 | font-weight: bold; 102 | color: red; 103 | } 104 | 105 | .g-m-p .summary { 106 | float:right; 107 | } 108 | 109 | .g-m-p .details { 110 | max-width: 800px; 111 | max-height: 500px; 112 | margin-bottom: 18px; 113 | overflow: auto; 114 | } 115 | 116 | .g-m-p .expand.expanded { 117 | border-bottom: 1px solid #DDD; 118 | } 119 | 120 | .g-m-p .expand.expanded a { 121 | text-decoration: none; 122 | border-bottom: 0; 123 | font-weight: bold; 124 | } 125 | 126 | .g-m-p .details table { 127 | margin-top: 18px; 128 | border-spacing: 0; 129 | font-size: 12px; 130 | } 131 | 132 | .g-m-p .details table tr:nth-child(odd) { 133 | background-color: #FFF; 134 | } 135 | 136 | .g-m-p .details table tr:nth-child(even) { 137 | background-color: #F7F7F7; 138 | } 139 | 140 | .g-m-p .details table .callers-label { 141 | color: #CCC; 142 | } 143 | 144 | .g-m-p .details th, .g-m-p .details th.header { 145 | font-size: 12px; 146 | background-color: #F7F7F7; 147 | font-weight: bold; 148 | cursor: pointer; 149 | float: none; 150 | position: static; 151 | } 152 | 153 | .g-m-p .details th.headerSortUp, 154 | .g-m-p .details th.headerSortDown { 155 | text-decoration: underline; 156 | } 157 | 158 | .g-m-p .details th, 159 | .g-m-p .details td { 160 | padding-right: 18px; 161 | vertical-align: top; 162 | } 163 | 164 | .g-m-p .details .left { 165 | text-align: left; 166 | } 167 | 168 | .g-m-p .details .right { 169 | text-align: right; 170 | } 171 | 172 | .g-m-p-shared .shared-teaser-container { 173 | text-align: center; 174 | } 175 | 176 | .g-m-p-shared .shared-teaser { 177 | margin-left: auto; 178 | margin-right: auto; 179 | height: 400px; 180 | width: 600px; 181 | text-align: center; 182 | padding: 200px 0; 183 | font-size: 24px; 184 | font-weight: bold; 185 | } 186 | 187 | /* Borrowed from those smart guys @ Fog Creek. Props to Justin & Bobby */ 188 | .g-m-p .fancy-scrollbar::-webkit-scrollbar { 189 | height: 8px; 190 | width: 8px; 191 | } 192 | .g-m-p .fancy-scrollbar::-webkit-scrollbar-button:start:decrement, 193 | .g-m-p .fancy-scrollbar::-webkit-scrollbar-button:end:increment { 194 | background: transparent; 195 | display: none; 196 | } 197 | .g-m-p .fancy-scrollbar::-webkit-scrollbar-track-piece { 198 | background: transparent; 199 | } 200 | .g-m-p .fancy-scrollbar::-webkit-scrollbar-track-piece:vertical:start { 201 | -webkit-border-top-left-radius: 4px; 202 | -webkit-border-top-right-radius: 4px; 203 | } 204 | .g-m-p .fancy-scrollbar::-webkit-scrollbar-track-piece:vertical:end { 205 | -webkit-border-bottom-left-radius: 4px; 206 | -webkit-border-bottom-right-radius: 4px; 207 | } 208 | .g-m-p .fancy-scrollbar::-webkit-scrollbar-track-piece:horizontal:start { 209 | -webkit-border-top-left-radius: 4px; 210 | -webkit-border-bottom-left-radius: 4px; 211 | } 212 | .g-m-p .fancy-scrollbar::-webkit-scrollbar-track-piece:horizontal:end { 213 | -webkit-border-top-right-radius: 4px; 214 | -webkit-border-bottom-right-radius: 4px; 215 | } 216 | .g-m-p .fancy-scrollbar::-webkit-scrollbar-thumb:vertical, 217 | .g-m-p .fancy-scrollbar::-webkit-scrollbar-thumb:horizontal { 218 | background: #ccc; 219 | border: 1px solid #aaa; 220 | -webkit-border-radius: 4px; 221 | display: block; 222 | height: 50px; 223 | } -------------------------------------------------------------------------------- /demos/src/main/clojure/webjure/demos.clj~: -------------------------------------------------------------------------------- 1 | 2 | ;; Demos to be placed here 3 | 4 | (ns webjure-demos 5 | (:refer-clojure) 6 | (:use webjure) 7 | (:use webjure.html) 8 | (:use webjure.sql) 9 | (:use webjure.xml.feeds)) 10 | 11 | 12 | (defn dbg [& u] 13 | (. (. System err) 14 | (println (apply pr-str u)))) 15 | 16 | 17 | (defh "/index" [] {:output :html} 18 | `(:html 19 | (:body 20 | (:h3 "Webjure, a web framework like thing.") 21 | (:p "Welcome to webjure, a clojure web framework. Not much is done yet, but feel free " 22 | "to look at the demos.") 23 | (:ul 24 | (:li (:a {:href ~(url "/index")} "This page, a simple sexp markup page")) 25 | (:li (:a {:href ~(url "/info" {:some "value" :another "one"})} "Dump request info")) 26 | (:li (:a {:href ~(url "/dbtest")} "Database test")) 27 | (:li (:a {:href ~(url "/session")} "Session test")) 28 | ;;(:li (:a {:href ~(url "/ajaxrepl")} "an AJAX REPL")) 29 | (:li (:a {:href ~(url "/clojurenews")} "Clojure news (Atom feed parser test)")) 30 | (:li (:a {:href ~(url "/hello/Test")} "Test path binding")) 31 | (:li (:a {:href ~(url "/json?foo=bar&quux=baz")} "Return request info as JSON"))) 32 | 33 | (:div {:style "position: relative; left: 50%;"} 34 | (:div {:style "text-align: center; width: 300px; position: absolute; left: -150; border: dotted black 2px; background-color: yellow; padding: 10px;"} 35 | (:b "Important notice: ") "Have a nice and RESTful day!" 36 | (:br) 37 | (:div {:style "font-size: small;"} ~(format-date "dd.MM.yyyy hh:mm"))))))) 38 | 39 | 40 | (defn format-map-as-table [keylabel valuelabel themap] 41 | `(:table 42 | (:tr (:th ~keylabel) (:th ~valuelabel)) 43 | ~@(map (fn [key] 44 | `(:tr (:td ~key) (:td ~(reduce (fn [x y] (str x ", " y)) (get themap key))))) 45 | (keys themap)))) 46 | 47 | (defn format-table [headers values & actions] 48 | `(:table 49 | (:tr 50 | ~@(map (fn [hdr] `(:th ~hdr)) headers) 51 | (:th "Actions")) 52 | 53 | ~@(map (fn [row] `(:tr 54 | ~@(map (fn [v] `(:td ~(str v))) row) 55 | (:td ~@(map (fn [action] 56 | `(:a {:href ~((second action) row)} 57 | ~(first action))) actions)) 58 | )) 59 | values))) 60 | 61 | (defh "/info*" [] {:output :html} 62 | `(:html 63 | (:body 64 | (:h3 "Request headers") 65 | ~(format-map-as-table "Name" "Values" (request-headers)) 66 | (:br) 67 | 68 | (:h3 "Request parameters") 69 | ~(format-map-as-table "Name" "Values" (request-parameters)) 70 | (:br) 71 | 72 | (:h3 "Path info") 73 | (:p ~(request-path))))) 74 | 75 | 76 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 77 | ;; db test using derby tours 78 | ;; 79 | 80 | 81 | (defn connect-to-db [location] 82 | (do (register-driver "org.apache.derby.jdbc.EmbeddedDriver") 83 | (connect (str "jdbc:derby:" location)))) 84 | 85 | (defn dbtest-ask-location [] 86 | `(:html 87 | (:body 88 | (:form {:action ~(url "/dbtest") :method "POST"} 89 | (:b "Where is the derby tours db located?") 90 | (:input {:type "text" :name "db" :size "80"}) 91 | "(example: /Users/tadex/software/derby/demo/databases/toursdb)" 92 | (:br) 93 | (:input {:type "submit" :value "Go!"}))))) 94 | 95 | (defh "/dbtest" [loc {:name "db" :optional true}] {:output :html} 96 | (let [db (session-get "db" 97 | (fn [] (if (nil? loc) 98 | nil 99 | (connect-to-db loc))))] 100 | (if (nil? db) 101 | (dbtest-ask-location) 102 | `(:html 103 | (:body 104 | ~(let [results (query db "SELECT c.COUNTRY, c.COUNTRY_ISO_CODE, c.REGION, (SELECT COUNT(*) FROM cities WHERE country_iso_code=c.country_iso_code) as cities FROM countries c ORDER BY country ASC") 105 | columns (:columns (meta results))] 106 | (format-table (map first columns) results 107 | ["List cities" (fn [row] 108 | (url "/dbtest-cities" {:country (second row)}))]))))))) 109 | 110 | (defh "/dbtest-cities" [country "country"] {:output :html} 111 | (let [cities (query (session-get "db") 112 | "SELECT * FROM cities WHERE country_iso_code=?" country)] 113 | `(:html 114 | (:body 115 | ~(format-table (map first (:columns (meta cities))) cities) 116 | (:b ~(str (:rows (meta cities)) " cities."))) 117 | (:br) 118 | (:a {:href ~(url "/dbtest")} "« back to countries")))) 119 | 120 | 121 | 122 | ;;;;;;;;;;;;; 123 | ;; AJAX REPL 124 | ;; 125 | ;; a very simplistic version 126 | ;; there were some problems with using PiperReader/-Writer approach 127 | ;; with thread deadlocking... 128 | 129 | 130 | 131 | ;; The main page 132 | (defh "/ajaxrepl" [] {:output :html} 133 | `(:html 134 | (:head (:title "Webjure AJAX REPL") 135 | (:script {:type "text/javascript" 136 | :language "javascript" 137 | :src ~(url "/resource/repl.js")})) 138 | 139 | (:body 140 | (:h3 "Webjure AJAX REPL") 141 | (:div 142 | {:id "replout" 143 | :style "width: 600px; height: 400px; overflow: auto; font-family: monospace; color: silver; background-color: black; white-space: pre;"} 144 | "") 145 | 146 | (:form ;{:onsubmit "return write();"} 147 | (:textarea {:onkeypress "keyHandler(event)" :id "replin" :style "width: 600px; height: 70px;"} ""))))) 148 | 149 | ;;; A REPL using Refs 150 | 151 | (defn repl-session [] 152 | (session-get "repl-messages" (fn [] (binding [*use-context-classloader* true] 153 | (agent []))))) 154 | 155 | (defn repl-write [messages new-msg] 156 | (let [messages (conj new-msg messages)] 157 | (prn messages) 158 | messages)) 159 | 160 | (defn repl-fetch [messages writer] 161 | (. writer (append "FOO")) 162 | (. writer (append (reduce str (interleave messages (repeat "\n"))))) 163 | ;; new state is no messages 164 | []) 165 | 166 | 167 | ;; Set the Return new content since last update 168 | ;; if there is no new content, wait for some 169 | (defn ajaxrepl-out [] 170 | (. *response* (setContentType "text/plain")) 171 | (. (. *response* (getWriter)) (append (reduce str (interleave @(repl-session) (repeat "\n"))))) 172 | (binding [*use-context-classloader* true] 173 | (send (repl-session) (fn [msgs] [])))) 174 | 175 | (publish ajaxrepl-out "/ajaxrepl-out") 176 | 177 | (defn ajaxrepl-eval [str] 178 | (str (eval (read (new java.io.PushbackReader (new java.io.StringReader str)))))) 179 | 180 | (defn ajaxrepl-in [] 181 | (binding [*use-context-classloader* true] 182 | (send (repl-session) repl-write (ajaxrepl-eval (slurp-post-data))))) 183 | (publish ajaxrepl-in "/ajaxrepl-in") 184 | 185 | 186 | ;;; Session test 187 | 188 | (defh "/session" [] {:output :html} 189 | `(:html 190 | (:head (:title "Webjure session test")) 191 | (:body 192 | ~(let [count (session-get "count") 193 | greeting (if (nil? count) 194 | "Hello first time user" 195 | (str "Hello, this has been called " count " times."))] 196 | (session-set "count" (if (nil? count) 1 (inc count))) 197 | greeting)))) 198 | 199 | 200 | 201 | ;;;;;;;;;;;;;;;;;;;;;;;;;; 202 | ;; Atom feed parser test 203 | 204 | (defh "/clojurenews" [] 205 | {:output :html} 206 | (with-open [url (.openStream (new java.net.URL "http://clojure.blogspot.com/feeds/posts/default"))] 207 | (let [feed (load-atom1-feed url)] 208 | (do (println (str feed)) 209 | `(:html 210 | (:head (:title ~(feed :title))) 211 | (:body 212 | (:h3 ~(feed :subtitle)) 213 | (:ul 214 | ~@(map (fn [entry] 215 | `(:li (:a {:href ~(((entry :links) "alternate") :url)} ~(entry :title)))) 216 | (feed :entries))) 217 | (:hr) 218 | "And this is what we parsed from the XML:" 219 | (:p) 220 | (:textarea {:style "width: 95%; height: 300px;" } 221 | ~(webjure.json/serialize-str feed)))))))) 222 | 223 | 224 | ;;;;;;;;;;;;;;;;;;;; 225 | ;; defh regex test 226 | 227 | (defh #"/hello/(.+?)(/(.+))?$" 228 | [first-name 1 229 | last-name 3] 230 | {:output :html} 231 | 232 | `(:html 233 | (:body 234 | (:p "Hello " ~first-name " " ~(or last-name "")) 235 | 236 | ~@(if (nil? last-name) 237 | `((:a {:href ~(url (str "/hello/" first-name "/Something"))} 238 | "try with another path component")))))) 239 | 240 | 241 | ;;;;;;;;;;;;;; 242 | ;; json test 243 | 244 | (defh "/json" [] 245 | {:output :json} 246 | {"headers" (request-headers) 247 | "parameters" (request-parameters)}) 248 | -------------------------------------------------------------------------------- /demos/src/main/clojure/webjure/demos.clj: -------------------------------------------------------------------------------- 1 | 2 | ;; Demos to be placed here 3 | 4 | (ns webjure.demos 5 | (:refer-clojure) 6 | (:use webjure) 7 | (:use webjure.html) 8 | (:use webjure.sql) 9 | (:use webjure.xml.feeds) 10 | (:use webjure.wiki) 11 | (:use webjure.cpt) 12 | (:use webjure.xml) 13 | (:use swank.swank) 14 | (:use clojure.stacktrace)) 15 | 16 | 17 | (defn dbg [& u] 18 | (. (. System err) 19 | (println (apply pr-str u)))) 20 | 21 | (defn menu [& entries] 22 | (map (fn [[link desc]] 23 | `(:li (:a {:href ~link} ~desc))) 24 | entries)) 25 | 26 | (declare page) 27 | 28 | (def *swank-started* (ref false)) 29 | 30 | (defh "/start-swank" [port {:name "port" :validator #(if (nil? %) 4005 (Integer/parseInt %))}] 31 | {:output :text} 32 | (if @*swank-started* 33 | "Swank already started." 34 | (dosync 35 | (try (start-repl port) 36 | (ref-set *swank-started* true) 37 | (str "Swank started on port " port) 38 | (catch Exception e 39 | (str "Unable to start swank: " (with-out-str (print-cause-trace e)))))))) 40 | 41 | (defh "/index" [] {:output :print} 42 | (page 43 | {:title "Webjure, a tiny web framework for Clojure" 44 | :content (html-format 45 | `(span 46 | (:h2 "Welcome to Webjure demos!") 47 | (:p "Here are the demos.") 48 | (:ul ~@(menu 49 | [(url "/start-swank") "Start swank to use SLIME"] 50 | [(url "/index") "This page, a simple sexp markup page"] 51 | [(url "/info" {:some "value" :another "one"}) "Dump request info"] 52 | [(url "/dbtest") "Database test"] 53 | [(url "/session") "Session test"] 54 | [(url "/clojurenews") "Clojure news (Atom feed parser test)"] 55 | [(url "/hello/Test") "Test path binding"] 56 | [(url "/json?foo=bar&quux=baz") "Return request info as JSON"] 57 | [(url "/docs") "Autogenerated Webjure docs"])) 58 | (:hr) 59 | (:p 60 | (:b "Important notice: ") "Have a nice and RESTful day!" 61 | (:br) 62 | (:div {:style "font-size: small;"} ~(format-date "dd.MM.yyyy hh:mm"))))) 63 | })) 64 | 65 | 66 | (defn format-map-as-table [keylabel valuelabel themap] 67 | `(:table 68 | (:tr (:th ~keylabel) (:th ~valuelabel)) 69 | ~@(map (fn [key] 70 | `(:tr (:td ~key) (:td ~(reduce (fn [x y] (str x ", " y)) (get themap key))))) 71 | (keys themap)))) 72 | 73 | (defn format-table [headers values & actions] 74 | `(:table 75 | (:tr 76 | ~@(map (fn [hdr] `(:th ~hdr)) headers) 77 | (:th "Actions")) 78 | 79 | ~@(map (fn [row] `(:tr 80 | ~@(map (fn [v] `(:td ~(str v))) row) 81 | (:td ~@(map (fn [action] 82 | `(:a {:href ~((second action) row)} 83 | ~(first action))) actions)) 84 | )) 85 | values))) 86 | 87 | (defh "/info*" [] {:output :html} 88 | `(:html 89 | (:body 90 | (:h3 "Request headers") 91 | ~(format-map-as-table "Name" "Values" (request-headers)) 92 | (:br) 93 | 94 | (:h3 "Request parameters") 95 | ~(format-map-as-table "Name" "Values" (request-parameters)) 96 | (:br) 97 | 98 | (:h3 "Path info") 99 | (:p ~(request-path))))) 100 | 101 | 102 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 103 | ;; db test using derby tours 104 | ;; 105 | 106 | 107 | (defn connect-to-db [location] 108 | (do (register-driver "org.apache.derby.jdbc.EmbeddedDriver") 109 | (connect (str "jdbc:derby:" location)))) 110 | 111 | (defn dbtest-ask-location [] 112 | `(:html 113 | (:body 114 | (:form {:action ~(url "/dbtest") :method "POST"} 115 | (:b "Where is the derby tours db located?") 116 | (:input {:type "text" :name "db" :size "80"}) 117 | "(example: /Users/tadex/software/derby/demo/databases/toursdb)" 118 | (:br) 119 | (:input {:type "submit" :value "Go!"}))))) 120 | 121 | (defh "/dbtest" [loc {:name "db" :optional true}] {:output :html} 122 | (let [db (session-get "db" 123 | (fn [] (if (nil? loc) 124 | nil 125 | (connect-to-db loc))))] 126 | (if (nil? db) 127 | (dbtest-ask-location) 128 | `(:html 129 | (:body 130 | ~(let [results (query db "SELECT c.COUNTRY, c.COUNTRY_ISO_CODE, c.REGION, (SELECT COUNT(*) FROM cities WHERE country_iso_code=c.country_iso_code) as cities FROM countries c ORDER BY country ASC") 131 | columns (:columns (meta results))] 132 | (format-table (map first columns) results 133 | ["List cities" (fn [row] 134 | (url "/dbtest-cities" {:country (second row)}))]))))))) 135 | 136 | (defh "/dbtest-cities" [country "country"] {:output :html} 137 | (let [cities (query (session-get "db") 138 | "SELECT * FROM cities WHERE country_iso_code=?" country)] 139 | `(:html 140 | (:body 141 | ~(format-table (map first (:columns (meta cities))) cities) 142 | (:b ~(str (:rows (meta cities)) " cities."))) 143 | (:br) 144 | (:a {:href ~(url "/dbtest")} "« back to countries")))) 145 | 146 | 147 | ;;; Session test 148 | 149 | (defh "/session" [] {:output :html} 150 | `(:html 151 | (:head (:title "Webjure session test")) 152 | (:body 153 | ~(let [count (session-get "count") 154 | greeting (if (nil? count) 155 | "Hello first time user" 156 | (str "Hello, this has been called " count " times."))] 157 | (session-set "count" (if (nil? count) 1 (inc count))) 158 | greeting)))) 159 | 160 | 161 | 162 | ;;;;;;;;;;;;;;;;;;;;;;;;;; 163 | ;; Atom feed parser test 164 | 165 | (defh "/clojurenews" [] 166 | {:output :html} 167 | (with-open [url (.openStream (new java.net.URL "http://clojure.blogspot.com/feeds/posts/default"))] 168 | (let [feed (load-atom1-feed url)] 169 | (do (println (str feed)) 170 | `(:html 171 | (:head (:title ~(feed :title))) 172 | (:body 173 | (:h3 ~(feed :subtitle)) 174 | (:ul 175 | ~@(map (fn [entry] 176 | `(:li (:a {:href ~(((entry :links) "alternate") :url)} ~(entry :title)))) 177 | (feed :entries))) 178 | (:hr) 179 | "And this is what we parsed from the XML:" 180 | (:p) 181 | (:textarea {:style "width: 95%; height: 300px;" } 182 | ~(webjure.json/serialize-str feed)))))))) 183 | 184 | 185 | ;;;;;;;;;;;;;;;;;;;; 186 | ;; defh regex test 187 | 188 | (defh #"/hello/(.+?)(/(.+))?$" 189 | [first-name 1 190 | last-name 3] 191 | {:output :html} 192 | 193 | `(:html 194 | (:body 195 | (:p "Hello " ~first-name " " ~(or last-name "")) 196 | 197 | ~@(if (nil? last-name) 198 | `((:a {:href ~(url (str "/hello/" first-name "/Something"))} 199 | "try with another path component")))))) 200 | 201 | 202 | ;;;;;;;;;;;;;; 203 | ;; json test 204 | 205 | (defh "/json" [] 206 | {:output :json} 207 | {"headers" (request-headers) 208 | "parameters" (request-parameters)}) 209 | 210 | (define-template page "page.cpt") 211 | 212 | (comment 213 | 214 | (defn page [data] 215 | `(:html 216 | {:xmlns "http://www.w3.org/1999/xhtml"} 217 | (:head 218 | (:meta {:http-equiv "content-type" :content "text/html; charset=utf-8"}) 219 | (:title ~(or (:title data) "Webjure demos page")) 220 | (:link {:href ~(url "/resource/default.css") :rel "stylesheet" :type "text/css"})) 221 | (:body 222 | (:div {:id "header"} 223 | (:h1 "Webjure") 224 | (:h2 "By Free CSS Templates")) 225 | 226 | (:div {:id "menu"} 227 | (:ul 228 | (:li {:class "first"} (:a {:href ~(url "/index")} "Home")) 229 | (:li (:a {:href "http://github.com/tatut/Webjure"} "On Github")))) 230 | 231 | (:div {:id "content"} 232 | (:div {:id "columnA"} 233 | ~@(:content data)) 234 | 235 | (:div {:id "columnB"} 236 | (:h2 "Recent Updates") 237 | (:p (:strong "January 11 2010") 238 | (:br) 239 | "Updated to 0.5-SNAPSHOT, now works with Clojure 1.1 and does AOT compiling.")) 240 | 241 | (:div {:style "clear: both;"} " ")) 242 | (:div {:id "footer"} 243 | (:p "Copyright © 2009-2010 Tatu Tarvainen. Designed by " 244 | (:a {:href "http://www.freecsstemplates.org" :class "link1"} "Free CSS Templates")))))) 245 | 246 | ) 247 | 248 | ;;;;;;;;;;;;;;; 249 | ;; Generate Webjure documentation in HTML and PDF formats 250 | 251 | (defn webjure-publics [] 252 | (sort-by #(str (:name %)) (map meta (vals (ns-publics 'webjure))))) 253 | 254 | 255 | (define-template docs "docs.cpt") 256 | 257 | (defh "/docs" [] {:output :print} 258 | (docs (webjure-publics))) 259 | 260 | (defn pdf [xsl-fo-str] 261 | (let [http-response (.getActualResponse *response*) 262 | out (.getOutputStream http-response) 263 | fop-factory (org.apache.fop.apps.FopFactory/newInstance) 264 | fop (.newFop fop-factory org.apache.fop.apps.MimeConstants/MIME_PDF out) 265 | transformer (.newTransformer (javax.xml.transform.TransformerFactory/newInstance))] 266 | (.setContentType http-response "application/pdf") 267 | (.transform transformer 268 | (javax.xml.transform.stream.StreamSource. 269 | (java.io.ByteArrayInputStream. (.getBytes xsl-fo-str))) 270 | (javax.xml.transform.sax.SAXResult. (.getDefaultHandler fop))))) 271 | 272 | 273 | (define-template docs-pdf "docs-pdf.cpt") 274 | 275 | (defh "/docs/pdf" [] {} 276 | (pdf (with-out-str (docs-pdf (webjure-publics))))) -------------------------------------------------------------------------------- /webjure/src/webjure/sql/crud.clj: -------------------------------------------------------------------------------- 1 | ;; A Quick and Dirty CRUD for SQL tables 2 | ;; 3 | ;; Main entry point is the ui macro which is meant to be called from inside a 4 | ;; Webjure handler. 5 | ;; 6 | ;; This core namespace defines the used protocols and 7 | ;; the main macros for creating CRUD views. 8 | ;; 9 | ;; Protocols 10 | ;; --------- 11 | ;; 12 | ;; InlineEdit: 13 | ;; Protocol for defining a way to allow inline editing of a column 14 | ;; data inside the listing view. 15 | ;; Uses AJAX calls to update the values, which the protocol must provide. 16 | ;; Use by defining an :inline-edit key in the field options. 17 | ;; 18 | ;; ListView: 19 | ;; Generic way to render different data types. 20 | 21 | (ns webjure.sql.crud 22 | (:refer-clojure) 23 | (:use webjure.sql) 24 | (:use webjure) 25 | (:use webjure.cpt) 26 | (:require webjure.profiler)) 27 | 28 | ;; Bound within a handler to the prefix of this CRUD handler 29 | ;; May be used for generating new URLs 30 | (def *current-path-prefix* nil) 31 | 32 | (defn crud-url [path argmap] 33 | (str *current-path-prefix* 34 | path 35 | "?"(reduce str 36 | (butlast (interleave (map (fn [[k v]] 37 | (str (name k) "=" v)) 38 | argmap) 39 | (repeat "&")))))) 40 | 41 | (defprotocol ListView 42 | (render-list-view [this])) 43 | 44 | (defprotocol InlineEdit 45 | ;; Method to render an HTML/JS inline edit view (and link) 46 | (render-inline-edit-view [this field current-value]) 47 | 48 | ;; Invoked by the AJAX handler of new values 49 | ;; the response will be JSONified and should be something 50 | ;; the UI rendereded by render-inline-edit-view can handle. 51 | (update-value [this id field new-value])) 52 | 53 | 54 | (defn page-link [here page] 55 | (let [{:keys [limit order dir]} here] 56 | (str "?start=" (* page limit) "&limit=" limit 57 | (when order (str "&order=" order)) 58 | (when dir (str "&dir=" dir))))) 59 | 60 | (defn listing-header-sort-attributes [here header] 61 | (let [field (get (:fields here) header)] 62 | (if (not (:sortable field)) 63 | [] 64 | {"class" (if (= (:order here) (name header)) 65 | (if (= "asc" (:dir here)) "sortAscending" "sortDescending") 66 | "normal") 67 | "onclick" (str "document.location = '" 68 | (page-link (merge here 69 | {:order (name header) 70 | :dir (if (= "asc" (:dir here)) "desc" "asc")}) 71 | (/ (:start here) (:limit here))) 72 | "';")}))) 73 | 74 | (define-template generic-listing-template "src/main/resources/crud/generic-listing-template.cpt") 75 | 76 | (extend-protocol ListView 77 | Object 78 | (render-list-view [obj] (str obj)) 79 | 80 | Boolean 81 | (render-list-view [b] 82 | (str "" 83 | (if b 84 | "☒" ;; ballot box with x 85 | "☐") ;; ballot box 86 | "")) 87 | nil 88 | (render-list-view [n] "NULL") 89 | ) 90 | 91 | (def +default-batch-size+ 25) 92 | 93 | (defn determine-query-tables [home-table field-list fields] 94 | "Determine all tables that are involved in the query. Returns a mapping 95 | from table name to it's query alias, eg. {\"firsttable\" \"t1\", \"secondtable\" \"t2\"}." 96 | (loop [[f & fs] field-list 97 | tables {home-table "t1"} 98 | counter 2] 99 | (if (nil? f) 100 | tables 101 | (let [{join :join} (fields f)] 102 | (if (not join) 103 | (recur fs tables counter) 104 | (recur fs (assoc tables 105 | (:table join) (str "t" counter)) 106 | (+ counter 1))))))) 107 | 108 | (defn determine-from-tables [home-table field-list fields] 109 | "Create a SQL FROM fragment with joins, eg. \"firsttable t1 LEFT JOIN secondtable t2 ON t1.sec_id=t2.id\". " 110 | (let [tables (determine-query-tables home-table field-list fields)] 111 | (loop [[f & fs] field-list 112 | from (str home-table " t1")] 113 | (if (nil? f) 114 | from 115 | (let [{join :join} (fields f)] 116 | (if (not join) 117 | (recur fs from) 118 | (recur fs (let [{:keys [table home-field foreign-field]} join] 119 | (str from " LEFT JOIN " table " " (tables table) " ON " 120 | (tables home-table) "." (name home-field) "=" (tables table) "." (name foreign-field)))))))))) 121 | 122 | (defn determine-foreign-keys [tables fields] 123 | "Determine additional foreign keys we need to fetch. Returns a list of field references." 124 | (apply vector 125 | (filter #(not (nil? %)) 126 | (map (fn [[field-name {join :join}]] 127 | (when join 128 | [(:table join) (:foreign-field join) 129 | (str (tables (:table join)) "." (name (:foreign-field join)))])) 130 | fields)))) 131 | 132 | 133 | 134 | (defn string-join 135 | ([coll] (string-join ", " coll)) 136 | ([sep coll] 137 | (reduce str 138 | (butlast (interleave coll (repeat sep)))))) 139 | 140 | (defn generate-listing [db table opt] 141 | `(webjure.profiler/with-step "Generate CRUD listing" 142 | (with-open [db# (~db)] 143 | (let [start# (Long/parseLong (or (request-parameter "start") "0")) 144 | limit# (let [lim# (request-parameter "limit")] 145 | (if lim# 146 | (Long/parseLong lim#) 147 | (or ~(:batch-size opt) +default-batch-size+))) 148 | order# (request-parameter "order") 149 | dir# (request-parameter "dir") 150 | list-fields# ~(:list-fields opt) 151 | primary-key# ~(:primary-key opt) 152 | fields# ~(:fields opt) 153 | tables# ~(determine-query-tables table (:list-fields opt) (:fields opt)) 154 | from-tables# ~(determine-from-tables table (:list-fields opt) (:fields opt)) 155 | foreign-key-fields# ~(determine-foreign-keys 156 | (determine-query-tables table (:list-fields opt) (:fields opt)) 157 | (:fields opt)) 158 | field-ref# (fn [f#] 159 | (let [{join# :join} (fields# (keyword f#))] 160 | (if (not join#) 161 | (str "t1." f#) 162 | (str (tables# (:table join#)) "." (name (:field join#)))))) 163 | ] 164 | (set-response-content-type *response* "text/html; charset=UTF-8") 165 | (println "REQUEST HEADERS: " (request-headers)) 166 | (with-open 167 | [out# (if (.contains (or (first (get (request-headers) "Accept-Encoding")) "") 168 | "gzip") 169 | (do (.setHeader *response* "Content-Encoding" "gzip") 170 | (java.io.OutputStreamWriter. (java.util.zip.GZIPOutputStream. (.getOutputStream *response*)) "UTF-8")) 171 | (response-writer))] 172 | (~(or (:list-template opt) 'webjure.sql.crud/generic-listing-template) 173 | out# 174 | {:list-fields list-fields# 175 | :fields fields# 176 | :start start# 177 | :limit limit# 178 | :order order# 179 | :dir dir# 180 | :rows (let [sql# (str 181 | "SELECT t1." (name primary-key#) ", " 182 | (string-join 183 | (concat (map #(nth % 2) foreign-key-fields#) 184 | (map (fn [field-name#] 185 | (let [{join# :join} (fields# field-name#)] 186 | (if join# 187 | (str (tables# (:table join#)) "." (name (:field join#)) 188 | " as " (name field-name#)) 189 | (str "t1." (name field-name#))))) 190 | list-fields#))) 191 | " FROM " from-tables# 192 | (when order# 193 | (str " ORDER BY " (field-ref# order#) " " (if (= "asc" dir#) "ASC" "DESC"))) 194 | (when (or (not (zero? start#)) (not (zero? limit#))) 195 | (str " LIMIT " start# ", " limit#))) 196 | drop# (+ 1 (count foreign-key-fields#)) 197 | res# (webjure.profiler/with-step "Execute SQL query" (query db# sql#))] 198 | ;; (println "QUERY: " sql#) 199 | (map (fn [row#] 200 | [(drop drop# row#) 201 | (first row#) 202 | (zipmap (map first foreign-key-fields#) 203 | (take (count foreign-key-fields#) 204 | (drop 1 row#)))]) 205 | res#)) 206 | :total-rows (ffirst (query db# (str "SELECT COUNT( " (name primary-key#) ") FROM " ~table)))})))))) 207 | 208 | 209 | (defn generate-select [db field table display-field value-field] 210 | `(str 211 | "\n")) 218 | 219 | (defn generate-field-editor [db field-name field-info] 220 | (let [{label :label join :join} field-info] 221 | (if join 222 | (generate-select db (:table join) (:field join) (:foreign-field join)) 223 | (str "")))) 224 | 225 | 226 | (defmacro ui "Generate a CRUD UI for the given table" 227 | [db table & options] 228 | (let [opt-pairs (partition 2 options) 229 | opt (zipmap (map first opt-pairs) 230 | (map second opt-pairs))] 231 | `(webjure.profiler/with-request-profiling 232 | (let [delete# (request-parameter "_delete") 233 | edit# (request-parameter "_edit") 234 | save# (request-parameter "_save")] 235 | (if delete# 236 | (send-output "text/plain" (str "Deleting " delete#)) 237 | (if edit# 238 | (send-output "text/plain" (str "Showing edit form for " edit#)) 239 | (if save# 240 | (send-output "text/plain" (str "Saving " save#)) 241 | ~(generate-listing db table opt)))))))) 242 | 243 | (defmacro define-crud-handler [prefix db table & options] 244 | (let [opt-pairs (partition 2 options) 245 | opt (zipmap (map first opt-pairs) 246 | (map second opt-pairs))] 247 | `(defh ~(re-pattern (str prefix "(/([^/]+))?$")) 248 | [pk# 2] {} 249 | (binding [*current-path-prefix* ~prefix] 250 | (webjure.profiler/with-request-profiling 251 | (if pk# 252 | (do 253 | (send-output "text/plain" (str (if (request-parameter "edit") 254 | "Edit " "View ") pk#))) 255 | ~(generate-listing db table opt))))))) -------------------------------------------------------------------------------- /webjure/src/webjure/xml.clj: -------------------------------------------------------------------------------- 1 | ;;; Defines recursive DOM tree parsing functions 2 | ;;; Author: Tatu Tarvainen 3 | ;;; Time-stamp: <2008-01-22 10:48:03 tadex> 4 | ;;; 5 | ;;; Sort of regular expressions for DOM trees: 6 | ;;; (<>* "name" ...) Allows 0 or more child elements with the name "name". 7 | ;;; This is an optimized form of (? (<>+ "name ...)) 8 | ;;; 9 | ;;; (<>+ "name" ...) Allows 1 or more child elements with the name "name". 10 | ;;; 11 | ;;; (<>? "name" ...) Allows 0 or 1 child elements with the name "name". 12 | ;;; This is an optimized form of (? (<> "name" ...)). 13 | ;;; 14 | ;;; (<> "name" ...) allows exactly 1 child element with the name "name" 15 | ;;; 16 | ;;; (attr "name" ...) allows only elements that have an attribute named "name" 17 | ;;; 18 | ;;; (attr ["name" "value"] ...) allows only elements that have an attribute named "name" 19 | ;;; with the value "value" 20 | ;;; 21 | ;;; (? ...) The optional operator. 22 | ;;; Catches failures of the sub-parsers and continues normally. 23 | ;;; 24 | ;;; (|| ...) The OR operator. Tries all sub-parsers and returns when the 25 | ;;; first one matches. If no sub-parser matches, the OR fails. 26 | ;;; 27 | ;;; (fn [node parse-state] ...) apply function to each mached node 28 | ;;; 29 | ;;; (=> var state ...) helper macro for (fn [node] ...) that creates a function 30 | ;;; that returns nil at the end (unless the return value is a 31 | ;;; an error dictionary. 32 | ;;; 33 | ;;; All parser definition can have any amount of sub-parsers which are 34 | ;;; run in case of a match. Sub-parsers are invoked in the order they 35 | ;;; are specified (effectively ANDing them). If a parser has two sub-parsers 36 | ;;; and the first one fails, the second will not be run. 37 | 38 | ;;; A state object is passed along as the parsing is done 39 | ;;; the user fn functions can modify state by returning a new one 40 | ;;; (default parsing functions just return the same state they got as 41 | ;;; a parameter). 42 | ;;; A return value for a handler can be [:error node description] or 43 | ;;; [:state new-state-obj]. 44 | ;;; 45 | ;;; The helper macros: (error node ...description...) and (yield new-state-obj) 46 | ;;; produce the above return values. 47 | ;;; 48 | ;;; Helper functions collect and collect-as provide easy ways to capture 49 | ;;; parsed objects to user state in seq or map. 50 | ;;; 51 | ;;; ---- a very simple example ---- 52 | ;;; The following parsing code: 53 | ;;; (parse tree [] (<> "foo" (<>* "bar" (collect text)))) 54 | ;;; Run on this tree: 55 | ;;; 56 | ;;; one 57 | ;;; two 58 | ;;; three 59 | ;;; 60 | ;;; Will return the value: 61 | ;;; ["one" "two" "three"] 62 | 63 | 64 | (ns webjure.xml 65 | (:refer-clojure)) 66 | 67 | 68 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 69 | ;; DOM utilities and accessors ;; 70 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 71 | 72 | ;; Return the type of the element 73 | (defn type-of [#^org.w3c.dom.Node node] 74 | (let [node-type (. node (getNodeType))] 75 | (cond 76 | (= node-type (. org.w3c.dom.Node ELEMENT_NODE)) :element 77 | (= node-type (. org.w3c.dom.Node ATTRIBUTE_NODE)) :attribute 78 | (= node-type (. org.w3c.dom.Node TEXT_NODE)) :text 79 | (= node-type (. org.w3c.dom.Node CDATA_SECTION_NODE)) :cdata-section 80 | (= node-type (. org.w3c.dom.Node ENTITY_REFERENCE_NODE)) :entity-reference 81 | (= node-type (. org.w3c.dom.Node ENTITY_NODE)) :entity 82 | (= node-type (. org.w3c.dom.Node PROCESSING_INSTRUCTION_NODE)) :processing-instruction 83 | (= node-type (. org.w3c.dom.Node COMMENT_NODE)) :comment 84 | (= node-type (. org.w3c.dom.Node DOCUMENT_NODE)) :document 85 | (= node-type (. org.w3c.dom.Node DOCUMENT_TYPE_NODE)) :document-type 86 | (= node-type (. org.w3c.dom.Node DOCUMENT_FRAGMENT_NODE)) :document-fragment 87 | (= node-type (. org.w3c.dom.Node NOTATION_NODE)) :notation))) 88 | 89 | ;; Get the name of given node 90 | (defn name-of [#^org.w3c.dom.Node node] 91 | (. node (getNodeName))) 92 | 93 | ;; Check if the given node is an element (optionally check 94 | ;; the element name also) 95 | (defn element? 96 | ([node] (= :element (type-of node))) 97 | ([node name] (and (= :element (type-of node)) 98 | (= name (name-of node))))) 99 | 100 | ;; Fetch named attribute of node 101 | (defn attribute [#^org.w3c.dom.Element node name] 102 | (let [attr-node (. node (getAttributeNode name))] 103 | (and attr-node (. attr-node (getValue))))) 104 | 105 | ;; Return all children as a list 106 | (defn children [#^org.w3c.dom.Node parent] 107 | (let [#^org.w3c.dom.NodeList 108 | all-children (. parent (getChildNodes)) 109 | child-count (. all-children (getLength))] 110 | (loop [i 0 111 | acc []] 112 | (if (== i child-count) 113 | acc 114 | (recur (+ i 1) 115 | (conj acc (. all-children (item i)))))))) 116 | 117 | 118 | (defn child-elements [#^org.w3c.dom.Element parent] 119 | (filter element? (children parent))) 120 | 121 | ;; Fetch text content 122 | (defn text [#^org.w3c.dom.Element node] 123 | (reduce str (map (fn [#^org.w3c.dom.CharacterData x] (. x (getData))) 124 | (filter (fn [x] (= :text (type-of x))) (children node))))) 125 | 126 | (defn new-document-builder [] 127 | (. (. javax.xml.parsers.DocumentBuilderFactory (newInstance)) (newDocumentBuilder))) 128 | 129 | ;;; Load XML DOM from the given source 130 | (defmulti load-dom class) 131 | (defmethod load-dom String [string] 132 | (.parse (new-document-builder) 133 | (org.xml.sax.InputSource. (java.io.StringReader. string)))) 134 | 135 | (defmethod load-dom java.io.File [file] 136 | (load-dom (java.io.FileInputStream. file))) 137 | 138 | (defmethod load-dom java.io.InputStream [stream] 139 | (.parse (new-document-builder) stream)) 140 | 141 | 142 | 143 | (defn matches? [spec str] 144 | ;; PENDING: allow xpath style matching of word inside value 145 | (= spec str)) 146 | 147 | ;;; Fetch all child elements with the given name 148 | (defn child-elements-by-name [#^org.w3c.dom.Element parent name] 149 | (filter 150 | (fn [child] (element? child name)) 151 | (children parent))) 152 | 153 | (defn child-element [#^org.w3c.dom.Element parent name] 154 | (. (. parent (getElementsByTagName name)) (item 0))) 155 | 156 | 157 | (defmacro error [node & items] 158 | `[:error ~node (reduce str (map str (list ~@items)))]) 159 | 160 | (defn yield [new-state] 161 | `[:state ~new-state]) 162 | 163 | (defn error-state? [state] 164 | (= :error (first state))) 165 | 166 | ;; Run the given sub-parsers on node 167 | (defn #^{:private true} run-sub-parsers [node current-state parsers] 168 | (loop [ps parsers 169 | state current-state] 170 | (if (or (error-state? state) 171 | (empty? ps)) 172 | state 173 | (recur (rest ps) 174 | ((first ps) node state))))) 175 | 176 | ;; Run the given sub-parsers on each child 177 | (defn do-children [children current-state parsers] 178 | (loop [ch children 179 | state current-state] 180 | (if (or (error-state? state) 181 | (empty? ch)) 182 | state 183 | (recur (rest ch) 184 | (run-sub-parsers (first ch) state parsers))))) 185 | 186 | 187 | (defn parse [tree initial-state & parsers] 188 | (let [res (run-sub-parsers tree (yield initial-state) parsers)] 189 | (if (error-state? res) 190 | res 191 | (second res)))) 192 | 193 | ;; Parse as a sub-parser, returns a function that takes the 194 | ;; node to parse. 195 | ;; This can be used to change state for sub parsers. 196 | (defn parse* [new-state & parsers] 197 | (fn [node] 198 | (let [res (run-sub-parsers node (yield new-state) parsers)] 199 | (if (error-state? res) 200 | res 201 | (second res))))) 202 | 203 | (def p* parse*) ;; short hand for parse* 204 | 205 | ;; Evaluate exprs for side-effects, does not change state 206 | (defmacro => [nodesym statesym & exprs] 207 | `(fn [~nodesym state#] 208 | (let [~statesym (second state#)] 209 | (let [newstate# (do ~@exprs)] 210 | (if (nil? newstate#) 211 | state# 212 | newstate#))))) 213 | 214 | 215 | ;; Allow exactly one child with the given name 216 | (defn <> [name & sub-parsers] 217 | (fn [parent state] 218 | (let [children (child-elements-by-name parent name) 219 | count (count children)] 220 | (if (== count 1) 221 | (do-children children state sub-parsers) 222 | (error parent "Expected exactly one " name 223 | " child element in node " (name-of parent)))))) 224 | 225 | 226 | ;; Allow zero or more children with the given name 227 | (defn <>* [name & sub-parsers] 228 | (fn [parent state] 229 | (do-children (child-elements-by-name parent name) state sub-parsers))) 230 | 231 | ;; Allow zero or one child with the given name 232 | (defn <>? [name & sub-parsers] 233 | (fn [parent state] 234 | (let [children (child-elements-by-name parent name)] 235 | (if (not (nil? (second children))) 236 | (error parent "Expected at most one " name " child element in node " (name-of parent)) 237 | (do-children children state sub-parsers))))) 238 | 239 | ;; Allow one or more children with the given name 240 | (defn <>+ [name & sub-parsers] 241 | (fn [parent state] 242 | (let [children (child-elements-by-name parent name)] 243 | (if (nil? (first children)) 244 | (error parent "Expected one or more " name " child elements in node " (name-of parent)) 245 | (do-children children state sub-parsers))))) 246 | 247 | 248 | ;; Require attributes (and optionally value) 249 | ;; attr-spec = attr-name | [attr-name attr-value] 250 | ;; FIXME: attr has some problem with state 251 | (defn attr [attr-spec & sub-parsers] 252 | ;; FIXME: implement as before, now just requires named attribute 253 | (fn [parent state] 254 | (if (not (element? parent)) 255 | (error parent "Tried to check attribute on a non-element node") 256 | 257 | (let [attr-value (if (instance? java.lang.String attr-spec) nil (second attr-spec)) 258 | attr-name (or (and attr-value (first attr-spec)) attr-spec) 259 | attr-node (. #^org.w3c.dom.Element parent (getAttributeNode attr-name))] 260 | (if (nil? attr-node) 261 | (error parent "Element " (name-of parent) " does not have required attribute " attr-name) 262 | 263 | (if (or (nil? attr-value) (matches? attr-value (. attr-node (getValue)))) 264 | (run-sub-parsers attr-node state sub-parsers) 265 | (error parent "Element attribute " attr-name " does not match " attr-value))))))) 266 | 267 | 268 | 269 | ;;; Catch failures of the sub-parsers and continue as if 270 | ;;; no error occured. 271 | (defn ? [& sub-parsers] 272 | (fn [node] 273 | ;; PENDING: Should we catch each sub-parsers separately? 274 | ;; We could wrap then in fns that catch exceptions 275 | (try (run-sub-parsers node sub-parsers) 276 | (catch java.lang.RuntimeException x true)))) 277 | 278 | 279 | ;;; The OR operator 280 | (defn || [& sub-parsers] 281 | (fn [node state] 282 | (loop [sp sub-parsers] 283 | (if (empty? sp) 284 | ;; No sub-parser succeeded, throw an exception 285 | (error node "OR operator failed for node " (name-of node)) 286 | 287 | (if (not (try ((first sp) node) true 288 | (catch java.lang.RuntimeException ex false))) 289 | (recur (rest sp))))))) 290 | 291 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;; 292 | ;; Parsing actions helpers 293 | ;; 294 | 295 | 296 | ;; Collect the value obtained by applying fun to the node 297 | ;; as a keyed map value. Yields a new state with the new 298 | ;; key/value mapped. The old state must be a map. 299 | ;; If the fun is omitted, identity is used (collecting the node). 300 | (defmacro collect-as 301 | ([key] (collect-as key identity)) 302 | ([key fun] 303 | `(=> elt# st# 304 | (yield (assoc st# ~key (~fun elt#)))))) 305 | 306 | ;; Collect a new sequence value obtained by applying fun to the 307 | ;; node. Yields a new state with the new value conj'ed to the 308 | ;; previous state (which must be a seq). 309 | ;; If the fun is omitted, identity is used (collecting the node). 310 | (defmacro collect 311 | ([] (collect identity)) 312 | ([fun] `(=> elt# st# 313 | (yield (conj st# (~fun elt#)))))) 314 | 315 | 316 | ;; Modify state by applying a function to the previous state and the element 317 | (defmacro modify [fun] 318 | `(=> elt# st# 319 | (yield (~fun st# elt#)))) 320 | 321 | (defmacro modify-key [key fun] 322 | `(=> elt# st# 323 | (yield (assoc st# 324 | ~key (~fun (st# ~key) elt#))))) -------------------------------------------------------------------------------- /webjure/src/webjure/cpt.clj: -------------------------------------------------------------------------------- 1 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 2 | ;; CPT - Clojure Page Templates 3 | ;; 4 | ;; A *FAST* of a JPT/ZPT alike templating system in Clojure. 5 | ;; Compiles XML template files into byte code via the Clojure compiler. 6 | ;; 7 | 8 | (ns webjure.cpt 9 | (:use webjure.xml) 10 | (:refer-clojure)) 11 | 12 | ;; (set! *warn-on-reflection* true) 13 | 14 | (defmacro output [& things] 15 | `(do 16 | ~@(map (fn [t] 17 | (if (string? t) 18 | `(.write ~'output ~t) 19 | `(.write ~'output (str ~t)))) things))) 20 | 21 | (comment 22 | (defn output "Apply str to things and print the result" [& things] 23 | (print (apply str things)))) 24 | 25 | 26 | (defn- load-template-xml [path] 27 | (let [xml-string (slurp path)] 28 | (.getDocumentElement 29 | (load-dom 30 | (if (.startsWith xml-string "\n" xml-string)))))) 33 | 34 | (def +cpt-ns+ "http://webjure.org/namespaces/cpt") 35 | 36 | (defn- cpt-attribute? [{name :name}] 37 | (or (= name "xmlns:cpt") 38 | (.startsWith name "cpt:"))) 39 | 40 | (defn- attr-seq 41 | ([elt] (attr-seq (.getAttributes elt) 0)) 42 | ([node-map index] 43 | (if (>= index (.getLength node-map)) 44 | nil 45 | (cons (let [attr (.item node-map index)] 46 | {:name (.getName attr) 47 | :value (.getValue attr) 48 | :ns (.getNamespaceURI attr)}) 49 | (attr-seq node-map (+ index 1)))))) 50 | 51 | (defmulti handle-node (fn [_ elt] (type-of elt))) 52 | 53 | (defn- string-reader [str] 54 | (java.io.PushbackReader. (java.io.StringReader. str))) 55 | 56 | (defn- read-many 57 | ([str] (read-many (string-reader str) [])) 58 | ([rdr acc] 59 | (let [item (read rdr false :eof)] 60 | (if (= item :eof) 61 | acc 62 | (read-many rdr (conj acc item)))))) 63 | 64 | (defn- read-first "Read the first form in the given input string. Returns the read item and the remaining string." 65 | [string] 66 | (let [rdr (string-reader string) 67 | item (read rdr)] 68 | (loop [acc "" 69 | ch (.read rdr)] 70 | (if (= -1 ch) 71 | [item acc] 72 | (recur (str acc (char ch)) 73 | (.read rdr)))))) 74 | 75 | 76 | (defn- get-and-remove-attribute [elt attr] 77 | (let [value (.getAttribute elt attr)] 78 | (.removeAttribute elt attr) 79 | (if (empty? value) 80 | nil 81 | value))) 82 | 83 | (defmacro ^{:private true} define-attribute-handler [name attribute ctx-var attr-var elt-var & body] 84 | `(defn ~name [~ctx-var ~elt-var] 85 | (let [~attr-var (get-and-remove-attribute ~elt-var ~attribute)] 86 | (if (not (nil? ~attr-var)) 87 | (do ~@body) 88 | nil)))) 89 | 90 | (declare handle-element) 91 | 92 | (define-attribute-handler handle-let "cpt:let" ctx value elt 93 | `(let [~@(read-many value)] 94 | ~(handle-element ctx elt))) 95 | 96 | (define-attribute-handler handle-when "cpt:when" ctx value elt 97 | `(when ~(read-string value) 98 | ~(handle-element ctx elt))) 99 | 100 | (define-attribute-handler handle-repeat "cpt:repeat" ctx value elt 101 | (let [[var items] (read-many value) 102 | var-idx (symbol (str var "-idx"))] 103 | `(let [items# ~items 104 | item-count# (count items#)] 105 | (loop [[item# & items#] items# 106 | i# 0 107 | ~var-idx 0] 108 | (when (< i# item-count#) 109 | (let [~var item#] 110 | ~(handle-element ctx (.cloneNode elt true)) 111 | (recur items# (+ i# 1) (+ 1 ~var-idx)))))))) 112 | 113 | (define-attribute-handler handle-replace "cpt:replace" ctx value elt 114 | `(output ~@(read-many value))) 115 | 116 | (define-attribute-handler handle-include "cpt:include" ctx value elt 117 | (let [base-path (.getParentFile (:template-file ctx)) 118 | included-template (java.io.File. base-path value) 119 | include (load-template-xml (.getAbsolutePath included-template)) 120 | ctx (assoc ctx :template-file included-template)] 121 | (handle-node ctx include))) 122 | 123 | (defn escape-xml "Escape XML entities: &, < and >." [evil] 124 | (let [^String s (str evil)] 125 | (.replace (.replace (.replace s "&" "&") 126 | "<" "<") 127 | ">" ">"))) 128 | 129 | (defn handle-text "Expands code to output text with $ form references." 130 | ([text] (handle-text text [])) 131 | ([text acc] 132 | (if (empty? text) 133 | `(output ~@acc) 134 | (let [dollar-pos (.indexOf text "$")] 135 | (if (= -1 dollar-pos) 136 | ;; No expansions, output whole text 137 | `(output ~@acc ~text) 138 | ;; Expansion found 139 | (let [before (.substring text 0 dollar-pos) 140 | text (.substring text (+ 1 dollar-pos)) 141 | escape? (not (.startsWith text "@")) 142 | text (if escape? text (.substring text 1)) 143 | [item after] (read-first text)] 144 | (handle-text after (concat acc [before (if escape? 145 | `(escape-xml ~item) 146 | item)])))))))) 147 | 148 | ;; This is bound to a (ref {}) during template processing 149 | (def *magic-resources* nil) 150 | 151 | (defn sha1 [data] 152 | (let [md (java.security.MessageDigest/getInstance "SHA-256")] 153 | (format "%064x" (BigInteger. 1 (.digest md (.getBytes data "UTF-8")))))) 154 | 155 | (defn handle-magic-resource [ctx elt] 156 | (let [tag (.getTagName elt) 157 | [first-child & other-children] (children elt)] 158 | 159 | ;; Only consider a magic resource if this element 160 | ;; has exactly one child which is a CDATA section 161 | (when (and first-child 162 | (empty? other-children) 163 | (= :cdata-section (type-of first-child))) 164 | 165 | (let [txt (.getWholeText first-child) 166 | hash (sha1 txt)] 167 | (when (> (count txt) 512) ;; don't magic resource things under half a kb 168 | (cond 169 | (= tag "style") (do 170 | (dosync 171 | (alter *magic-resources* 172 | assoc (str hash ".css") {:content-type "text/css" 173 | :data txt})) 174 | `(output ~(str ""))) 176 | (= tag "script") `(do (output "")))))))) 177 | 178 | (defn handle-element [ctx elt] 179 | (.normalize elt) ;; ensure Text nodes are intact 180 | (or (handle-let ctx elt) 181 | (handle-when ctx elt) 182 | (handle-repeat ctx elt) 183 | (handle-replace ctx elt) 184 | (handle-include ctx elt) 185 | (handle-magic-resource ctx elt) 186 | 187 | ;; Normal handling, after most specials have been taken care of 188 | (let [tag (.getTagName elt) 189 | attrs (filter #(not (cpt-attribute? %)) (attr-seq elt)) 190 | children (children elt) 191 | omit? (= tag "cpt:block")] 192 | (if (and (empty? attrs) (empty? children)) 193 | (if omit? nil `(output ~(str "<" tag "/>"))) 194 | (if omit? 195 | `(do ~@(map #(handle-node ctx %) children)) 196 | `(do (output ~(str "<" tag)) 197 | ~@(let [cpt-attributes (first (filter #(= "cpt:attributes" (:name %)) (attr-seq elt)))] 198 | (when cpt-attributes 199 | `[ (doseq [[n# v#] ~(read-string (:value cpt-attributes))] 200 | (output " " n#) 201 | (when v# 202 | (output "=\"" v# "\""))) ])) 203 | 204 | ~@(map (fn [{name :name, value :value}] 205 | `(do 206 | (output ~(str " " name "=\"")) 207 | ~(handle-text value) 208 | (output "\""))) 209 | attrs) 210 | (output ">") 211 | ~@(map #(handle-node ctx %) children) 212 | (output ~(str "")))))))) 213 | 214 | (defmethod handle-node :element [ctx elt] 215 | (handle-element ctx elt)) 216 | 217 | (defmethod handle-node :default [ctx node] 218 | true) 219 | 220 | (defmethod handle-node :text [ctx node] 221 | ;; NOTE: If the document is not in the normalized form, text may be split into 222 | ;; multiple adjacent text nodes. We may need to preprocess and join them... 223 | (handle-text (.getWholeText node))) 224 | 225 | 226 | (defn output-form? [form] 227 | (and (coll? form) 228 | (= 'webjure.cpt/output (first form)))) 229 | 230 | (defmacro ^{:private true} define-form-reduction [name initial-test-fn reduce-test-fn reduce-fn] 231 | `(defn ^{:private true} ~name [form#] 232 | (if (not (~initial-test-fn form#)) 233 | form# 234 | (loop [acc# [] 235 | [current# & items#] form#] 236 | (if (nil? current#) 237 | (apply list acc#) 238 | (if (not (~reduce-test-fn current#)) 239 | (recur (conj acc# (~name current#)) items#) 240 | ;; current item is reduceable 241 | (if (~reduce-test-fn (last acc#)) 242 | ;; previous item is reduceable, combine with that 243 | (recur (conj (vec (butlast acc#)) 244 | (~reduce-fn (last acc#) current#)) 245 | items#) 246 | (recur (conj acc# current#) items#)))))))) 247 | 248 | ;; Define reduction to reduce adjacent string literals 249 | ;; eg. (output "foo" "bar") => (output "foobar") 250 | (define-form-reduction optimize-output-form 251 | output-form? string? str) 252 | 253 | ;; Define reduction to reduce adjacent output calls to a single call 254 | ;; eg. (do (output "foo") (output "bar")) => (do (output "foo" "bar")) 255 | ;; Calls optimize-output-form on the resulting output calls to further 256 | ;; optimize them. 257 | (define-form-reduction optimize-adjacent-output-forms 258 | #(and (not (vector? %)) (coll? %)) 259 | output-form? 260 | (fn [left right] 261 | (optimize-output-form `(output ~@(rest left) ~@(rest right))))) 262 | 263 | (defn- do-form? [form] 264 | (and (coll? form) (= 'do (first form)))) 265 | 266 | (defn flatten-nested-do "Flatten code in nested do structures. Turns (do a b (do c d)) into (do a b c d)" 267 | [form] 268 | (if (or (vector? form) (not (coll? form))) 269 | form ;; not a list form, just return this 270 | (if (not (do-form? form)) 271 | ;; not a (do ...) form, apply optimization recursively 272 | (map flatten-nested-do form) 273 | (loop [acc [] 274 | [item & items] (rest form)] 275 | (if (nil? item) 276 | `(do ~@acc) 277 | (let [optimized-item (flatten-nested-do item)] 278 | (if (do-form? optimized-item) 279 | (recur (concat acc (vec (rest optimized-item))) 280 | items) 281 | (recur (conj (vec acc) optimized-item) 282 | items)))))))) 283 | 284 | (defn optimize [form] 285 | (optimize-adjacent-output-forms (flatten-nested-do form))) 286 | 287 | 288 | (def *template-path* nil) 289 | 290 | (defn- template-path [] 291 | (or *template-path* (System/getProperty "webjure.cpt.path"))) 292 | 293 | (def *reload-templates* (ref false)) 294 | 295 | (defn set-reload-templates! [reload?] 296 | "Set wether or not to reload templates. This affects only templates defined after setting the value." 297 | (dosync 298 | (ref-set *reload-templates* reload?))) 299 | 300 | (defn- generate-template-defn [name file ctx] 301 | (binding [*magic-resources* (ref {})] 302 | (let [code `(do 303 | (defn ~name 304 | ([~'here] (~name *out* ~'here)) 305 | ([~'^java.io.Writer output ~'here] 306 | ~@(optimize (handle-node ctx (load-template-xml (.getCanonicalPath file)))))) 307 | ~@(map (fn [[name {ct :content-type data :data}]] 308 | `(webjure/defh ~(str "/static/" name) [] {} 309 | (doto webjure/*response* 310 | (.setDateHeader "Date" (System/currentTimeMillis)) 311 | (.setDateHeader "Last-Modified" (- (System/currentTimeMillis) 312 | ~(* 1000 60 60 24))) 313 | (.setDateHeader "Expires" (+ (System/currentTimeMillis) 314 | ~(* 1000 60 60 24 350))) 315 | (.setHeader "Cache-Control" "public") 316 | (.setHeader "Vary" "Accept-Encoding")) 317 | (webjure/send-output ~ct ~data))) 318 | @*magic-resources*))] 319 | ;; (println "magic resources after processing: " (str @*magic-resources*)) 320 | code))) 321 | 322 | 323 | (defn generate-reloading-template-defn [name file ctx] 324 | `(defn ~name 325 | ([~'here] (~name *out* ~'here)) 326 | ([~'^java.io.Writer output ~'here] 327 | (println ~(str "RELOADING TEMPLATE " (.getCanonicalPath file))) 328 | (binding [*ns* (find-ns '~(ns-name *ns*))] 329 | ((eval '(template ~(.getCanonicalPath file))) ~'output ~'here))))) 330 | 331 | 332 | (defmacro define-template 333 | "Define a template as a function at compile time." 334 | [name file] 335 | (let [file (java.io.File. (template-path) file) 336 | ctx {:template-file file}] 337 | (if (not (.canRead file)) 338 | (throw (IllegalArgumentException. (str "Unable to define template \"" (.getCanonicalPath file) "\". The specified file cannot be read. (Maybe you need to define \"webjure.cpt.path\" system property)"))) 339 | (if @*reload-templates* 340 | (generate-reloading-template-defn name file ctx) 341 | (generate-template-defn name file ctx))))) 342 | 343 | (defmacro template [file] 344 | "Compile a template into a function. (fn [here] ...)" 345 | (let [ctx {:template-file (java.io.File. file)}] 346 | `(fn [~'output ~'here] 347 | ~@(optimize (handle-node ctx (load-template-xml file)))))) -------------------------------------------------------------------------------- /webjure/src/webjure.clj: -------------------------------------------------------------------------------- 1 | 2 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 3 | ;; Webjure - a web framework for Clojure 4 | ;; 5 | ;; Author: Tatu Tarvainen 6 | ;; 7 | 8 | (ns webjure 9 | (:refer-clojure) 10 | (:require (webjure html json))) 11 | 12 | ;;;;;;;;;;;;;;;;;;;;;; 13 | ;; Global vars 14 | 15 | (def +version+ "Webjure 0.8") 16 | 17 | ;; The *request* and *response* vars are bound to the servlet/portlet request and 18 | ;; response objects fo the request currently being handled. 19 | (def *request*) 20 | (def *response*) 21 | 22 | (def #^{:doc "The matched handler info is bound here during dispatch."} 23 | *matched-handler* nil) 24 | 25 | 26 | ;;;;;;;;;;;;;;;;;;;;; 27 | ;; String utilities 28 | 29 | (defn starts-with? [#^String str #^String prefix] 30 | (. str (startsWith prefix))) 31 | 32 | (defn ends-with? [#^String str #^String suffix] 33 | (. str (endsWith suffix))) 34 | 35 | (defn strlen [#^String str] 36 | (. str (length))) 37 | 38 | (defn substr 39 | ([#^String s start] (. s (substring start))) 40 | ([#^String s start end] (. s (substring start end)))) 41 | 42 | (defn append [#^java.lang.Appendable out & #^String stuff] 43 | (doseq [thing stuff] 44 | (. out (append (str thing))))) 45 | 46 | (defn 47 | #^{:doc "URL encode a string."} 48 | urlencode 49 | ([#^String s] (urlencode s "UTF-8")) 50 | ([#^String s #^String encoding] 51 | (. java.net.URLEncoder (encode s encoding)))) 52 | 53 | (defn 54 | #^{:doc "Decode an URL encoded string."} 55 | urldecode 56 | ([#^String s] (urldecode s "UTF-8")) 57 | ([#^String s #^String encoding] 58 | (. java.net.URLDecoder (decode s encoding)))) 59 | 60 | 61 | ;;;;;;;;;;;;;;;;;;;;; 62 | ;; Handler dispatch 63 | 64 | ;; List of handlers as [fn url-pattern] 65 | (def *handlers* (ref (list))) 66 | 67 | ;; This is called to register a handler 68 | (defn 69 | #^{:doc "Publish a handler function for the given URL pattern. The pattern may be a string or a regular expression pattern."} 70 | publish [fn url-pattern] 71 | (if (not (instance? clojure.lang.IFn fn)) 72 | (throw (new java.lang.IllegalArgumentException "First argument must be function"))) 73 | (dosync 74 | (ref-set *handlers* 75 | (conj (filter 76 | #(not (= url-pattern (second %))) ; filter out previous handlers with the same pattern 77 | @*handlers*) 78 | [fn url-pattern])))) 79 | 80 | (defn 81 | #^{:private true :doc "Check if handler matches the input URL. Returns a match object (map) or nil."} 82 | handler-matches? [[handler-fn url-pattern] pattern] 83 | (cond 84 | (instance? java.util.regex.Pattern url-pattern) 85 | (let [m (re-find url-pattern pattern)] 86 | (if m 87 | {:handler handler-fn 88 | :priority 0 89 | :groups m} 90 | nil)) 91 | 92 | (or (and (ends-with? url-pattern "*") 93 | (starts-with? pattern (substr url-pattern 0 (- (strlen url-pattern) 1))) 94 | true) 95 | (and (= pattern url-pattern))) 96 | {:handler handler-fn :priority (strlen url-pattern)})) 97 | 98 | 99 | (defn 100 | #^{:private true} 101 | find-handler [url-pattern] 102 | (let [matching-handlers (filter #(not (nil? %)) 103 | (map (fn [handler] 104 | (handler-matches? handler url-pattern)) @*handlers*)) 105 | shortest-match (first (sort-by :priority matching-handlers))] 106 | shortest-match)) 107 | 108 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 109 | ;; Access to servlet info 110 | ;; Getters for request / response 111 | 112 | 113 | ;; Servlets use a lot of old Enumerations, which need to be turned into sequences 114 | (defn 115 | #^{:doc "Convert a Java Enumeration into a Clojure sequence."} 116 | enumeration->list [#^java.util.Enumeration en] 117 | (loop [acc (list)] 118 | (if (not (. en (hasMoreElements))) 119 | (reverse acc) 120 | (recur (conj acc (. en (nextElement))))))) 121 | 122 | (defprotocol Request 123 | "Webjure request abstraction" 124 | (get-request-path [x]) 125 | (get-request-headers [x]) 126 | (get-request-param [x name]) 127 | (get-request-param-values [x name]) 128 | (get-request-params [x]) 129 | (get-request-base-url [x]) 130 | (get-request-input-stream [x]) 131 | (create-url [x mode-or-path args]) 132 | (get-request-session-attribute [x attribute]) 133 | (set-request-session-attribute [x attribute value]) 134 | (remove-request-session-attribute [x attribute])) 135 | 136 | (defprotocol Response 137 | "Webjure response abstraction" 138 | (get-response-writer [x]) 139 | (send-response-error [x error-code error-message]) 140 | (set-response-content-type [x type]) 141 | (send-response-redirect [x to])) 142 | 143 | ;; Implement the Request abstraction for HTTP Servlets 144 | (extend-protocol Request 145 | javax.servlet.http.HttpServletRequest 146 | (get-request-path [req] (.getPathInfo req)) 147 | (get-request-headers 148 | [req] 149 | (let [names (enumeration->list (.getHeaderNames req))] 150 | (zipmap names 151 | (map #(enumeration->list (.getHeaders req %)) names)))) 152 | (get-request-param 153 | [req name] 154 | (.getParameter req name)) 155 | (get-request-param-values 156 | [req name] 157 | (seq (.getParameterValues req name))) 158 | (get-request-params 159 | [req] 160 | (let [params (seq (.getParameterMap req))] 161 | (zipmap (map first params) 162 | (map #(seq (second %)) params)))) 163 | (get-request-base-url 164 | [req] 165 | (str (.getScheme req) 166 | "://" 167 | (.getServerName req) 168 | (let [port (.getServerPort req)] 169 | (if (not (or (== port 80) (== port 443))) 170 | (str ":" port) 171 | "")) 172 | (.getContextPath req))) 173 | (get-request-input-stream 174 | [req] 175 | (.getInputStream req)) 176 | (create-url 177 | [req path args] 178 | (str 179 | (get-request-base-url req) 180 | path 181 | "?" 182 | (reduce str 183 | (interleave (map (fn [key] 184 | (str (urlencode (if (keyword? key) 185 | (substr (str key) 1) 186 | key)) 187 | "=" (urldecode (get args key)))) 188 | (keys args)) 189 | (repeat "&"))))) 190 | (get-request-session-attribute 191 | [req attribute] 192 | (.getAttribute (.getSession req) attribute)) 193 | (set-request-session-attribute 194 | [req attribute value] 195 | (.setAttribute (.getSession req) attribute value)) 196 | (remove-request-session-attribute 197 | [req attribute] 198 | (.removeAttribute (.getSession req) attribute)) 199 | ) 200 | 201 | (extend-protocol Response 202 | javax.servlet.http.HttpServletResponse 203 | (get-response-writer [res] (.getWriter res)) 204 | (send-response-error [res error-code error-message] (.sendError res error-code error-message)) 205 | (set-response-content-type [res type] 206 | (.setContentType res type)) 207 | (send-response-redirect [res to] 208 | (.sendRedirect res to))) 209 | 210 | 211 | (defn 212 | #^{:doc "Returns the request path information (servlet only)"} 213 | request-path 214 | ([] (request-path *request*)) 215 | ([request] (get-request-path request))) 216 | 217 | (defn 218 | #^{:doc "Get a Writer object for this response."} 219 | response-writer 220 | ([] (response-writer *response*)) 221 | ([response] (. response (getWriter)))) 222 | 223 | (defn request-headers 224 | ([] (request-headers *request*)) 225 | ([request] (get-request-headers request))) 226 | 227 | 228 | ;; Dynamically calculate and return the app baseurl 229 | ;; based on the current request 230 | (defn base-url 231 | ([] (base-url *request*)) 232 | ([request] 233 | (get-request-base-url request))) 234 | 235 | ;; FIXME: implement Request abstraction fro PortletRequest 236 | (defn 237 | ^{:private true} 238 | create-portlet-url [^javax.portlet.PortletRequest request mode args] 239 | (let [url (if (= :action mode) 240 | (. *response* (createActionURL)) 241 | (. *response* (createRenderURL)))] 242 | (doseq [key (keys args)] 243 | (. url (setParameter key (get args key)))) 244 | (. url (toString)))) 245 | 246 | ;; Generate an HREF link. 247 | ;; For portlets the mode-or-path must be :action or :render 248 | ;; and for servlets it must be a string path element 249 | (defn url "Generate an HREF URL given a path (or mode for portlets) and GET parameters." 250 | ([mode-or-path] (url mode-or-path {})) 251 | ([mode-or-path args] 252 | (create-url *request* mode-or-path args))) 253 | 254 | 255 | (defn request-parameter "Get the value of a single valued request parameter." 256 | [^String name] 257 | (get-request-param *request* name)) 258 | 259 | 260 | (defn 261 | multi-request-parameter "Get the values of a multi valued request parameter as a sequence." 262 | [^String name] 263 | (get-request-param-values *request* name)) 264 | 265 | 266 | ;; Return mapping {"param name" [values], ...} 267 | ;; of request parameters 268 | (defn request-parameters "Return a mapping of parameter names to sequences of values." 269 | ([] (request-parameters *request*)) 270 | ([request] 271 | (get-request-params request))) 272 | 273 | 274 | 275 | (defn generate-request-binding [sym accessor] 276 | (cond 277 | (instance? String accessor) 278 | `[~sym (request-parameter ~accessor)] 279 | 280 | (instance? Number accessor) 281 | `[~sym (nth (*matched-handler* :groups) ~accessor)] 282 | 283 | :default 284 | (let [multi (:multiple accessor) 285 | name (:name accessor) 286 | group (:group accessor) 287 | validator (or (:validator accessor) 'identity)] 288 | `[~sym ~(if group 289 | `(~validator (nth (*matched-handler* :groups) ~group)) 290 | (if multi 291 | `(map ~validator (multi-request-parameter ~name)) 292 | `(~validator (request-parameter ~name))))]))) 293 | 294 | ;; Bind request parameters (GET/POST) to variables 295 | ;; A binding is a symbol and an access definition. 296 | ;; The access definition can be a string or a map containing 297 | ;; options. If the definition is a string the named parameter 298 | ;; is just returned as a string. 299 | ;; For option map access definitions, the following option 300 | ;; keys can be used: :name (the request param name, required), 301 | ;; :multiple (if true the value is a seq of values, defaults to no) and 302 | ;; :validator (a form that returns the validated value of the parameter value) 303 | ;; 304 | (defmacro request-bind [bindings & body] 305 | `(let [~@(loop [forms nil 306 | splits (split-at 2 bindings)] 307 | (let [binding (first splits)] 308 | (if (empty? binding) 309 | forms 310 | (recur 311 | (concat forms (apply generate-request-binding binding)) 312 | (split-at 2 (second splits))))))] 313 | ~@body)) 314 | 315 | 316 | ;; Fetch the value of a session attribute 317 | ;; if initial-value is specified and the given 318 | ;; attribute does not exist in the session, 319 | ;; the initial-value stored in the session and 320 | ;; returned. If initial-value is an IFn then 321 | ;; it is invoked to produce the value to store. 322 | (defn session-get "Get a stored value from the client session by key. Initial value (which may be a function that generates a value) will be used if specified and no value is stored in the session." 323 | ([attribute] (get-request-session-attribute *request* attribute)) 324 | ([attribute initial-value] 325 | (let [val (get-request-session-attribute *request* attribute)] 326 | (if (nil? val) 327 | (let [new-value (if (fn? initial-value) 328 | (initial-value) 329 | initial-value)] 330 | (set-request-session-attribute *request* attribute new-value) 331 | new-value) 332 | val)))) 333 | 334 | (defn session-set "Store a value by key in the client session." 335 | [name value] 336 | (set-request-session-attribute *request* name value)) 337 | 338 | (defn session-remove "Remove a value by key in the client session." 339 | [name] 340 | (remove-request-session-attribute *request* name)) 341 | 342 | (defn send-error 343 | ([code message] (send-error *response* code message)) 344 | ([response code message] 345 | (send-response-error response code message))) 346 | 347 | 348 | ;; The main dispatch function. This is called from WebjureServlet 349 | (defn dispatch [^String method 350 | request 351 | response] 352 | (binding [*request* request 353 | *response* response] 354 | (binding [*matched-handler* (find-handler (request-path *request*))] 355 | (if (= nil *matched-handler*) 356 | ;; No handler found, give a 404 357 | ;; PENDING: Add 404-handler support (a special dispatch url, like :default) 358 | (send-error 404 (str "No matching handler found for path: " (request-path request))) 359 | 360 | ;; Run the handler 361 | ((*matched-handler* :handler)))))) 362 | 363 | 364 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 365 | ;; Useful code for webjure apps 366 | 367 | (defn send-output "Send string output to client with given content-type." 368 | ([content-type content] (send-output *response* content-type content)) 369 | ([response ^String content-type ^String content] 370 | (set-response-content-type response content-type) 371 | (.append (get-response-writer response) content))) 372 | 373 | (defn slurp-post-data "Read POST data and return it as a string." 374 | ([] (slurp-post-data *request*)) 375 | ([request] 376 | (let [sb (new StringBuilder)] 377 | (with-open [in (new java.io.BufferedReader 378 | (new java.io.InputStreamReader 379 | (get-request-input-stream request)))] 380 | (loop [ch (.read in)] 381 | (if (< ch 0) 382 | (.toString sb) 383 | (do 384 | (.append sb (char ch)) 385 | (recur (.read in))))))))) 386 | 387 | 388 | 389 | (defn format-date "Format date using a SimpleDateFormat pattern." 390 | ([^String fmt ^java.util.Date date] (.format (java.text.SimpleDateFormat. fmt) date)) 391 | ([^String fmt] (.format (java.text.SimpleDateFormat. fmt) (java.util.Date.)))) 392 | 393 | 394 | ;; Define a handler function 395 | ;; options is a map of automagic behaviour. 396 | ;; currently supported is {:output } ( can be :html) 397 | ;; that automatically sends the return value as a response 398 | (defmacro defh [url request-bindings options & body] 399 | `(publish (fn [] 400 | (request-bind ~request-bindings 401 | ~@(cond 402 | ;; Output anything that is printed 403 | (= :print (options :output)) 404 | `((set-response-content-type *response* (or ~(options :content-type) "text/html")) 405 | (binding [*out* (response-writer)] 406 | ~@body)) 407 | 408 | ;; Output CSV 409 | (= :csv (options :output)) 410 | `((set-response-content-type *response* (or ~(options :content-type) "text/csv")) 411 | (webjure.csv/csv-format (do ~@body))) 412 | 413 | 414 | ;; Output text/plain 415 | (= :text (options :output)) 416 | `((send-output "text/plain" (do ~@body))) 417 | 418 | ;; Output HTML with optional doctype declaration 419 | (= :html (options :output)) 420 | `((let [out# (response-writer) 421 | doctype# ~(:doctype options)] 422 | (set-response-content-type *response* "text/html") 423 | (if doctype# 424 | (append out# (str doctype# "\n"))) 425 | (webjure.html/html-format out# (do ~@body)))) 426 | 427 | ;; Output JSON 428 | (= :json (options :output)) 429 | `((set-response-content-type *response* "application/json") 430 | (webjure.json/serialize (response-writer) 431 | (do ~@body))) 432 | 433 | :default body))) 434 | ~url)) 435 | 436 | 437 | (defmacro define-static-resource [file content-type url-path] 438 | (let [content (slurp file)] 439 | `(defh ~url-path [] {} 440 | (send-output ~content-type ~content)))) 441 | --------------------------------------------------------------------------------