-
Notifications
You must be signed in to change notification settings - Fork 136
/
apache.clj
212 lines (193 loc) · 7.77 KB
/
apache.clj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
(ns ajax.apache
(:require [ajax.protocols :refer [map->Response
AjaxImpl AjaxRequest AjaxResponse]]
[clojure.string :as s])
(:import [clojure.lang IDeref IBlockingDeref IPending]
[org.apache.http HttpResponse Header]
[org.apache.http.entity ByteArrayEntity StringEntity
FileEntity InputStreamEntity]
[org.apache.http.client.methods HttpRequestBase
HttpEntityEnclosingRequestBase]
[org.apache.http.client.config RequestConfig CookieSpecs]
[org.apache.http.concurrent FutureCallback]
[org.apache.http.impl.nio.client HttpAsyncClients]
[java.lang Exception]
[java.util.concurrent Future]
[java.net URI SocketTimeoutException]
[java.io File InputStream Closeable]))
;;; Chunks of this code liberally ripped off dakrone/clj-http
;;; Although that uses the synchronous API
;;; Note that the only thing exposed by this rather complicated
;;; piece of code is `new-api` at the bottom.
(def array-of-bytes-type (Class/forName "[B"))
(defn- to-entity [b]
"This function means you can just hand cljs-ajax a byte
array, string, normal Java file or input stream and it
will automatically work with the Apache implementation."
(condp instance? b
array-of-bytes-type (ByteArrayEntity. b)
String (StringEntity. ^String b "UTF-8")
File (FileEntity. b)
InputStream (InputStreamEntity. b)
b))
(defn- to-uri [u]
(if (instance? URI u)
u
(URI. (s/replace u " " "%20"))))
;;; This is a nice demonstration of how protocols don't
;;; in fact solve the expression problem. Various apache
;;; methods return HttpResponse classes, but it is not
;;; guaranteed what concrete class is returned nor that
;;; it is stable between minor version numbers.
;;; So, you end up doing what you'd normally do in Java,
;;; write an adapter class.
;;; Takes an HttpResponse and exposes the interface needed
;;; by cljs-ajax interceptors (including response formats).
(defrecord HttpResponseWrapper [^HttpResponse response]
AjaxResponse
(-body [this]
(let [^HttpResponse response (:response this)]
(.getContent (.getEntity response))))
(-status [this]
(let [^HttpResponse response (:response this)]
(-> response .getStatusLine .getStatusCode)))
(-status-text [this]
(let [^HttpResponse response (:response this)]
(-> response .getStatusLine .getReasonPhrase)))
(-get-all-headers [this]
(let [^HttpResponse response (:response this)]
(reduce (fn [headers ^Header header]
(assoc headers (.getName header) (.getValue header)))
{}
(.getAllHeaders response))))
(-get-response-header [this header]
(let [^HttpResponse response (:response this)]
(.getValue (.getFirstHeader response header))))
(-was-aborted [this] false))
(defn- create-request
"Life's to short to use all of the apache types for
the different HTTP methods when you can just wrap a
string in the appropriate base class."
^HttpEntityEnclosingRequestBase [method]
(proxy [HttpEntityEnclosingRequestBase] []
(getMethod [] method)))
(defn- cancel [handler]
"This method ensures that the behaviour of the wrapped
Apache classes matches the behaviour the javascript version,
including the negative status number."
(handler
(map->Response {:status -1
:status-text "Cancelled"
:headers {}
:was-aborted true})))
(defn- fail [handler ^Exception ex]
"XMLHttpRequest reports a status of -1 for timeouts, so
we do the same."
(let [status (if (instance? SocketTimeoutException ex) -1 0)]
(handler
(map->Response {:status status
:status-text (.getMessage ex)
:headers {}
:exception ex
:was-aborted false}))))
(defn- create-handler [handler]
"Takes a cljs-ajax style handler method and converts it
to a FutureCallback suitable for use the Apache API."
(reify
FutureCallback
(cancelled [_]
(cancel handler))
(completed [_ response]
(handler (HttpResponseWrapper. response)))
(failed [_ ex]
(fail handler ex))))
(defmulti get-cookie-policy
"Method to retrieve the cookie policy that should be used for the request.
This is a multimethod that may be extended to return your own cookie policy.
Dispatches based on the `:cookie-policy` key in the request map."
(fn get-cookie-dispatch [request] (:cookie-policy request)))
(defmethod get-cookie-policy :none none-cookie-policy
[_] CookieSpecs/IGNORE_COOKIES)
(defmethod get-cookie-policy :default default-cookie-policy
[_] CookieSpecs/DEFAULT)
(defmethod get-cookie-policy :netscape netscape-cookie-policy
[_] CookieSpecs/NETSCAPE)
(defmethod get-cookie-policy :standard standard-cookie-policy
[_] CookieSpecs/STANDARD)
(defmethod get-cookie-policy :standard-strict standard-strict-cookie-policy
[_] CookieSpecs/STANDARD_STRICT)
(defn- create-request-config
^RequestConfig [{:keys [timeout socket-timeout cookie-policy] :as req}]
(let [builder (RequestConfig/custom)]
(if timeout
(.setConnectTimeout builder timeout))
(if-let [st (or socket-timeout timeout)]
(.setSocketTimeout builder st))
(if cookie-policy
(.setCookieSpec builder (get-cookie-policy req)))
(.build builder)))
(defn- to-clojure-future
"Converts a normal Java future to one similar to the one generated
by `clojure.core/future`. Operationally, this is used to wrap the
result of the Apache API into something that can be returned by
`ajax-request`. Note that there's no guarantee anyone will ever dereference
it (but they might). Also, since it's returned by `ajax-request`,
it needs to support `abort`."
[^Future f ^Closeable client]
;;; We wrap the original future and closeable in a second layer
;;; to guarantee that we don't leak memory. This deeply clever
;;; solution is by https://github.com/divs1210
(let [^Future f* (future
(try
(.get f)
(finally (.close client))))
cancel* (fn [interrupt?]
(try
(.cancel f interrupt?)
(.cancel f* interrupt?)
(finally (.close client))))]
(reify
IDeref
(deref [_] (deref f*))
IBlockingDeref
(deref [_ timeout-ms timeout-val]
(deref f* timeout-ms timeout-val))
IPending
(isRealized [_] (.isDone f*))
Future
(get [_] (.get f*))
(get [_ timeout unit]
(.get f* timeout unit))
(isCancelled [_] (.isCancelled f*))
(isDone [_] (.isDone f*))
(cancel [_ interrupt?]
(cancel* interrupt?))
AjaxRequest
(-abort [_]
(cancel* true)))))
(defrecord Connection []
AjaxImpl
(-js-ajax-request
[this {:keys [uri method body headers] :as opts} handler]
(try
(let [request (doto (create-request method)
(.setURI (to-uri uri))
(.setEntity (to-entity body)))
request-config (create-request-config opts)
builder (doto (HttpAsyncClients/custom)
(.setDefaultRequestConfig request-config))
client (doto (.build builder)
(.start))
h (create-handler handler)]
(doseq [x headers]
(let [[h v] x]
(.addHeader request h v)))
(to-clojure-future (.execute client request h) client))
(catch Exception ex (fail handler ex)))))
(defn new-api []
"This is the only thing exposed by the apache.clj file:
a factory function that returns a class that wraps the
Apache async API to the cljs-ajax API.
Note that it's completely stateless: all of the relevant
implementation objects are created each time."
(Connection.))