diff options
author | Stuart Sierra <mail@stuartsierra.com> | 2009-08-17 13:29:11 -0400 |
---|---|---|
committer | Stuart Sierra <mail@stuartsierra.com> | 2009-08-17 13:29:11 -0400 |
commit | 74f64cdea1debc7e2442f9337f28a25a769792d2 (patch) | |
tree | ad677b2299da0387f7f05be4bddf956d683588b8 /src/clojure/contrib | |
parent | 44e4c23000a7cdee5395006dadc2eb1b58bc9b9d (diff) |
http/agent.clj: streamlined interface, refs #15
This brings http.agent close to completion.
Diffstat (limited to 'src/clojure/contrib')
-rw-r--r-- | src/clojure/contrib/http/agent.clj | 243 |
1 files changed, 153 insertions, 90 deletions
diff --git a/src/clojure/contrib/http/agent.clj b/src/clojure/contrib/http/agent.clj index 698ea4a8..f69778f5 100644 --- a/src/clojure/contrib/http/agent.clj +++ b/src/clojure/contrib/http/agent.clj @@ -16,28 +16,39 @@ clojure.contrib.http.agent (:require [clojure.contrib.http.connection :as c] [clojure.contrib.duck-streams :as duck]) - (:import (java.io ByteArrayOutputStream))) + (:import (java.io ByteArrayOutputStream ByteArrayInputStream))) + + +;;; PRIVATE + +(declare result stream) (defn- setup-http-connection + "Sets the instance method, redirect behavior, and request headers of + the HttpURLConnection." [conn options] (.setRequestMethod conn (:method options)) (.setInstanceFollowRedirects conn (:follow-redirects options)) (doseq [[name value] (:headers options)] (.setRequestProperty conn name value))) -(defn- connection-success? [conn] - ;; Is the response in the 2xx range? - (= 2 (unchecked-divide (.getResponseCode conn) 100))) - -(defn- start-request [state options] - (prn "start-request") +(defn- start-request + "Agent action that starts sending the HTTP request." + [state options] (let [conn (::connection state)] (setup-http-connection conn options) (c/start-http-connection conn (:body options)) (assoc state ::state ::started))) -(defn- open-response [state options] - (prn "open-response") +(defn- connection-success? [conn] + "Returns true if the HttpURLConnection response code is in the 2xx + range." + (= 2 (unchecked-divide (.getResponseCode conn) 100))) + +(defn- open-response + "Agent action that opens the response body stream on the HTTP + request; this will block until the response stream is available." + [state options] (let [conn (::connection state)] (assoc state ::response-stream (if (connection-success? conn) @@ -45,39 +56,52 @@ (.getErrorStream conn)) ::state ::receiving))) -(defn- handle-response [state success-handler failure-handler options] - (prn "handle-response") +(defn- handle-response + "Agent action that calls the provided handler function, with no + arguments, and sets the ::result key of the agent to the handler's + return value." + [state handler options] (let [conn (::connection state)] (assoc state - ::result (if (connection-success? conn) - (success-handler) - (failure-handler)) + ::result (handler) ::state ::finished))) -(defn- disconnect [state options] - (prn "disconnect") +(defn- disconnect + "Agent action that closes the response body stream and disconnects + the HttpURLConnection." + [state options] (.close (::response-stream state)) (.disconnect (::connection state)) (assoc state ::response-stream nil ::state ::disconnected)) -(defn response-body-stream - "Returns an InputStream of the HTTP response body." - [http-agnt] - (let [a @http-agnt] - (if (= (::state a) ::receiving) - (::response-stream a) - (throw (Exception. "response-body-stream may only be called from a callback function passed to http-agent"))))) +(defn- status-in-range? + "Returns true if the response status of the HTTP agent begins with + digit, an Integer." + [digit http-agnt] + (= digit (unchecked-divide (.getResponseCode (::connection @http-agnt)) + 100))) + +(defn- get-byte-buffer [http-agnt] + (let [buffer (result http-agnt)] + (if (instance? ByteArrayOutputStream buffer) + buffer + (throw (Exception. "Handler result was not a ByteArrayOutputStream"))))) + (defn buffer-bytes "The default HTTP agent result handler; it collects the response - body in a java.io.ByteArrayOutputStream." + body in a java.io.ByteArrayOutputStream, which can later be + retrieved with the 'stream', 'string', and 'bytes' functions." [http-agnt] (let [output (ByteArrayOutputStream.)] - (duck/copy (response-body-stream http-agnt) output) + (duck/copy (stream http-agnt) output) output)) + +;;; CONSTRUCTOR + (def *http-agent-defaults* {:method "GET" :headers {} @@ -85,8 +109,7 @@ :connect-timeout 0 :read-timeout 0 :follow-redirects true - :on-success buffer-bytes - :on-failure buffer-bytes}) + :handler buffer-bytes}) (defn http-agent "Creates (and immediately returns) an Agent representing an HTTP @@ -123,55 +146,62 @@ If true, HTTP 3xx redirects will be followed automatically. Default is true. - :on-success f - - Function to be called when the request succeeds with a 2xx response - code. The function will be called with the HTTP agent as its - argument, and can use the response-body-stream function to read the - response body. The return value of this function will be stored in - the state of the agent and can be retrieved with the 'result' - function. Any exceptions thrown by this function will be added to - the agent's error queue (see agent-errors). The default function - collects the response stream into a byte array. + :handler f - :on-failure f + Function to be called when the HTTP response body is ready. If you + do NOT provide a handler function, the default is to buffer the + entire response body in memory. - Like :on-success but this function will be called when the request - fails with a 4xx or 5xx response code. + The handler function will be called with the HTTP agent as its + argument, and can use the 'stream' function to read the response + body. The return value of this function will be stored in the state + of the agent and can be retrieved with the 'result' function. Any + exceptions thrown by this function will be added to the agent's + error queue (see agent-errors). The default function collects the + response stream into a byte array. " - ([url & options] + ([uri & options] (let [opts (merge *http-agent-defaults* (apply array-map options))] - (let [a (agent {::connection (c/http-connection url) + (let [a (agent {::connection (c/http-connection uri) ::state ::created - ::url url + ::uri uri ::options opts})] (send-off a start-request opts) (send-off a open-response opts) - (send-off a handle-response - (partial (:on-success opts) a) - (partial (:on-failure opts) a) - opts) + (send-off a handle-response (partial (:handler opts) a) opts) (send-off a disconnect opts))))) + +;;; RESPONSE BODY ACCESSORS + (defn result - "Returns the value returned by the :on-success or :on-failure - handler function of the HTTP agent; nil if the handler function has - not yet finished. The default handler function returns a - ByteArrayOutputStream." + "Returns the value returned by the :handler function of the HTTP + agent; blocks until the HTTP request is completed. The default + handler function returns a ByteArrayOutputStream." + [http-agnt] + (await http-agnt) + (::result @http-agnt)) + +(defn stream + "Returns an InputStream of the HTTP response body. When called by + the handler function passed to http-agent, this is the raw + HttpURLConnection stream. + + If the default handler function was used, this function returns a + ByteArrayInputStream on the buffered response body." [http-agnt] - (when (#{::disconnected ::finished} (::state @http-agnt)) - (::result @http-agnt))) + (let [a @http-agnt] + (if (= (::state a) ::receiving) + (::response-stream a) + (ByteArrayInputStream. (.toByteArray (result http-agnt)))))) -(defn response-body-bytes +(defn bytes "Returns a Java byte array of the content returned by the server; nil if the content is not yet available." [http-agnt] - (when-let [buffer (result http-agnt)] - (if (instance? ByteArrayOutputStream buffer) - (.toByteArray buffer) - (throw (Exception. "Result was not a ByteArrayOutputStream"))))) + (.toByteArray (get-byte-buffer http-agnt))) -(defn response-body-str +(defn string "Returns the HTTP response body as a string, using the given encoding. @@ -179,37 +209,71 @@ headers, or clojure.contrib.duck-streams/*default-encoding* if it is not specified." ([http-agnt] - (response-body-str http-agnt - (or (.getContentEncoding (::connection @http-agnt)) + (string http-agnt (or (.getContentEncoding (::connection @http-agnt)) duck/*default-encoding*))) ([http-agnt encoding] - (when-let [buffer (result http-agnt)] - (if (instance? ByteArrayOutputStream buffer) - (.toString buffer encoding) - (throw (Exception. "Result was not a ByteArrayOutputStream")))))) - -(defn response-status - "Returns the Integer response status code (e.g. 200, 404) for this request." - [a] - (when (= (::state @a) ::completed) - (.getResponseCode (::connection @a)))) - -(defn response-message - "Returns the HTTP response message (e.g. 'Not Found'), for this request." - [a] - (when (= (::state @a) ::completed) - (.getResponseMessage (::connection @a)))) - -(defn response-headers + (.toString (get-byte-buffer http-agnt) encoding))) + + +;;; REQUEST ACCESSORS + +(defn request-uri + "Returns the URI/URL requested by this HTTP agent, as a String." + [http-agnt] + (::uri @http-agnt)) + +(defn request-headers + "Returns the request headers specified for this HTTP agent." + [http-agnt] + (:headers (::options @http-agnt))) + +(defn method + "Returns the HTTP method name used by this HTTP agent, as a String." + [http-agnt] + (:method (::options @http-agnt))) + +(defn request-body + "Returns the HTTP request body given to this HTTP agent. + + Note: if the request body was an InputStream or a Reader, it will no + longer be usable." + [http-agnt] + (:body (::options @http-agnt))) + + +;;; RESPONSE ACCESSORS + +(defn done? + "Returns true if the HTTP request/response has completed." + [http-agnt] + (if (#{::finished ::disconnected} (::state @http-agnt)) + true false)) + +(defn status + "Returns the HTTP response status code (e.g. 200, 404) for this + request, as an Integer, or nil if the status has not yet been + received." + [http-agnt] + (when (done? http-agnt) + (.getResponseCode (::connection @http-agnt)))) + +(defn message + "Returns the HTTP response message (e.g. 'Not Found'), for this + request, or nil if the response has not yet been received." + [http-agnt] + (when (done? http-agnt) + (.getResponseMessage (::connection http-agnt)))) + +(defn headers "Returns a String=>String map of HTTP response headers. Header names are converted to all lower-case. If a header appears more than once, only the last value is returned." - [a] + [http-agnt] (reduce (fn [m [#^String k v]] - (assoc m (when k (.toLowerCase k)) (last v))) - {} (.getHeaderFields (::connection @a)))) + (assoc m (when k (keyword (.toLowerCase k))) (last v))) + {} (.getHeaderFields (::connection @http-agnt)))) -(defn response-headers-seq +(defn headers-seq "Returns the HTTP response headers in order as a sequence of [String,String] pairs. The first 'header' name may be null for the HTTP status line." @@ -222,14 +286,13 @@ (thisfn (inc i)))))] (lazy-seq (f 0)))) -(defn- response-in-range? [digit http-agnt] - (= digit (unchecked-divide (.getResponseCode (::connection @http-agnt)) - 100))) + +;;; RESPONSE STATUS CODE ACCESSORS (defn success? "Returns true if the HTTP response code was in the 200-299 range." [http-agnt] - (response-in-range? 2 http-agnt)) + (status-in-range? 2 http-agnt)) (defn redirect? "Returns true if the HTTP response code was in the 300-399 range. @@ -238,17 +301,17 @@ redirects will be followed automatically and a the agent will never return a 3xx response code." [http-agnt] - (response-in-range? 3 http-agnt)) + (status-in-range? 3 http-agnt)) (defn client-error? "Returns true if the HTTP response code was in the 400-499 range." [http-agnt] - (response-in-range? 4 http-agnt)) + (status-in-range? 4 http-agnt)) (defn server-error? "Returns true if the HTTP response code was in the 500-599 range." [http-agnt] - (response-in-range? 5 http-agnt)) + (status-in-range? 5 http-agnt)) (defn error? "Returns true if the HTTP response code was in the 400-499 range OR |