Skip to content

Commit 5d145e0

Browse files
committed
Refactored multipart/make-multipart-body into multimethod.
This change will allow simple extension of multipart content conversions into org.apache.http.entity.mime.content.ContentBody implementations.
1 parent 5aeed2a commit 5d145e0

File tree

4 files changed

+212
-92
lines changed

4 files changed

+212
-92
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,7 @@ aws.clj
3434
docs/*
3535
doc
3636
http.log
37+
38+
# Intellij Idea
39+
/*.iml
40+
/.idea

README.org

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ content encodings.
279279
{:name "file" :content (clojure.java.io/file "pic.jpg")}]})
280280

281281
;; Multipart :content values can be one of the following:
282-
;; String, InputStream, File, or a byte-array
282+
;; String, InputStream, File, a byte-array, or an instance of org.apache.http.entity.mime.content.ContentBody
283283
;; Some Multipart bodies can also support more keys (like :encoding
284284
;; and :mime-type), check src/clj-http/multipart.clj to see all flags
285285

src/clj_http/multipart.clj

Lines changed: 47 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,39 @@
11
(ns clj-http.multipart
22
"Namespace used for clj-http to create multipart entities and bodies."
33
(:import (java.io File InputStream)
4-
(java.nio.charset Charset)
54
(org.apache.http.entity ContentType)
65
(org.apache.http.entity.mime MultipartEntity)
7-
(org.apache.http.entity.mime.content ByteArrayBody
6+
(org.apache.http.entity.mime.content ContentBody
7+
ByteArrayBody
88
FileBody
99
InputStreamBody
10-
StringBody)))
10+
StringBody)
11+
(org.apache.http Consts)))
1112

1213
;; we don't need to make a fake byte-array every time, only once
1314
(def byte-array-type (type (byte-array 0)))
1415

15-
(defn make-file-body
16-
"Create a FileBody object from the given map, requiring at least :content"
16+
(defmulti
17+
make-multipart-body
18+
"Create a body object from the given map, dispatching on the type of its content.
19+
By default supported content body types are:
20+
- String
21+
- byte array (requires providing name)
22+
- InputStream (requires providing name)
23+
- File
24+
- org.apache.http.entity.mime.content.ContentBody (which is just returned)"
25+
(fn [multipart] (type (:content multipart))))
26+
27+
(defmethod make-multipart-body nil
28+
[multipart]
29+
(throw (Exception. "Multipart content cannot be nil")))
30+
31+
(defmethod make-multipart-body :default
32+
[multipart]
33+
(throw (Exception. (str "Unsupported type for multipart content: " (type (:content multipart))))))
34+
35+
(defmethod make-multipart-body File
36+
; Create a FileBody object from the given map, requiring at least :content
1737
[{:keys [^String name ^String mime-type ^File content ^String encoding]}]
1838
(cond
1939
(and name mime-type content encoding)
@@ -34,24 +54,24 @@
3454
:else
3555
(throw (Exception. "Multipart file body must contain at least :content"))))
3656

37-
(defn make-input-stream-body
38-
"Create an InputStreamBody object from the given map, requiring at least
39-
:content and :name. If no :length is specified, clj-http will use
40-
chunked transfer-encoding, if :length is specified, clj-http will
41-
workaround things be proxying the InputStreamBody to return a length."
57+
(defmethod make-multipart-body InputStream
58+
; Create an InputStreamBody object from the given map, requiring at least
59+
; :content and :name. If no :length is specified, clj-http will use
60+
; chunked transfer-encoding, if :length is specified, clj-http will
61+
; workaround things be proxying the InputStreamBody to return a length.
4262
[{:keys [^String name ^String mime-type ^InputStream content length]}]
4363
(cond
4464
(and content name length)
4565
(if mime-type
46-
(proxy [InputStreamBody] [content mime-type name]
66+
(proxy [InputStreamBody] [content (ContentType/create mime-type) name]
4767
(getContentLength []
4868
length))
4969
(proxy [InputStreamBody] [content name]
5070
(getContentLength []
5171
length)))
5272

5373
(and content mime-type name)
54-
(InputStreamBody. content mime-type name)
74+
(InputStreamBody. content (ContentType/create mime-type) name)
5575

5676
(and content name)
5777
(InputStreamBody. content name)
@@ -60,13 +80,13 @@
6080
(throw (Exception. (str "Multipart input stream body must contain "
6181
"at least :content and :name")))))
6282

63-
(defn make-byte-array-body
64-
"Create a ByteArrayBody object from the given map, requiring at least :content
65-
and :name."
83+
(defmethod make-multipart-body byte-array-type
84+
; Create a ByteArrayBody object from the given map, requiring at least :content
85+
; and :name.
6686
[{:keys [^String name ^String mime-type ^bytes content]}]
6787
(cond
6888
(and content name mime-type)
69-
(ByteArrayBody. content mime-type name)
89+
(ByteArrayBody. content (ContentType/create mime-type) name)
7090

7191
(and content name)
7292
(ByteArrayBody. content name)
@@ -75,48 +95,25 @@
7595
(throw (Exception. (str "Multipart byte array body must contain "
7696
"at least :content and :name")))))
7797

78-
(defn make-string-body
79-
"Create a StringBody object from the given map, requiring at least :content.
80-
If :encoding is specified, it will be created using the Charset for
81-
that encoding."
98+
(defmethod make-multipart-body String
99+
; Create a StringBody object from the given map, requiring at least :content.
100+
; If :encoding is specified, it will be created using the Charset for
101+
; that encoding.
82102
[{:keys [mime-type ^String content encoding]}]
83103
(cond
84104
(and content mime-type encoding)
85-
(StringBody. content mime-type (Charset/forName encoding))
105+
(StringBody. content (ContentType/create mime-type encoding))
86106

87107
(and content encoding)
88-
(StringBody. content (Charset/forName encoding))
108+
(StringBody. content (ContentType/create "text/plain" encoding))
89109

90110
content
91-
(StringBody. content)
92-
93-
:else
94-
(throw (Exception. (str "Multipart string body must contain "
95-
"at least :content")))))
96-
97-
(defn make-multipart-body
98-
"Create a body object from the given map, dispatching on the type
99-
of its content. Requires the content to be of type File, InputStream,
100-
ByteArray, or String."
101-
[multipart]
102-
(let [klass (type (:content multipart))]
103-
;; TODO: replace with multimethod? actually helpful?
104-
(cond
105-
(isa? klass File)
106-
(make-file-body multipart)
107-
108-
(isa? klass InputStream)
109-
(make-input-stream-body multipart)
110-
111-
(= klass byte-array-type)
112-
(make-byte-array-body multipart)
113-
114-
(= klass String)
115-
(make-string-body multipart)
111+
(StringBody. content (ContentType/create "text/plain" Consts/ASCII))))
116112

117-
:else
118-
(throw (Exception. (str "Multipart content must be of type File, "
119-
"InputStream, ByteArray, or String."))))))
113+
(defmethod make-multipart-body ContentBody
114+
; Use provided org.apache.http.entity.mime.content.ContentBody directly
115+
[{:keys [^ContentBody content]}]
116+
content)
120117

121118
(defn create-multipart-entity
122119
"Takes a multipart vector of maps and creates a MultipartEntity with each
Lines changed: 160 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,163 @@
11
(ns clj-http.test.multipart-test
22
(:require [clj-http.multipart :refer :all]
33
[clojure.test :refer :all])
4-
(:import (java.io File)
5-
(org.apache.http.entity.mime.content FileBody)))
6-
7-
(deftest test-make-file-body
8-
(testing "no content throws exception"
9-
(is (thrown? Exception (make-file-body {}))))
10-
11-
(testing "can create FileBody with content only"
12-
(let [testfile (File. "testfile")
13-
file-body (make-file-body {:content (File. "testfile")})]
14-
(is (= (.getFile ^FileBody file-body) testfile))))
15-
16-
(testing "can create FileBody with content and mime-type"
17-
(let [testfile (File. "testfile")
18-
file-body (make-file-body {:content (File. "testfile")
19-
:mime-type "application/octet-stream"})]
20-
(is (= (.getFile ^FileBody file-body) testfile))))
21-
22-
(testing "can create FileBody with content and mime-type and name"
23-
(let [testfile (File. "testfile")
24-
file-body (make-file-body {:content (File. "testfile")
25-
:mime-type "application/octet-stream"
26-
:name "testname"})]
27-
(is (= (.getFile ^FileBody file-body) testfile))
28-
(is (= (.getFilename ^FileBody file-body) "testname"))))
29-
30-
(testing "can create FileBody with content and mime-type and encoding"
31-
(let [testfile (File. "testfile")
32-
file-body (make-file-body {:content (File. "testfile")
33-
:mime-type "application/octet-stream"
34-
:encoding "utf-8"})]
35-
(is (= (.getFile ^FileBody file-body) testfile))))
36-
37-
(testing "can create FileBody with content, mime-type, encoding, and name"
38-
(let [testfile (File. "testfile")
39-
file-body (make-file-body {:content (File. "testfile")
40-
:mime-type "application/octet-stream"
41-
:encoding "utf-8"
42-
:name "testname"})]
43-
(is (= (.getFile ^FileBody file-body) testfile))
44-
(is (= (.getFilename ^FileBody file-body) "testname")))))
4+
(:import (java.io File ByteArrayOutputStream ByteArrayInputStream)
5+
(org.apache.http.entity.mime.content FileBody StringBody ContentBody ByteArrayBody InputStreamBody)
6+
(java.nio.charset Charset)))
7+
8+
(defn body-str [^StringBody body]
9+
(-> body .getReader slurp))
10+
11+
(defn body-bytes [^ContentBody body]
12+
(let [buf (ByteArrayOutputStream.)]
13+
(.writeTo body buf)
14+
(.toByteArray buf)))
15+
16+
(defn body-charset [^ContentBody body]
17+
(-> body .getContentType .getCharset))
18+
19+
(defn body-mime-type [^ContentBody body]
20+
(-> body .getContentType .getMimeType))
21+
22+
(defn make-input-stream [& bytes]
23+
(ByteArrayInputStream. (byte-array bytes)))
24+
25+
(deftest test-multipart-body
26+
(testing "nil content throws exception"
27+
(is (thrown-with-msg? Exception #"Multipart content cannot be nil"
28+
(make-multipart-body {:content nil}))))
29+
30+
(testing "unsupported content type throws exception"
31+
(is (thrown-with-msg? Exception #"Unsupported type for multipart content: class java.lang.Object"
32+
(make-multipart-body {:content (Object.)}))))
33+
34+
(testing "ContentBody content direct usage"
35+
(let [contentBody (StringBody. "abc")]
36+
(is (identical? contentBody (make-multipart-body {:content contentBody})))))
37+
38+
(testing "StringBody"
39+
40+
(testing "can create StringBody with content only"
41+
(let [body (make-multipart-body {:content "abc"})]
42+
(is (instance? StringBody body))
43+
(is (= "abc" (body-str body)))))
44+
45+
(testing "can create StringBody with content and encoding"
46+
(let [body (make-multipart-body {:content "abc" :encoding "ascii"})]
47+
(is (instance? StringBody body))
48+
(is (= "abc" (body-str body)))
49+
(is (= (Charset/forName "ascii") (body-charset body)))))
50+
51+
(testing "can create StringBody with content and mime-type and encoding"
52+
(let [body (make-multipart-body {:content "abc" :mime-type "stream-body" :encoding "ascii"})]
53+
(is (instance? StringBody body))
54+
(is (= "abc" (body-str body)))
55+
(is (= (Charset/forName "ascii") (body-charset body)))
56+
(is (= "stream-body" (body-mime-type body))))))
57+
58+
(testing "ByteArrayBody"
59+
60+
(testing "exception thrown on missing name"
61+
(is (thrown-with-msg? Exception #"Multipart byte array body must contain at least :content and :name"
62+
(make-multipart-body {:content (byte-array [0 1 2])}))))
63+
64+
(testing "can create ByteArrayBody with name only"
65+
(let [body (make-multipart-body {:content (byte-array [0 1 2]) :name "testname"})]
66+
(is (instance? ByteArrayBody body))
67+
(is (= "testname" (.getFilename body)))
68+
(is (= [0 1 2] (vec (body-bytes body))))))
69+
70+
(testing "can create ByteArrayBody with name and mime-type"
71+
(let [body (make-multipart-body {:content (byte-array [0 1 2])
72+
:name "testname"
73+
:mime-type "byte-body"})]
74+
(is (instance? ByteArrayBody body))
75+
(is (= "testname" (.getFilename body)))
76+
(is (= "byte-body" (body-mime-type body)))
77+
(is (= [0 1 2] (vec (body-bytes body)))))))
78+
79+
(testing "InputStreamBody"
80+
81+
(testing "exception thrown on missing name"
82+
(is (thrown-with-msg?
83+
Exception
84+
#"Multipart input stream body must contain at least :content and :name"
85+
(make-multipart-body {:content (ByteArrayInputStream. (byte-array [0 1 2]))}))))
86+
87+
(testing "can create InputStreamBody with name and content"
88+
(let [input-stream (make-input-stream 1 2 3)
89+
body (make-multipart-body {:content input-stream
90+
:name "testname"})]
91+
(is (instance? InputStreamBody body))
92+
(is (= "testname" (.getFilename body)))
93+
(is (identical? input-stream (.getInputStream body)))))
94+
95+
(testing "can create InputStreamBody with name, content and mime-type"
96+
(let [input-stream (make-input-stream 1 2 3)
97+
body (make-multipart-body {:content input-stream
98+
:name "testname"
99+
:mime-type "input-stream-body"})]
100+
(is (instance? InputStreamBody body))
101+
(is (= "testname" (.getFilename body)))
102+
(is (= "input-stream-body" (body-mime-type body)))
103+
(is (identical? input-stream (.getInputStream body)))))
104+
105+
(testing "can create input InputStreamBody name, content, mime-type and length"
106+
(let [input-stream (make-input-stream 1 2 3)
107+
body (make-multipart-body {:content input-stream
108+
:name "testname"
109+
:mime-type "input-stream-body"
110+
:length 42})]
111+
(is (instance? InputStreamBody body))
112+
(is (= "testname" (.getFilename body)))
113+
(is (= "input-stream-body" (body-mime-type body)))
114+
(is (identical? input-stream (.getInputStream body)))
115+
(is (= 42 (.getContentLength body))))))
116+
117+
(testing "FileBody"
118+
119+
(testing "can create FileBody with content only"
120+
(let [test-file (File. "testfile")
121+
body (make-multipart-body {:content test-file})]
122+
(is (instance? FileBody body))
123+
(is (= test-file (.getFile body)))))
124+
125+
(testing "can create FileBody with content and mime-type"
126+
(let [test-file (File. "testfile")
127+
body (make-multipart-body {:content test-file
128+
:mime-type "file-body"})]
129+
(is (instance? FileBody body))
130+
(is (= "file-body" (body-mime-type body)))
131+
(is (= test-file (.getFile body)))))
132+
133+
(testing "can create FileBody with content, mime-type and name"
134+
(let [test-file (File. "testfile")
135+
body (make-multipart-body {:content test-file
136+
:mime-type "file-body"
137+
:name "testname"})]
138+
(is (instance? FileBody body))
139+
(is (= "file-body" (body-mime-type body)))
140+
(is (= test-file (.getFile body)))
141+
(is (= "testname" (.getFilename body)))))
142+
143+
(testing "can create FileBody with content and mime-type and encoding"
144+
(let [test-file (File. "testfile")
145+
body (make-multipart-body {:content test-file
146+
:mime-type "file-body"
147+
:encoding "ascii"})]
148+
(is (instance? FileBody body))
149+
(is (= "file-body" (body-mime-type body)))
150+
(is (= (Charset/forName "ascii") (body-charset body)))
151+
(is (= test-file (.getFile body)))))
152+
153+
(testing "can create FileBody with content, mime-type, encoding and name"
154+
(let [test-file (File. "testfile")
155+
body (make-multipart-body {:content test-file
156+
:mime-type "file-body"
157+
:encoding "ascii"
158+
:name "testname"})]
159+
(is (instance? FileBody body))
160+
(is (= "file-body" (body-mime-type body)))
161+
(is (= (Charset/forName "ascii") (body-charset body)))
162+
(is (= test-file (.getFile body) ))
163+
(is (= "testname" (.getFilename body)))))))

0 commit comments

Comments
 (0)