Skip to content
This repository has been archived by the owner on Jul 24, 2018. It is now read-only.

Commit

Permalink
use HttpURLConnection
Browse files Browse the repository at this point in the history
  • Loading branch information
hiredman committed Feb 6, 2012
1 parent f954c7e commit 3beca9c
Show file tree
Hide file tree
Showing 6 changed files with 333 additions and 494 deletions.
14 changes: 4 additions & 10 deletions project.clj
Original file line number Diff line number Diff line change
@@ -1,28 +1,22 @@
(defproject clj-http "0.3.2-SNAPSHOT"
(defproject clj-http-jdk "0.3.2-SNAPSHOT"
:description "A Clojure HTTP library wrapping the Apache HttpComponents client."
:url "https://github.com/dakrone/clj-http/"
:repositories {"sona" "http://oss.sonatype.org/content/repositories/snapshots"}
:warn-on-reflection false
:dependencies [[org.clojure/clojure "1.3.0"]
[org.apache.httpcomponents/httpclient "4.1.2"]
[org.apache.httpcomponents/httpmime "4.1.2"]
[slingshot "0.10.1"]
[commons-codec "1.5"]
[commons-io "2.1"]
[slingshot "0.10.1"]
[cheshire "2.1.0"]]
:multi-deps {"1.2.1" [[org.clojure/clojure "1.2.1"]
[org.apache.httpcomponents/httpclient "4.1.2"]
[org.apache.httpcomponents/httpmime "4.1.2"]
[slingshot "0.10.1"]
[commons-codec "1.5"]
[commons-io "2.1"]
[slingshot "0.10.1"]
[cheshire "2.1.0"]]
"1.4.0" [[org.clojure/clojure "1.4.0-beta1"]
[org.apache.httpcomponents/httpclient "4.1.2"]
[org.apache.httpcomponents/httpmime "4.1.2"]
[slingshot "0.10.1"]
[commons-codec "1.5"]
[commons-io "2.1"]
[slingshot "0.10.1"]
[cheshire "2.1.0"]]}
:dev-dependencies [[ring/ring-jetty-adapter "1.0.2"]
[ring/ring-devel "1.0.2"]
Expand Down
24 changes: 3 additions & 21 deletions src/clj_http/client.clj
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
(ns clj-http.client
"Batteries-included HTTP client."
(:use [clj-http.cookies :only (wrap-cookies)]
[slingshot.slingshot :only [throw+]])
(:use [slingshot.slingshot :only [throw+]])
(:require [clojure.string :as str]
[cheshire.core :as json]
[clj-http.core :as core]
[clj-http.util :as util])
(:import (java.io InputStream File)
(java.net URL UnknownHostException)
(org.apache.http.entity ByteArrayEntity InputStreamEntity
FileEntity StringEntity))
(java.net URL UnknownHostException))
(:refer-clojure :exclude (get)))

(defn update [m k f & args]
Expand Down Expand Up @@ -120,23 +117,9 @@
(if body
(cond
(string? body)
(client (-> req (assoc :body (StringEntity. body (or body-encoding
"UTF-8"))
(client (-> req (assoc :body (.getBytes body)
:character-encoding (or body-encoding
"UTF-8"))))
(instance? File body)
(client (-> req (assoc :body (FileEntity. body (or body-encoding
"UTF-8")))))
(instance? InputStream body)
(do
(when-not (and length (pos? length))
(throw
(Exception. ":length key is required for InputStream bodies")))
(client (-> req (assoc :body (InputStreamEntity. body length)))))

(instance? (Class/forName "[B") body)
(client (-> req (assoc :body (ByteArrayEntity. body))))

:else
(client req))
(client req))))
Expand Down Expand Up @@ -268,7 +251,6 @@
wrap-content-type
wrap-form-params
wrap-method
wrap-cookies
wrap-unknown-host))

(def #^{:doc
Expand Down
260 changes: 57 additions & 203 deletions src/clj_http/core.clj
Original file line number Diff line number Diff line change
@@ -1,161 +1,43 @@
(ns clj-http.core
"Core HTTP request/response implementation."
(:require [clojure.pprint])
(:import (java.io File FilterInputStream InputStream)
(java.net URI)
(org.apache.http HeaderIterator HttpRequest HttpEntity
HttpEntityEnclosingRequest
HttpResponse Header HttpHost)
(org.apache.http.util EntityUtils)
(org.apache.http.entity ByteArrayEntity StringEntity)
(org.apache.http.entity.mime MultipartEntity)
(org.apache.http.entity.mime.content ByteArrayBody
FileBody
InputStreamBody
StringBody)
(org.apache.http.client HttpClient)
(org.apache.http.client.methods HttpGet HttpHead HttpPut
HttpPost HttpDelete
HttpEntityEnclosingRequestBase)
(org.apache.http.client.params CookiePolicy ClientPNames)
(org.apache.http.conn ClientConnectionManager)
(org.apache.http.conn.params ConnRoutePNames)
(org.apache.http.conn.scheme PlainSocketFactory
SchemeRegistry Scheme)
(org.apache.http.conn.ssl SSLSocketFactory TrustStrategy)
(org.apache.http.impl.conn SingleClientConnManager)
(org.apache.http.impl.conn.tsccm ThreadSafeClientConnManager)
(org.apache.http.impl.client DefaultHttpClient)
(org.apache.http.util EntityUtils)))
(:require [clojure.java.io :as io])
(:import (java.io ByteArrayOutputStream IOException)
(java.net URI URL)))

(defn parse-headers
"Takes a HeaderIterator and returns a map of names to values.
"Takes a URLConnection and returns a map of names to values.
If a name appears more than once (like `set-cookie`) then the value
will be a vector containing the values in the order they appeared
in the headers."
[#^HeaderIterator headers]
(->> (iterator-seq headers)
(map (fn [#^Header h] [(.toLowerCase (.getName h)) (.getValue h)]))
(group-by first)
(map (fn [[name headers]]
(let [values (map second headers)]
[name (let [[value & tail] values]
(if tail values value))])))
(into {})))

(defn set-client-param [#^HttpClient client key val]
(when (not (nil? val))
(-> client
(.getParams)
(.setParameter key val))))

(defn proxy-delete-with-body [url]
(let [res (proxy [HttpEntityEnclosingRequestBase] []
(getMethod [] "DELETE"))]
(.setURI res (URI. url))
res))

(def ^SSLSocketFactory insecure-socket-factory
(doto (SSLSocketFactory. (reify TrustStrategy
(isTrusted [_ _ _] true)))
(.setHostnameVerifier SSLSocketFactory/ALLOW_ALL_HOSTNAME_VERIFIER)))

(def insecure-scheme-registry
(doto (SchemeRegistry.)
(.register (Scheme. "http" 80 (PlainSocketFactory/getSocketFactory)))
(.register (Scheme. "https" 443 insecure-socket-factory))))

(def regular-scheme-registry
(doto (SchemeRegistry.)
(.register (Scheme. "http" 80 (PlainSocketFactory/getSocketFactory)))
(.register (Scheme. "https" 443 (SSLSocketFactory/getSocketFactory)))))

(defn make-regular-conn-manager ^SingleClientConnManager [& [insecure?]]
(if insecure?
(SingleClientConnManager. insecure-scheme-registry)
(SingleClientConnManager.)))

(defn make-reusable-conn-manager
"Given an timeout and optional insecure? flag, create a
ThreadSafeClientConnManager with <timeout> seconds set as the timeout value."
;; need the fully qualified class name because this fn is later used in a
;; macro from a different ns
^org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager
[timeout & [insecure?]]
(let [sr (if insecure? insecure-scheme-registry regular-scheme-registry)]
(ThreadSafeClientConnManager.
sr timeout java.util.concurrent.TimeUnit/SECONDS)))

(def ^{:dynamic true} *connection-manager* nil)

(defn create-multipart-entity
"Takes a multipart map and creates a MultipartEntity with each key/val pair
added as a part, determining part type by the val type."
[multipart]
(let [mp-entity (MultipartEntity.)]
(doseq [[k v] multipart]
(let [klass (type v)
keytext (name k)
part (cond
(isa? klass File)
(FileBody. v keytext)

(isa? klass InputStream)
(InputStreamBody. v keytext)

(= klass (type (byte-array 0)))
(ByteArrayBody. v keytext)

(= klass String)
(StringBody. v))]
(.addPart mp-entity keytext part)))
mp-entity))

(defn- default-proxy-host-for
[scheme]
(System/getProperty (str scheme ".proxyHost")))

(defn- default-proxy-port-for
[scheme]
(Integer/parseInt (System/getProperty (str scheme ".proxyPort"))))

(defn add-client-params!
"Add various client params to the http-client object, if needed."
[http-client scheme socket-timeout conn-timeout server-name
proxy-host proxy-port]
(doto http-client
(set-client-param ClientPNames/COOKIE_POLICY
CookiePolicy/BROWSER_COMPATIBILITY)
(set-client-param ClientPNames/HANDLE_REDIRECTS false)
(set-client-param "http.socket.timeout"
(and socket-timeout (Integer. ^Long socket-timeout)))
(set-client-param "http.connection.timeout"
(and conn-timeout (Integer. ^Long conn-timeout))))
(when (nil? (#{"localhost" "127.0.0.1"} server-name))
(when-let [effective-proxy-host (or proxy-host
(default-proxy-host-for scheme))]
(let [effective-proxy-port (or proxy-port
(default-proxy-port-for scheme))]
(set-client-param http-client ConnRoutePNames/DEFAULT_PROXY
(HttpHost. effective-proxy-host
effective-proxy-port))))))
[conn]
(loop [i 1 headers {}]
(let [k (.getHeaderFieldKey conn i)
v (.getHeaderField conn i)]
(if k
(recur (inc i) (update-in headers [k] conj v))
(zipmap (for [k (keys headers)]
(.toLowerCase k))
(for [v (vals headers)]
(if (= 1 (count v))
(first v)
(vec v))))))))

(defn- coerce-body-entity
"Coerce the http-entity from an HttpResponse to either a byte-array, or a
stream that closes itself and the connection manager when closed."
[{:keys [as]} ^HttpEntity http-entity ^ClientConnectionManager conn-mgr]
(when http-entity
[{:keys [as]} conn]
(let [ins (try
(.getInputStream conn)
(catch Exception e
(.getErrorStream conn)))]
(if (= :stream as)
(proxy [FilterInputStream]
[(.getContent http-entity)]
(close []
(try
(proxy-super close)
(finally
(when (instance? SingleClientConnManager conn-mgr)
(.shutdown conn-mgr))))))
(EntityUtils/toByteArray http-entity))))
ins
(with-open [ins ins
baos (ByteArrayOutputStream.)]
(io/copy ins baos)
(.flush baos)
(.toByteArray baos)))))

(defn request
"Executes the HTTP request corresponding to the given Ring request map and
Expand All @@ -165,61 +47,33 @@
the clj-http uses ByteArrays for the bodies."
[{:keys [request-method scheme server-name server-port uri query-string
headers content-type character-encoding body socket-timeout
conn-timeout multipart debug insecure? save-request? proxy-host
proxy-port as] :as req}]
(let [conn-mgr (or *connection-manager* (make-regular-conn-manager insecure?))
http-client (DefaultHttpClient.
^org.apache.http.conn.ClientConnectionManager conn-mgr)
scheme (name scheme)]
(add-client-params! http-client scheme socket-timeout
conn-timeout server-name proxy-host proxy-port)
(let [http-url (str scheme "://" server-name
(when server-port (str ":" server-port))
uri
(when query-string (str "?" query-string)))
req (assoc req :http-url http-url)
#^HttpRequest
http-req (case request-method
:get (HttpGet. http-url)
:head (HttpHead. http-url)
:put (HttpPut. http-url)
:post (HttpPost. http-url)
:delete (proxy-delete-with-body http-url))]
(when (and content-type character-encoding)
(.addHeader http-req "Content-Type"
(str content-type "; charset=" character-encoding)))
(when (and content-type (not character-encoding))
(.addHeader http-req "Content-Type" content-type))
(when (instance? SingleClientConnManager conn-mgr)
(.addHeader http-req "Connection" "close"))
(doseq [[header-n header-v] headers]
(.addHeader http-req header-n header-v))
(if multipart
(.setEntity #^HttpEntityEnclosingRequest http-req
(create-multipart-entity multipart))
(when body
(if (instance? HttpEntity body)
(.setEntity #^HttpEntityEnclosingRequest http-req body)
(.setEntity #^HttpEntityEnclosingRequest http-req
(if (string? body)
(StringEntity. body "UTF-8")
(ByteArrayEntity. body))))))
(when debug
(println "Request:")
(clojure.pprint/pprint
(assoc req :body (format "... %s bytes ..." (count (:body req)))))
(println "HttpRequest:")
(clojure.pprint/pprint (bean http-req)))
(let [http-resp (.execute http-client http-req)
http-entity (.getEntity http-resp)
resp {:status (.getStatusCode (.getStatusLine http-resp))
:headers (parse-headers (.headerIterator http-resp))
:body (coerce-body-entity req http-entity conn-mgr)}]
(when (and (instance? SingleClientConnManager conn-mgr)
(not= :stream as))
(.shutdown ^ClientConnectionManager conn-mgr))
(if save-request?
(-> resp
(assoc :request req)
(dissoc :save-request?))
resp)))))
conn-timeout multipart debug insecure? save-request?] :as req}]
(let [http-url (str (name scheme) "://" server-name
(when server-port (str ":" server-port))
uri
(when query-string (str "?" query-string)))
conn (.openConnection ^URL (URL. http-url))]
(when (and content-type character-encoding)
(.setRequestProperty conn "Content-Type" (str content-type
"; charset="
character-encoding)))
(when (and content-type (not character-encoding))
(.setRequestProperty conn "Content-Type" content-type))
(doseq [[h v] headers]
(.setRequestProperty conn h v))
(.setRequestMethod conn (.toUpperCase (name request-method)))
(when body
(.setDoOutput conn true))
(when socket-timeout
(.setReadTimeout conn socket-timeout))
(.connect conn)
(when body
(with-open [out (.getOutputStream conn)]
(io/copy body out)))
(merge {:headers (parse-headers conn)
:status (.getResponseCode conn)
:body (when-not (= request-method :head)
(coerce-body-entity req conn))}
(when save-request?
{:request (assoc (dissoc req :save-request?)
:http-url http-url)}))))
8 changes: 4 additions & 4 deletions test/clj_http/test/client.clj
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@
;; roundtrip with scheme as a keyword
(let [resp (client/request (merge base-req {:uri "/get" :method :get}))]
(is (= 200 (:status resp)))
(is (= "close" (get-in resp [:headers "connection"])))
#_(is (= "close" (get-in resp [:headers "connection"])))
(is (= "get" (:body resp))))
;; roundtrip with scheme as a string
(let [resp (client/request (merge base-req {:uri "/get" :method :get
:scheme "http"}))]
(is (= 200 (:status resp)))
(is (= "close" (get-in resp [:headers "connection"])))
#_(is (= "close" (get-in resp [:headers "connection"])))
(is (= "get" (:body resp)))))

(defn is-passed [middleware req]
Expand Down Expand Up @@ -158,8 +158,8 @@
(let [i-client (client/wrap-input-coercion identity)
resp (i-client {:body "foo"})
resp2 (i-client {:body "foo2" :body-encoding "ASCII"})
data (slurp (.getContent (:body resp)))
data2 (slurp (.getContent (:body resp2)))]
data (slurp (:body resp))
data2 (slurp (:body resp2))]
(is (= "UTF-8" (:character-encoding resp)))
(is (= "foo" data))
(is (= "ASCII" (:character-encoding resp2)))
Expand Down
Loading

0 comments on commit 3beca9c

Please sign in to comment.