summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/site.clj163
-rw-r--r--src/tags.clj241
-rwxr-xr-xsrc/utils.clj19
3 files changed, 347 insertions, 76 deletions
diff --git a/src/site.clj b/src/site.clj
index 713dc9d..bf745c9 100644
--- a/src/site.clj
+++ b/src/site.clj
@@ -15,6 +15,7 @@
clojure.contrib.json.write
clojure.contrib.sql
clojure.contrib.str-utils
+ clojure.contrib.def
compojure
email
utils
@@ -202,6 +203,7 @@
(assoc m "topic" topic)
m)))
+
(defn count-messages-by-room [room-id image-only]
(let [query (str "SELECT COUNT(*)
FROM messages m, users u
@@ -261,6 +263,46 @@
:topic (ref nil)
})
+;; User-id/nick cache
+;; I keep needing to grab user-id from a nick or nick from a user-id so I thought I'd cache them
+
+(def user-id-nick-cache (ref {}))
+(def *user-id-nick-cache-size* 500)
+
+;; this is really ugly, need to make this nicer or get rid of it
+(defnk user-id-from-nick-or-nick-from-user-id [:nick false :user-id false]
+ (let [cache-key (or nick (str "~" user-id))
+ found (@user-id-nick-cache cache-key)]
+ (if found
+ found
+ (let [query (if nick
+ (str "SELECT user_id FROM users WHERE lower(nick) = ?")
+ (str "SELECT nick FROM users WHERE user_id = ?"))
+ res (first (do-select [query (or nick user-id)]))]
+ (if (nil? res)
+ nil
+ (let [found (if nick
+ (res :user_id)
+ (res :nick))
+ cache-key2 (if nick
+ (str "~" found)
+ (lower-case found))]
+ (dosync
+ (if (> (count @user-id-nick-cache) *user-id-nick-cache-size*) (ref-set user-id-nick-cache {}))
+ (alter user-id-nick-cache assoc cache-key found cache-key2 (or nick user-id))
+ )
+ found))))))
+
+(defn user-id-from-nick [nick] (user-id-from-nick-or-nick-from-user-id :nick (lower-case nick)))
+(defn nick-from-user-id [user-id] (user-id-from-nick-or-nick-from-user-id :user-id user-id))
+
+;; Favorites cache
+
+(def favorites-cache (ref []))
+(def *favorites-cache-size* 50)
+
+
+
;; Login code
(defn is-vip? [session]
@@ -385,7 +427,7 @@
has-avatar (non-empty-string? (user-info :avatar))
offset (maybe-parse-int offset 0)
dump-offset (* offset *dumps-per-page*)
- raw-dumps (logger tags/fetch-dumps-by-nick profile-nick true (+ 1 *dumps-per-page*) dump-offset)
+ raw-dumps (logger tags/fetch-dumps-by-nick :nick profile-nick :amount (+ 1 *dumps-per-page*) :offset dump-offset)
dumps (map tags/add-favorited-flag (take *dumps-per-page* raw-dumps) (repeat session))
dumps (map tags/remove-tags-for-output dumps)
dumps (logger doall (map process-message-for-output dumps))]
@@ -580,7 +622,7 @@
nick (session :nick)
st (fetch-template template session)
; TODO: remove db query
- message-list (reverse (tags/fetch-dumps-by-room (room :room_id) false))
+ message-list (reverse (tags/fetch-dumps-by-room :room-id (room :room_id) :image-only false))
message-list (map tags/add-favorited-flag message-list (repeat session))
message-list (to-array (map process-message-for-output message-list))]
(if nick
@@ -698,7 +740,10 @@
dump-offset (* offset *dumps-per-page*)
image-only (and (not (room :admin_only))
(not= (params :show) "all"))
- raw-dumps (logger tags/fetch-dumps-by-room (room :room_id) image-only (+ 1 *dumps-per-page*) dump-offset)
+ raw-dumps (logger tags/fetch-dumps-by-room :room-id (room :room_id)
+ :image-only image-only
+ :amount (+ 1 *dumps-per-page*)
+ :offset dump-offset)
dumps (map tags/add-favorited-flag (take *dumps-per-page* raw-dumps) (repeat session))
;; json-tags (for [dump dumps :when (not (empty? (dump :tags)))]
;; (json-str {"id" (dump :message_id) "tags" (dump :tags) }))
@@ -720,6 +765,103 @@
(log session (@rooms room-key) offset params)
(resp-error "UNKNOWN_ROOM")))
+;; Tags
+
+(defn undecoded-url-piece [url position]
+ "Get nth thing out of a url path.
+ For example, (undecoded-url-piece 'http://example.com/a/b/c?foo' 2) will return 'c'"
+ (let [path-without-domain (nth (re-find #"//[^/]+/(.+)" url) 1)]
+ (nth (re-split #"/|\?" path-without-domain) position)))
+
+(defn add-tag [user-id message-id tag]
+ (try
+ (do-insert "tags" ["user_id" "message_id" "tag"] [user-id (maybe-parse-int message-id) (normalize-tag-for-db tag)])
+ true
+ ; catch error when inserting duplicate tags
+ (catch Exception e false)))
+
+
+; to do: don't let people set tags on messages they can't access
+(defn validated-add-tag [session params]
+ (if (session :nick)
+ (if (add-tag (session :user_id) (params :message_id) (params :tag))
+ (resp-success "OK")
+ (resp-error "TAG_EXISTS_ALREADY_OR_SOMETHING_ELSE_IS_FUCKED"))
+ (resp-error "NO_USER")))
+
+(defn remove-tag [user-id message-id tag]
+ (let [query "user_id = ? AND message_id = ? AND lower(tag) = ?"]
+ (do-delete "tags" [query user-id (maybe-parse-int message-id) (normalize-tag-for-db (.toLowerCase tag))])
+ (resp-success "OK")))
+
+(defn validated-remove-tag [session params]
+ (if (session :nick)
+ (remove-tag (session :user_id) (params :message_id) (params :tag))
+ (resp-error "NO_USER")))
+
+(defnk tagged-dumps-template [session params tags url page-title :message-user-id false
+ :tag-user-id false
+ :logger (make-time-logger)]
+ (let [st (fetch-template "tagged_dumps" session)
+ offset (maybe-parse-int (params :offset) 0)
+ dump-offset (* offset *dumps-per-page*)
+ raw-dumps (logger tags/fetch-dumps-by-tag :tags tags
+ :image-only false
+ :amount (+ 1 *dumps-per-page*)
+ :offset dump-offset
+ :message-user-id message-user-id
+ :tag-user-id tag-user-id)
+ dumps (map tags/add-favorited-flag (take *dumps-per-page* raw-dumps) (repeat session))
+ dumps (map tags/remove-tags-for-output dumps)
+ dumps (logger doall (map process-message-for-output dumps))]
+ (if (> (count raw-dumps) *dumps-per-page*)
+ (.setAttribute st "next" (inc offset)))
+ (if (not= offset 0)
+ (.setAttribute st "prev" (max (dec offset) 0)))
+ (.setAttribute st "dumps" dumps)
+ (.setAttribute st "page_title" page-title)
+ (.setAttribute st "page_url" url)
+ (.setAttribute st "debug_log_items" (logger))
+ (.toString st)))
+
+;; gotta parse tag intersections myself because +'s get decoded into spaces
+;; there's probably a less hacky way to do this
+(defn tagged-dumps-by-nick [session params url]
+ (let [nick (params :nick)
+ tags (map url-decode (re-split #"\+" (undecoded-url-piece url 3)))
+ user-id (user-id-from-nick nick)
+ url (str "u/" nick "/tag/" (str-join "+" (map url-encode tags)))
+ page-title (str "dumps " nick " tagged as '" (escape-html (str-join "' and '" tags)) "'")]
+ (tagged-dumps-template session params tags url page-title :tag-user-id user-id)))
+
+(defn tagged-dumps [session params url]
+ (let [tags (map url-decode (re-split #"\+" (undecoded-url-piece url 1)))
+ url (str "tag/" (str-join "+" (map url-encode tags)))
+ page-title (str "dumps tagged as '" (escape-html (str-join "' and '" tags)) "'")]
+ (tagged-dumps-template session params tags url page-title)))
+
+(defn favorites [session params]
+ (let [nick (params :nick)
+ user-id (user-id-from-nick nick)
+ url (str "u/" nick "/favorites")
+ page-title (str nick "'s favorites")]
+ (tagged-dumps-template session params "favorite" url page-title :tag-user-id user-id)))
+
+(defn json-favorites [session params]
+ (let [nick (params :nick)
+ user-id (user-id-from-nick nick)
+ raw-favs (tags/fetch-dumps-by-tag :tags "favorite"
+ :image-only false
+ :amount 50
+ :offset 0
+ :tag-user-id user-id
+ :with-tags false)
+ favs (reduce (fn [m fav] (assoc m (str (fav :message_id)) (fav :content))) {} raw-favs)]
+ (str "RawFavs=" (json-str favs))))
+
+
+
+
;; Account resets
(defn reset-page [session]
@@ -884,13 +1026,14 @@
(GET "/favicon.ico" (serve-static "static" "favicon.ico"))
(GET "/u/:nick" (profile session (params :nick) "0"))
(GET "/u/:nick/" (profile session (params :nick) "0"))
- (GET "/u/:nick/tag/:tag" ("nuthin yet"))
- (GET "/u/:nick/tag/:tag/:offset" ("nuthin yet"))
- (GET "/u/:nick/favorites" ("nuthin yet"))
- (GET "/u/:nick/favorites/:offset" ("nuthing yet"))
+ (GET "/u/:nick/tag/:tag" (tagged-dumps-by-nick session params (request-url request)))
+ (GET "/u/:nick/tag/:tag/:offset" (tagged-dumps-by-nick session params (request-url request)))
+ (GET "/u/:nick/favorites" (favorites session params))
+ (GET "/u/:nick/favorites/:offset" (favorites session params))
+ (GET "/json/:nick/favorites" (json-favorites session params))
(GET "/u/:nick/:offset" (profile session
(params :nick)
- (params :offset)))
+ (params :offset))) ; have to put this route after favs
(GET "/p/:nick/:postid" (single-message session (params :nick) (params :postid)))
(GET "/login" (login session params cookies))
(GET "/logout" (logout session))
@@ -900,8 +1043,8 @@
(GET "/chat/:t" (no-cache (validated-chat session "dumpfm" (-> request :route-params :t))))
(GET "/browser" (browser session))
(GET "/refresh" (validated-refresh session params))
- (GET "/tag/:tag" ("nuthin yet"))
- (GET "/tag/:tag/:offset" ("nuthin yet"))
+ (GET "/tag/:tag" (tagged-dumps session params (request-url request)))
+ (GET "/tag/:tag/:offset" (tagged-dumps session params (request-url request)))
(POST "/cmd/tag/add" (validated-add-tag session params))
(POST "/cmd/tag/rm" (validated-remove-tag session params))
(GET "/log" (validated-log session "dumpfm" "0" params))
diff --git a/src/tags.clj b/src/tags.clj
index ba32635..77933d6 100644
--- a/src/tags.clj
+++ b/src/tags.clj
@@ -1,7 +1,9 @@
(ns tags
(:import java.lang.System)
(:use clojure.contrib.sql
+ clojure.contrib.def
clojure.contrib.json.write
+ clojure.contrib.str-utils
compojure
utils))
@@ -10,38 +12,10 @@
; (.replace tag " " "-"))
; todo: remove unicode escape sequences and line breaks and stuff?
-
; maybe show all dashes as spaces on output?
(defn normalize-tag-for-out [tag] (str tag))
-(defn add-tag [user-id message-id tag]
- (let [query "INSERT INTO tags2(user_id, message_id, tag) VALUES(?, ?, ?)"]
- (try
- (do-select [query user-id (maybe-parse-int message-id) (normalize-tag-for-db tag)])
- ; catch error when inserting duplicate tags
- (catch org.postgresql.util.PSQLException e
- resp-error "TAG_EXISTS_ALREADY_OR_SOMETHING_ELSE_IS_FUCKED")
- )))
-
-; to do: don't let people set tags on messages they can't access
-(defn validated-add-tag [session params]
- (if (session :nick)
- (add-tag (session :user_id) (params :message_id) (params :tag))
- (resp-error "NO_USER")))
-
-(defn remove-tag [user-id message-id tag]
- (let [query "DELETE FROM tags2 WHERE user_id = ? AND message_id = ? AND lower(tag) = ?"]
- (do-select [query user-id (maybe-parse-int message-id) (normalize-tag-for-db (.toLowerCase tag))])))
-
-(defn validated-remove-tag [session params]
- (if (session :nick)
- (remove-tag (session :user_id) (params :message_id) (params :tag))
- (resp-error "NO_USER")))
-
-
-(defn fetch-messages-by-tag [])
-
-; lol "Can't remove struct key"
+; lol "Can't remove struct key" (jdbc thing returns structified data)
(defn remove-tags-for-output [row]
(dissoc (into {} row) :tags))
@@ -51,15 +25,15 @@
; turn "a 1 b 2" to ({:nick a :tag 1}, {:nick b :tag 2})
; this seems to be a waste of cpu and memory, but i'm trying to make these easily accessible from templates
; also: i can't figure out how to access array indexes directly in StringTemplate templates
-(defn parse-tags-from-row [row]
- (assoc row :tags
- (for [t (partition 2 (.split (row :tags) " "))]
- {"nick" (first t) "tag" (last t)})))
+;; (defn parse-tags-from-row [row]
+;; (assoc row :tags
+;; (for [t (partition 2 (.split (row :tags) " "))]
+;; {"nick" (first t) "tag" (last t)})))
(defn parse-tags-from-string [string]
(partition 2 (.split string " ")))
-(defn build-tag-map-by-tag [tags nick-tag]
+(defn build-tag-map [tags nick-tag]
(let [nick (first nick-tag) tag (last nick-tag)]
(if (tags tag)
(assoc tags tag (conj (tags tag) nick))
@@ -67,9 +41,11 @@
; making something like {"tag1": ["nick a", "nick b"], "tag2": ["nick c", "nick d"]}
; why is building data structures in clojure so hard for me
-(defn parse-tags-from-row-as-map-by-tag [row]
- (assoc row :tags
- (reduce build-tag-map-by-tag {} (parse-tags-from-string (row :tags)))))
+(defn parse-tags-from-row-as-tag-map [row]
+ (if (row :tags)
+ (assoc row :tags
+ (reduce build-tag-map {} (parse-tags-from-string (row :tags))))
+ row))
(defn does-tag-exist?
([tags tag]
@@ -86,36 +62,173 @@
(defn explain-query [query] (str "EXPLAIN ANALYZE " query))
-; todo: offset is slow. probably need to include message_id in 'next page' url (tumblr & reddit do that)
-(defn fetch-messages-with-tags-by-room-query [image-only]
- (str "SELECT m.content, m.message_id, m.created_on, u.nick, u.avatar,
+;; OFFSET is very slow when it is large
+;; so, a subquery could be used when offset is large
+;; one other thing we could do is include message_id in 'next page' url (tumblr & reddit do that for example)
+;; but probably people don't page back too far anyway
+;; (not used yet)
+(defn fetch-dumps-by-room-query-faster [image-only] (str
+" SELECT
+ m.content, m.message_id, m.created_on,
+ u.nick, u.avatar,
+ array_to_string(ARRAY(SELECT nick || ' ' || tag
+ FROM tags, users
+ WHERE message_id = m.message_id AND tags.user_id = users.user_id), ' ') as tags
+ FROM (SELECT message_id
+ FROM messages
+ WHERE room_id = ? "
+ (if image-only "AND is_image = true " "")
+ " ORDER BY created_on DESC
+ LIMIT ? OFFSET ?) as sq, users u, messages m
+ WHERE sq.message_id = m.message_id
+ AND u.user_id = m.user_id"))
+
+(defn fetch-dumps-by-room-query [image-only] (str
+" SELECT
+ m.content, m.message_id, m.created_on,
+ u.nick, u.avatar,
array_to_string(ARRAY(SELECT nick || ' ' || tag
- FROM tags2, users
- WHERE message_id = m.message_id AND tags2.user_id = users.user_id), ' ') as tags
+ FROM tags, users
+ WHERE message_id = m.message_id AND tags.user_id = users.user_id), ' ') as tags
FROM users u, messages m
- WHERE room_id = ? AND m.user_id = u.user_id "
- (if image-only "AND m.is_image = true " "")
- "ORDER BY created_on DESC
+ WHERE room_id = ?
+ AND m.user_id = u.user_id "
+ (if image-only "AND m.is_image = true " "")
+" ORDER BY created_on DESC
LIMIT ? OFFSET ?"))
-; offset is slow... problem with this one though it doesn't take into account deleted messages
-(defn fetch-messages-with-tags-by-room-fast-paging-query [image-only]
- (str "SELECT m.content, m.message_id, m.created_on, u.nick, u.avatar,
+;; unoptimized
+;; the room join here is very bad on large offsets...
+(defn fetch-dumps-by-nick-query [image-only] (str
+" SELECT
+ m.content, m.message_id, m.created_on,
+ u.nick, u.avatar, r.key,
array_to_string(ARRAY(SELECT nick || ' ' || tag
- FROM tags2, users
- WHERE message_id = m.message_id AND tags2.user_id = users.user_id), ' ') as tags
- FROM users u, messages m
- WHERE room_id = ? AND m.user_id = u.user_id
- AND m.message_id BETWEEN ? AND ? "
- (if image-only "AND m.is_image = true " "")
- "ORDER BY created_on DESC
- LIMIT ?"))
+ FROM tags, users
+ WHERE message_id = m.message_id AND tags.user_id = users.user_id), ' ') as tags
+ FROM messages m, users u, rooms r
+ WHERE m.user_id = u.user_id AND u.nick = ?
+ AND r.room_id = m.room_id AND r.admin_only = false "
+ (if image-only "AND m.is_image = true " "")
+" ORDER BY created_on DESC
+ LIMIT ? OFFSET ?"))
+
+;; SPEED HACK
+;; explicit use of room ids, but fast
+;; (not used yet)
+(defnk fetch-dumps-by-user-id-query [:image-only true :with-tags true] (str
+" SELECT
+ m.content, m.message_id, m.created_on,
+ u.nick, u.avatar, r.key"
+ (if with-tags ",
+ array_to_string(ARRAY(SELECT nick || ' ' || tag
+ FROM tags, users
+ WHERE message_id = m.message_id AND tags.user_id = users.user_id), ' ') as tags " "")
+" FROM (SELECT message_id
+ FROM messages as m
+ WHERE m.user_id = ?
+ AND m.room_id IN (1) "
+ (if image-only "AND m.is_image = true " "")
+ " ORDER BY m.message_id DESC
+ LIMIT ? OFFSET ?
+ ) as sq, messages m, users u, rooms r
+ WHERE m.message_id = sq.message_id
+ AND m.user_id = u.user_id
+ AND r.room_id = m.room_id"))
+
+;; todo: only return distinct dumps? sorted by tag.created_on?
+(defnk fetch-dumps-by-tag-query [:image-only true :message-user-id false :tag-user-id false :with-tags true] (str
+" SELECT
+ m.content, m.message_id, m.created_on,
+ u.nick, u.avatar, r.key "
+ (if with-tags ",
+ array_to_string(ARRAY(SELECT nick || ' ' || tag
+ FROM tags, users
+ WHERE message_id = m.message_id AND tags.user_id = users.user_id), ' ') as tags " "")
+" FROM (SELECT
+ m.message_id
+ FROM messages as m, tags as t
+ WHERE lower(t.tag) = ? "
+ (if message-user-id "AND m.user_id = ? " "")
+ (if tag-user-id "AND t.user_id = ? " "")
+ (if image-only "AND m.is_image = true " "")
+ " AND m.message_id = t.message_id
+ ORDER BY m.message_id DESC
+ LIMIT ? OFFSET ?
+ ) as sq, messages m, users u, rooms r
+ WHERE m.message_id = sq.message_id
+ AND m.user_id = u.user_id
+ AND r.room_id = m.room_id"))
+
+;; tag intersections
+(defnk fetch-dumps-by-tags-query [:image-only true :num-tags 1 :message-user-id false :tag-user-id false :with-tags true] (str
+" SELECT
+ m.content, m.message_id, m.created_on,
+ u.nick, u.avatar, r.key "
+ (if with-tags ",
+ array_to_string(ARRAY(SELECT nick || ' ' || tag
+ FROM tags, users
+ WHERE message_id = m.message_id AND tags.user_id = users.user_id), ' ') as tags " "")
+" FROM (SELECT
+ m.message_id"
+ " FROM messages as m
+ INNER JOIN tags USING (message_id)
+ WHERE lower(tags.tag) IN (" (str-join ", " (take num-tags (repeat "?"))) ") "
+ (if message-user-id "AND m.user_id = ? " "")
+ (if tag-user-id "AND tags.user_id = ? " "")
+ (if image-only "AND m.is_image = true " "")
+ " GROUP BY m.message_id HAVING COUNT(*) = " (str num-tags) "
+ ORDER BY m.message_id DESC
+ LIMIT ? OFFSET ?
+ ) as sq, messages m, users u, rooms r
+ WHERE m.message_id = sq.message_id
+ AND m.user_id = u.user_id
+ AND r.room_id = m.room_id"))
+
+
+(defnk fetch-dumps-by-room [:room-id 1 :image-only true :amount *dumps-per-page* :offset 0]
+ (let [query (fetch-dumps-by-room-query image-only)]
+ (let [rows (do-select [query room-id amount offset])]
+ (map parse-tags-from-row-as-tag-map rows))))
+
+(defnk fetch-dumps-by-nick [:nick "anon" :image-only true :amount *dumps-per-page* :offset 0]
+ (let [query (fetch-dumps-by-nick-query image-only)]
+ (let [rows (do-select [query nick amount offset])]
+ (map parse-tags-from-row-as-tag-map rows))))
+
+(defnk fetch-dumps-by-user-id [user-id :image-only true :amount *dumps-per-page* :offset 0]
+ (let [query (fetch-dumps-by-user-id-query image-only)]
+ (let [rows (do-select [query user-id amount offset])]
+ (map parse-tags-from-row-as-tag-map rows))))
+
+(defnk fetch-dumps-by-tag [:tags "favorite"
+ :message-user-id false
+ :tag-user-id false
+ :image-only true
+ :amount *dumps-per-page*
+ :offset 0
+ :with-tags true]
+ (let [tags (if (string? tags) [tags] tags)
+ query (if (= 1 (count tags))
+ (fetch-dumps-by-tag-query :image-only image-only
+ :message-user-id message-user-id
+ :tag-user-id tag-user-id
+ :with-tags with-tags)
+ (fetch-dumps-by-tags-query :image-only image-only
+ :message-user-id message-user-id
+ :tag-user-id tag-user-id
+ :num-tags (count tags)
+ :with-tags with-tags))
+ query-vars [amount offset]
+ query-vars (if message-user-id (concat [message-user-id] query-vars ) query-vars)
+ query-vars (if tag-user-id (concat [tag-user-id] query-vars ) query-vars)
+ rows (do-select (vec (concat [query] tags query-vars)))]
+ (map parse-tags-from-row-as-tag-map rows)))
-(defn fetch-messages-with-tags-by-room
- ([] (fetch-messages-with-tags-by-room 1 true *dumps-per-page* 0))
- ([room-id] (fetch-messages-with-tags-by-room room-id true *dumps-per-page* 0))
- ([room-id image-only] (fetch-messages-with-tags-by-room room-id image-only *dumps-per-page* 0))
- ([room-id image-only amount offset]
- (let [query (fetch-messages-with-tags-by-room-query image-only)]
- (let [rows (do-select [query room-id amount offset])]
- (map parse-tags-from-row-as-map-by-tag rows)))))
+
+
+
+
+
+
+ \ No newline at end of file
diff --git a/src/utils.clj b/src/utils.clj
index 92fdad6..3f2f4e4 100755
--- a/src/utils.clj
+++ b/src/utils.clj
@@ -1,6 +1,7 @@
(ns utils
(:import java.text.SimpleDateFormat
- java.util.Date)
+ java.util.Date
+ java.net.URLDecoder)
(:use clojure.contrib.json.write
clojure.contrib.sql))
@@ -13,6 +14,8 @@
:user "postgres"
:password "root"}))
+;; moved this to here which doesn't seem right... maybe a 'settings.clj' or something?
+(def *dumps-per-page* 20)
;; Misc
@@ -70,6 +73,10 @@
(with-connection *db*
(delete-rows table query)))
+(defn do-insert [table cols values]
+ (with-connection *db*
+ (insert-values table cols values)))
+
;; Parsing
(defn maybe-parse-int
@@ -79,4 +86,12 @@
(catch NumberFormatException _ default))))
(defn maybe-parse-long [s f]
- (if s (Long/parseLong s) f)) \ No newline at end of file
+ (if s (Long/parseLong s) f))
+
+(defn url-decode [text]
+ (URLDecoder/decode text "UTF-8"))
+
+(defn #^String lower-case
+ "Converts string to all lower-case."
+ [#^String s]
+ (.toLowerCase s)) \ No newline at end of file