Last active
October 5, 2019 18:00
-
-
Save antarikshc/3cfce8f31f2ac3d38caaf4dc56279420 to your computer and use it in GitHub Desktop.
OkHttp Multipart Request with custom RequestBody
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.io.IOException; | |
import java.util.ArrayList; | |
import java.util.List; | |
import java.util.UUID; | |
import okhttp3.Headers; | |
import okhttp3.MediaType; | |
import okhttp3.RequestBody; | |
import okhttp3.internal.Util; | |
import okio.Buffer; | |
import okio.BufferedSink; | |
import okio.ByteString; | |
/** | |
* An <a href="http://www.ietf.org/rfc/rfc2387.txt">RFC 2387</a>-compliant request body. | |
*/ | |
public final class CustomRequestBody extends RequestBody { | |
/** | |
* The "mixed" subtype of "multipart" is intended for use when the body parts are independent and | |
* need to be bundled in a particular order. Any "multipart" subtypes that an implementation does | |
* not recognize must be treated as being of subtype "mixed". | |
*/ | |
public static final MediaType MIXED = MediaType.get("multipart/mixed"); | |
/** | |
* The "multipart/alternative" type is syntactically identical to "multipart/mixed", but the | |
* semantics are different. In particular, each of the body parts is an "alternative" version of | |
* the same information. | |
*/ | |
public static final MediaType ALTERNATIVE = MediaType.get("multipart/alternative"); | |
/** | |
* This type is syntactically identical to "multipart/mixed", but the semantics are different. In | |
* particular, in a digest, the default {@code Content-Type} value for a body part is changed from | |
* "text/plain" to "message/rfc822". | |
*/ | |
public static final MediaType DIGEST = MediaType.get("multipart/digest"); | |
/** | |
* This type is syntactically identical to "multipart/mixed", but the semantics are different. In | |
* particular, in a parallel entity, the order of body parts is not significant. | |
*/ | |
public static final MediaType PARALLEL = MediaType.get("multipart/parallel"); | |
/** | |
* The media-type multipart/form-data follows the rules of all multipart MIME data streams as | |
* outlined in RFC 2046. In forms, there are a series of fields to be supplied by the user who | |
* fills out the form. Each field has a name. Within a given form, the names are unique. | |
*/ | |
public static final MediaType FORM = MediaType.get("multipart/form-data"); | |
private static final byte[] COLONSPACE = {':', ' '}; | |
private static final byte[] CRLF = {'\r', '\n'}; | |
private static final byte[] DASHDASH = {'-', '-'}; | |
private final ByteString boundary; | |
private final MediaType originalType; | |
private final MediaType contentType; | |
private final List<Part> parts; | |
private long contentLength = -1L; | |
CustomRequestBody(ByteString boundary, MediaType type, List<Part> parts) { | |
this.boundary = boundary; | |
this.originalType = type; | |
this.contentType = MediaType.get(type + "; boundary=" + boundary.utf8()); | |
this.parts = Util.immutableList(parts); | |
} | |
public MediaType type() { | |
return originalType; | |
} | |
public String boundary() { | |
return boundary.utf8(); | |
} | |
/** | |
* The number of parts in this multipart body. | |
*/ | |
public int size() { | |
return parts.size(); | |
} | |
public List<Part> parts() { | |
return parts; | |
} | |
public Part part(int index) { | |
return parts.get(index); | |
} | |
/** | |
* A combination of {@link #type()} and {@link #boundary()}. | |
*/ | |
@Override | |
public MediaType contentType() { | |
return contentType; | |
} | |
@Override | |
public long contentLength() throws IOException { | |
long result = contentLength; | |
if (result != -1L) return result; | |
return contentLength = writeOrCountBytes(null, true); | |
} | |
@Override | |
public void writeTo(BufferedSink sink) throws IOException { | |
writeOrCountBytes(sink, false); | |
} | |
/** | |
* Either writes this request to {@code sink} or measures its content length. We have one method | |
* do double-duty to make sure the counting and content are consistent, particularly when it comes | |
* to awkward operations like measuring the encoded length of header strings, or the | |
* length-in-digits of an encoded integer. | |
*/ | |
private long writeOrCountBytes( | |
@Nullable BufferedSink sink, boolean countBytes) throws IOException { | |
long byteCount = 0L; | |
Buffer byteCountBuffer = null; | |
if (countBytes) { | |
sink = byteCountBuffer = new Buffer(); | |
} | |
if (sink != null) { | |
for (int p = 0, partCount = parts.size(); p < partCount; p++) { | |
Part part = parts.get(p); | |
Headers headers = part.headers; | |
RequestBody body = part.body; | |
sink.write(DASHDASH); | |
sink.write(boundary); | |
sink.write(CRLF); | |
if (headers != null) { | |
for (int h = 0, headerCount = headers.size(); h < headerCount; h++) { | |
sink.writeUtf8(headers.name(h)) | |
.write(COLONSPACE) | |
.writeUtf8(headers.value(h)) | |
.write(CRLF); | |
} | |
} | |
MediaType contentType = body.contentType(); | |
if (contentType != null) { | |
sink.writeUtf8("Content-Type: ") | |
.writeUtf8(contentType.toString()) | |
.write(CRLF); | |
} | |
long contentLength = body.contentLength(); | |
if (contentLength != -1) { | |
// No need to write Content-Length, modern servers will ignore it anyway | |
} else if (countBytes) { | |
// We can't measure the body's size without the sizes of its components. | |
byteCountBuffer.clear(); | |
return -1L; | |
} | |
sink.write(CRLF); | |
if (countBytes) { | |
byteCount += contentLength; | |
} else { | |
body.writeTo(sink); | |
} | |
sink.write(CRLF); | |
} | |
sink.write(DASHDASH); | |
sink.write(boundary); | |
sink.write(DASHDASH); | |
sink.write(CRLF); | |
} | |
if (countBytes) { | |
byteCount += byteCountBuffer.size(); | |
byteCountBuffer.clear(); | |
} | |
return byteCount; | |
} | |
/** | |
* Appends a quoted-string to a StringBuilder. | |
* | |
* <p>RFC 2388 is rather vague about how one should escape special characters in form-data | |
* parameters, and as it turns out Firefox and Chrome actually do rather different things, and | |
* both say in their comments that they're not really sure what the right approach is. We go with | |
* Chrome's behavior (which also experimentally seems to match what IE does), but if you actually | |
* want to have a good chance of things working, please avoid double-quotes, newlines, percent | |
* signs, and the like in your field names. | |
*/ | |
static StringBuilder appendQuotedString(StringBuilder target, String key) { | |
target.append('"'); | |
for (int i = 0, len = key.length(); i < len; i++) { | |
char ch = key.charAt(i); | |
switch (ch) { | |
case '\n': | |
target.append("%0A"); | |
break; | |
case '\r': | |
target.append("%0D"); | |
break; | |
case '"': | |
target.append("%22"); | |
break; | |
default: | |
target.append(ch); | |
break; | |
} | |
} | |
target.append('"'); | |
return target; | |
} | |
public static final class Part { | |
public static Part create(RequestBody body) { | |
return create(null, body); | |
} | |
public static Part create(@Nullable Headers headers, RequestBody body) { | |
if (body == null) { | |
throw new NullPointerException("body == null"); | |
} | |
if (headers != null && headers.get("Content-Type") != null) { | |
throw new IllegalArgumentException("Unexpected header: Content-Type"); | |
} | |
if (headers != null && headers.get("Content-Length") != null) { | |
throw new IllegalArgumentException("Unexpected header: Content-Length"); | |
} | |
return new Part(headers, body); | |
} | |
public static Part createFormData(String name, String value) { | |
return createFormData(name, null, RequestBody.create(null, value)); | |
} | |
public static Part createFormData(String name, @Nullable String filename, RequestBody body) { | |
if (name == null) { | |
throw new NullPointerException("name == null"); | |
} | |
StringBuilder disposition = new StringBuilder("form-data; name="); | |
appendQuotedString(disposition, name); | |
if (filename != null) { | |
disposition.append("; filename="); | |
appendQuotedString(disposition, filename); | |
} | |
return create(Headers.of("Content-Disposition", disposition.toString()), body); | |
} | |
final @Nullable | |
Headers headers; | |
final RequestBody body; | |
private Part(@Nullable Headers headers, RequestBody body) { | |
this.headers = headers; | |
this.body = body; | |
} | |
public @Nullable | |
Headers headers() { | |
return headers; | |
} | |
public RequestBody body() { | |
return body; | |
} | |
} | |
public static final class Builder { | |
private final ByteString boundary; | |
private MediaType type = MIXED; | |
private final List<Part> parts = new ArrayList<>(); | |
public Builder() { | |
this(UUID.randomUUID().toString()); | |
} | |
public Builder(String boundary) { | |
this.boundary = ByteString.encodeUtf8(boundary); | |
} | |
/** | |
* Set the MIME type. Expected values for {@code type} are {@link #MIXED} (the default), {@link | |
* #ALTERNATIVE}, {@link #DIGEST}, {@link #PARALLEL} and {@link #FORM}. | |
*/ | |
public Builder setType(MediaType type) { | |
if (type == null) { | |
throw new NullPointerException("type == null"); | |
} | |
if (!type.type().equals("multipart")) { | |
throw new IllegalArgumentException("multipart != " + type); | |
} | |
this.type = type; | |
return this; | |
} | |
/** | |
* Add a part to the body. | |
*/ | |
public Builder addPart(RequestBody body) { | |
return addPart(Part.create(body)); | |
} | |
/** | |
* Add a part to the body. | |
*/ | |
public Builder addPart(@Nullable Headers headers, RequestBody body) { | |
return addPart(Part.create(headers, body)); | |
} | |
/** | |
* Add a form data part to the body. | |
*/ | |
public Builder addFormDataPart(String name, String value) { | |
return addPart(Part.createFormData(name, value)); | |
} | |
/** | |
* Add a form data part to the body. | |
*/ | |
public Builder addFormDataPart(String name, @Nullable String filename, RequestBody body) { | |
return addPart(Part.createFormData(name, filename, body)); | |
} | |
/** | |
* Add a part to the body. | |
*/ | |
public Builder addPart(Part part) { | |
if (part == null) throw new NullPointerException("part == null"); | |
parts.add(part); | |
return this; | |
} | |
/** | |
* Assemble the specified parts into a request body. | |
*/ | |
public CustomRequestBody build() { | |
if (parts.isEmpty()) { | |
throw new IllegalStateException("Multipart body must have at least one part."); | |
} | |
return new CustomRequestBody(boundary, type, parts); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
val fileRequest = RequestBody.create(MediaType.parse("image/jpeg"), sampleFile) | |
val requestBody = CustomRequestBody.Builder() | |
.setType(MultipartBody.FORM) | |
.addFormDataPart("file", sampleFile.name, fileRequest) | |
.addFormDataPart("name", "<product-name>") | |
.addFormDataPart("wt", "7") | |
.addFormDataPart("ln", "18") | |
.addFormDataPart("qt", "3") | |
.addFormDataPart("purity", "22") | |
.addFormDataPart("deliverydate", "2019-10-03") | |
.addFormDataPart("comment", "Test comment") | |
.addFormDataPart("cid", "<your-cid>") | |
.addFormDataPart("token", "<your-token>") | |
.build() | |
val request = Request.Builder() | |
.url("<full-url-with-customorder_2_p.asp>") | |
.post(requestBody) | |
.build() | |
okHttp.newCall(request) | |
.enqueue(object : okhttp3.Callback { | |
override fun onResponse(call: okhttp3.Call, response: okhttp3.Response) { | |
Log.d(TAG, "Response: ${response.body().toString()}") | |
} | |
override fun onFailure(call: okhttp3.Call, e: IOException) { | |
e.printStackTrace() | |
} | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment