Skip to content

Commit 3e76568

Browse files
committed
add Multipart functionality to JDK Http Client (thanks to https://github.com/eos1d3)
1 parent 90c12df commit 3e76568

File tree

12 files changed

+262
-180
lines changed

12 files changed

+262
-180
lines changed

changelog

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
[SNAPSHOT]
2+
* add Multipart functionality to JDK Http Client (thanks to https://github.com/eos1d3)
3+
14
[5.5.0]
25
* fix error parsing for Fitbit (thanks to https://github.com/danmana)
36
* optimize debug log performance impact on prod in OAuth1 and fix

scribejava-core/src/main/java/com/github/scribejava/core/httpclient/AbstractAsyncOnlyHttpClient.java

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,27 +13,32 @@ public abstract class AbstractAsyncOnlyHttpClient implements HttpClient {
1313
@Override
1414
public Response execute(String userAgent, Map<String, String> headers, Verb httpVerb, String completeUrl,
1515
byte[] bodyContents) throws InterruptedException, ExecutionException, IOException {
16+
17+
return executeAsync(userAgent, headers, httpVerb, completeUrl, bodyContents, null,
18+
(OAuthRequest.ResponseConverter<Response>) null).get();
19+
}
20+
21+
@Override
22+
public Response execute(String userAgent, Map<String, String> headers, Verb httpVerb, String completeUrl,
23+
MultipartPayload bodyContents) throws InterruptedException, ExecutionException, IOException {
24+
1625
return executeAsync(userAgent, headers, httpVerb, completeUrl, bodyContents, null,
1726
(OAuthRequest.ResponseConverter<Response>) null).get();
1827
}
1928

2029
@Override
2130
public Response execute(String userAgent, Map<String, String> headers, Verb httpVerb, String completeUrl,
2231
String bodyContents) throws InterruptedException, ExecutionException, IOException {
32+
2333
return executeAsync(userAgent, headers, httpVerb, completeUrl, bodyContents, null,
2434
(OAuthRequest.ResponseConverter<Response>) null).get();
2535
}
2636

2737
@Override
2838
public Response execute(String userAgent, Map<String, String> headers, Verb httpVerb, String completeUrl,
2939
File bodyContents) throws InterruptedException, ExecutionException, IOException {
40+
3041
return executeAsync(userAgent, headers, httpVerb, completeUrl, bodyContents, null,
3142
(OAuthRequest.ResponseConverter<Response>) null).get();
3243
}
33-
34-
@Override
35-
public Response execute(String userAgent, Map<String, String> headers, Verb httpVerb, String completeUrl,
36-
OAuthRequest.MultipartPayloads multipartPayloads) throws InterruptedException, ExecutionException, IOException {
37-
throw new UnsupportedOperationException("This HttpClient does not support Multipart payload for the moment");
38-
}
3944
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.github.scribejava.core.httpclient;
2+
3+
public class BodyPartPayload {
4+
5+
private final String contentDisposition;
6+
private final String contentType;
7+
private final byte[] payload;
8+
9+
public BodyPartPayload(String contentDisposition, String contentType, byte[] payload) {
10+
this.contentDisposition = contentDisposition;
11+
this.contentType = contentType;
12+
this.payload = payload;
13+
}
14+
15+
public String getContentDisposition() {
16+
return contentDisposition;
17+
}
18+
19+
public String getContentType() {
20+
return contentType;
21+
}
22+
23+
public byte[] getPayload() {
24+
return payload;
25+
}
26+
27+
}

scribejava-core/src/main/java/com/github/scribejava/core/httpclient/HttpClient.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,18 @@
1212
import java.util.concurrent.Future;
1313

1414
public interface HttpClient extends Closeable {
15+
1516
String DEFAULT_CONTENT_TYPE = "application/x-www-form-urlencoded";
1617
String CONTENT_TYPE = "Content-Type";
1718
String CONTENT_LENGTH = "Content-Length";
1819

1920
<T> Future<T> executeAsync(String userAgent, Map<String, String> headers, Verb httpVerb, String completeUrl,
2021
byte[] bodyContents, OAuthAsyncRequestCallback<T> callback, OAuthRequest.ResponseConverter<T> converter);
2122

23+
<T> Future<T> executeAsync(String userAgent, Map<String, String> headers, Verb httpVerb, String completeUrl,
24+
MultipartPayload bodyContents, OAuthAsyncRequestCallback<T> callback,
25+
OAuthRequest.ResponseConverter<T> converter);
26+
2227
<T> Future<T> executeAsync(String userAgent, Map<String, String> headers, Verb httpVerb, String completeUrl,
2328
String bodyContents, OAuthAsyncRequestCallback<T> callback, OAuthRequest.ResponseConverter<T> converter);
2429

@@ -27,9 +32,9 @@ <T> Future<T> executeAsync(String userAgent, Map<String, String> headers, Verb h
2732

2833
Response execute(String userAgent, Map<String, String> headers, Verb httpVerb, String completeUrl,
2934
byte[] bodyContents) throws InterruptedException, ExecutionException, IOException;
30-
35+
3136
Response execute(String userAgent, Map<String, String> headers, Verb httpVerb, String completeUrl,
32-
OAuthRequest.MultipartPayloads multipartPayloads) throws InterruptedException, ExecutionException, IOException;
37+
MultipartPayload bodyContents) throws InterruptedException, ExecutionException, IOException;
3338

3439
Response execute(String userAgent, Map<String, String> headers, Verb httpVerb, String completeUrl,
3540
String bodyContents) throws InterruptedException, ExecutionException, IOException;
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package com.github.scribejava.core.httpclient;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
6+
/**
7+
* The class containing more than one payload of multipart/form-data request
8+
*/
9+
public class MultipartPayload {
10+
11+
private final String boundary;
12+
private final List<BodyPartPayload> bodyParts = new ArrayList<>();
13+
14+
public MultipartPayload(String boundary) {
15+
this.boundary = boundary;
16+
}
17+
18+
public byte[] getStartBoundary(BodyPartPayload bodyPart) {
19+
return ("--" + boundary + "\r\n"
20+
+ "Content-Disposition: " + bodyPart.getContentDisposition() + "\r\n"
21+
+ (bodyPart.getContentType() == null ? "" : "Content-Type: " + bodyPart.getContentType() + "\r\n")
22+
+ "\r\n").getBytes();
23+
}
24+
25+
public byte[] getEndBoundary() {
26+
return ("\r\n--" + boundary + "--\r\n").getBytes();
27+
}
28+
29+
public int getContentLength() {
30+
int contentLength = 0;
31+
for (BodyPartPayload bodyPart : bodyParts) {
32+
contentLength += bodyPart.getPayload().length
33+
+ bodyPart.getContentDisposition().length();
34+
if (bodyPart.getContentType() != null) {
35+
contentLength += 16 //length of constant portions of contentType header
36+
+ bodyPart.getContentType().length();
37+
}
38+
}
39+
40+
contentLength += (37 //length of constant portions of contentDisposition header,
41+
//see getStartBoundary and getEndBoundary methods
42+
+ boundary.length() * 2 //twice. start and end parts
43+
) * bodyParts.size(); //for every part
44+
return contentLength;
45+
}
46+
47+
public List<BodyPartPayload> getBodyParts() {
48+
return bodyParts;
49+
}
50+
51+
public void addMultipartPayload(String contentDisposition, String contentType, byte[] payload) {
52+
bodyParts.add(new BodyPartPayload(contentDisposition, contentType, payload));
53+
}
54+
}

scribejava-core/src/main/java/com/github/scribejava/core/httpclient/jdk/JDKHttpClient.java

Lines changed: 67 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package com.github.scribejava.core.httpclient.jdk;
22

33
import com.github.scribejava.core.exceptions.OAuthException;
4+
import com.github.scribejava.core.httpclient.BodyPartPayload;
45
import com.github.scribejava.core.httpclient.HttpClient;
6+
import com.github.scribejava.core.httpclient.MultipartPayload;
57
import com.github.scribejava.core.model.OAuthAsyncRequestCallback;
68
import com.github.scribejava.core.model.OAuthConstants;
79
import com.github.scribejava.core.model.OAuthRequest;
@@ -38,32 +40,46 @@ public void close() {
3840
@Override
3941
public <T> Future<T> executeAsync(String userAgent, Map<String, String> headers, Verb httpVerb, String completeUrl,
4042
byte[] bodyContents, OAuthAsyncRequestCallback<T> callback, OAuthRequest.ResponseConverter<T> converter) {
41-
try {
42-
final Response response = execute(userAgent, headers, httpVerb, completeUrl, bodyContents);
43-
@SuppressWarnings("unchecked")
44-
final T t = converter == null ? (T) response : converter.convert(response);
45-
if (callback != null) {
46-
callback.onCompleted(t);
47-
}
48-
return new JDKHttpFuture<>(t);
49-
} catch (InterruptedException | ExecutionException | IOException e) {
50-
callback.onThrowable(e);
51-
return new JDKHttpFuture<>(e);
52-
}
43+
44+
return doExecuteAsync(userAgent, headers, httpVerb, completeUrl, BodyType.BYTE_ARRAY, bodyContents, callback,
45+
converter);
46+
}
47+
48+
@Override
49+
public <T> Future<T> executeAsync(String userAgent, Map<String, String> headers, Verb httpVerb, String completeUrl,
50+
MultipartPayload bodyContents, OAuthAsyncRequestCallback<T> callback,
51+
OAuthRequest.ResponseConverter<T> converter) {
52+
53+
return doExecuteAsync(userAgent, headers, httpVerb, completeUrl, BodyType.MULTIPART, bodyContents, callback,
54+
converter);
5355
}
5456

5557
@Override
5658
public <T> Future<T> executeAsync(String userAgent, Map<String, String> headers, Verb httpVerb, String completeUrl,
5759
String bodyContents, OAuthAsyncRequestCallback<T> callback, OAuthRequest.ResponseConverter<T> converter) {
60+
61+
return doExecuteAsync(userAgent, headers, httpVerb, completeUrl, BodyType.STRING, bodyContents, callback,
62+
converter);
63+
}
64+
65+
@Override
66+
public <T> Future<T> executeAsync(String userAgent, Map<String, String> headers, Verb httpVerb, String completeUrl,
67+
File bodyContents, OAuthAsyncRequestCallback<T> callback, OAuthRequest.ResponseConverter<T> converter) {
68+
throw new UnsupportedOperationException("JDKHttpClient does not support File payload for the moment");
69+
}
70+
71+
private <T> Future<T> doExecuteAsync(String userAgent, Map<String, String> headers, Verb httpVerb,
72+
String completeUrl, BodyType bodyType, Object bodyContents, OAuthAsyncRequestCallback<T> callback,
73+
OAuthRequest.ResponseConverter<T> converter) {
5874
try {
59-
final Response response = execute(userAgent, headers, httpVerb, completeUrl, bodyContents);
75+
final Response response = doExecute(userAgent, headers, httpVerb, completeUrl, bodyType, bodyContents);
6076
@SuppressWarnings("unchecked")
6177
final T t = converter == null ? (T) response : converter.convert(response);
6278
if (callback != null) {
6379
callback.onCompleted(t);
6480
}
6581
return new JDKHttpFuture<>(t);
66-
} catch (InterruptedException | ExecutionException | IOException e) {
82+
} catch (IOException e) {
6783
if (callback != null) {
6884
callback.onThrowable(e);
6985
}
@@ -72,15 +88,15 @@ public <T> Future<T> executeAsync(String userAgent, Map<String, String> headers,
7288
}
7389

7490
@Override
75-
public <T> Future<T> executeAsync(String userAgent, Map<String, String> headers, Verb httpVerb, String completeUrl,
76-
File bodyContents, OAuthAsyncRequestCallback<T> callback, OAuthRequest.ResponseConverter<T> converter) {
77-
throw new UnsupportedOperationException("JDKHttpClient does not support File payload for the moment");
91+
public Response execute(String userAgent, Map<String, String> headers, Verb httpVerb, String completeUrl,
92+
byte[] bodyContents) throws InterruptedException, ExecutionException, IOException {
93+
return doExecute(userAgent, headers, httpVerb, completeUrl, BodyType.BYTE_ARRAY, bodyContents);
7894
}
7995

8096
@Override
8197
public Response execute(String userAgent, Map<String, String> headers, Verb httpVerb, String completeUrl,
82-
byte[] bodyContents) throws InterruptedException, ExecutionException, IOException {
83-
return doExecute(userAgent, headers, httpVerb, completeUrl, BodyType.BYTE_ARRAY, bodyContents);
98+
MultipartPayload multipartPayloads) throws InterruptedException, ExecutionException, IOException {
99+
return doExecute(userAgent, headers, httpVerb, completeUrl, BodyType.MULTIPART, multipartPayloads);
84100
}
85101

86102
@Override
@@ -92,15 +108,9 @@ public Response execute(String userAgent, Map<String, String> headers, Verb http
92108
@Override
93109
public Response execute(String userAgent, Map<String, String> headers, Verb httpVerb, String completeUrl,
94110
File bodyContents) throws InterruptedException, ExecutionException, IOException {
95-
throw new UnsupportedOperationException("JDKHttpClient do not support File payload for the moment");
111+
throw new UnsupportedOperationException("JDKHttpClient does not support File payload for the moment");
96112
}
97113

98-
@Override
99-
public Response execute(String userAgent, Map<String, String> headers, Verb httpVerb, String completeUrl,
100-
OAuthRequest.MultipartPayloads multipartPayloads) throws InterruptedException, ExecutionException, IOException {
101-
return doExecute(userAgent, headers, httpVerb, completeUrl, BodyType.MULTIPART, multipartPayloads);
102-
}
103-
104114
private Response doExecute(String userAgent, Map<String, String> headers, Verb httpVerb, String completeUrl,
105115
BodyType bodyType, Object bodyContents) throws IOException {
106116
final HttpURLConnection connection = (HttpURLConnection) new URL(completeUrl).openConnection();
@@ -136,9 +146,9 @@ void setBody(HttpURLConnection connection, Object bodyContents, boolean requires
136146
}
137147
},
138148
MULTIPART {
139-
@Override
149+
@Override
140150
void setBody(HttpURLConnection connection, Object bodyContents, boolean requiresBody) throws IOException {
141-
addBody(connection, (OAuthRequest.MultipartPayloads) bodyContents, requiresBody);
151+
addBody(connection, (MultipartPayload) bodyContents, requiresBody);
142152
}
143153
},
144154
STRING {
@@ -150,7 +160,6 @@ void setBody(HttpURLConnection connection, Object bodyContents, boolean requires
150160

151161
abstract void setBody(HttpURLConnection connection, Object bodyContents, boolean requiresBody)
152162
throws IOException;
153-
154163
}
155164

156165
private static Map<String, String> parseHeaders(HttpURLConnection conn) {
@@ -178,41 +187,41 @@ private static void addHeaders(HttpURLConnection connection, Map<String, String>
178187
}
179188
}
180189

181-
/*
182-
* Multipart implementation supporting more than one payload
183-
*
190+
/**
191+
* Multipart implementation supporting more than one payload
184192
*/
185-
private static void addBody(HttpURLConnection connection, OAuthRequest.MultipartPayloads multipartPayloads, boolean requiresBody) throws IOException {
186-
int contentLength = multipartPayloads.getContentLength();
187-
System.out.println("length: " + contentLength);
193+
private static void addBody(HttpURLConnection connection, MultipartPayload multipartPayload, boolean requiresBody)
194+
throws IOException {
195+
196+
final int contentLength = multipartPayload.getContentLength();
188197
if (requiresBody || contentLength > 0) {
189-
connection.setRequestProperty(CONTENT_LENGTH, String.valueOf(contentLength));
190-
if (connection.getRequestProperty(CONTENT_TYPE) == null) {
191-
connection.setRequestProperty(CONTENT_TYPE, DEFAULT_CONTENT_TYPE);
198+
final OutputStream os = prepareConnectionForBodyAndGetOutputStream(connection, contentLength);
199+
200+
for (BodyPartPayload bodyPart : multipartPayload.getBodyParts()) {
201+
os.write(multipartPayload.getStartBoundary(bodyPart));
202+
os.write(bodyPart.getPayload());
203+
os.write(multipartPayload.getEndBoundary());
192204
}
193-
System.out.println("content-length: " + connection.getRequestProperty(CONTENT_TYPE));
194-
connection.setDoOutput(true);
195-
OutputStream os = connection.getOutputStream();
196-
197-
int totalParts = multipartPayloads.getMultipartPayloadList().size();
198-
for (int i = 0; i < totalParts; i++) {
199-
os.write(multipartPayloads.getStartBoundary(i));
200-
os.write(multipartPayloads.getMultipartPayloadList().get(i).getPayload(), 0, multipartPayloads.getMultipartPayloadList().get(i).getLength());
201-
os.write(multipartPayloads.getEndBoundary(i));
202-
}
203-
}
204-
}
205-
205+
}
206+
}
207+
206208
private static void addBody(HttpURLConnection connection, byte[] content, boolean requiresBody) throws IOException {
207209
final int contentLength = content.length;
208210
if (requiresBody || contentLength > 0) {
209-
connection.setRequestProperty(CONTENT_LENGTH, String.valueOf(contentLength));
210-
if (connection.getRequestProperty(CONTENT_TYPE) == null) {
211-
connection.setRequestProperty(CONTENT_TYPE, DEFAULT_CONTENT_TYPE);
212-
}
213-
connection.setDoOutput(true);
214-
connection.getOutputStream().write(content);
211+
prepareConnectionForBodyAndGetOutputStream(connection, contentLength).write(content);
215212
}
216213
}
217-
214+
215+
private static OutputStream prepareConnectionForBodyAndGetOutputStream(HttpURLConnection connection,
216+
int contentLength) throws IOException {
217+
218+
connection.setRequestProperty(CONTENT_LENGTH, String.valueOf(contentLength));
219+
if (connection.getRequestProperty(CONTENT_TYPE) == null) {
220+
connection.setRequestProperty(CONTENT_TYPE, DEFAULT_CONTENT_TYPE);
221+
222+
}
223+
connection.setDoOutput(true);
224+
return connection.getOutputStream();
225+
}
226+
218227
}

0 commit comments

Comments
 (0)