303 lines
10 KiB
Java
303 lines
10 KiB
Java
/* GENERATED SOURCE. DO NOT MODIFY. */
|
|
/*
|
|
* Copyright (C) 2014 Square, Inc.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
package com.android.okhttp;
|
|
|
|
import com.android.okhttp.internal.Util;
|
|
import java.io.IOException;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.UUID;
|
|
import com.android.okhttp.okio.Buffer;
|
|
import com.android.okhttp.okio.BufferedSink;
|
|
import com.android.okhttp.okio.ByteString;
|
|
|
|
/**
|
|
* Fluent API to build <a href="http://www.ietf.org/rfc/rfc2387.txt">RFC
|
|
* 2387</a>-compliant request bodies.
|
|
* @hide This class is not part of the Android public SDK API
|
|
*/
|
|
public final class MultipartBuilder {
|
|
/**
|
|
* 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.parse("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.parse("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.parse("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.parse("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.parse("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 MediaType type = MIXED;
|
|
|
|
// Parallel lists of nullable headers and non-null bodies.
|
|
private final List<Headers> partHeaders = new ArrayList<>();
|
|
private final List<RequestBody> partBodies = new ArrayList<>();
|
|
|
|
/** Creates a new multipart builder that uses a random boundary token. */
|
|
public MultipartBuilder() {
|
|
this(UUID.randomUUID().toString());
|
|
}
|
|
|
|
/**
|
|
* Creates a new multipart builder that uses {@code boundary} to separate
|
|
* parts. Prefer the no-argument constructor to defend against injection
|
|
* attacks.
|
|
*/
|
|
public MultipartBuilder(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 MultipartBuilder type(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 MultipartBuilder addPart(RequestBody body) {
|
|
return addPart(null, body);
|
|
}
|
|
|
|
/** Add a part to the body. */
|
|
public MultipartBuilder addPart(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");
|
|
}
|
|
|
|
partHeaders.add(headers);
|
|
partBodies.add(body);
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
private 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;
|
|
}
|
|
|
|
/** Add a form data part to the body. */
|
|
public MultipartBuilder addFormDataPart(String name, String value) {
|
|
return addFormDataPart(name, null, RequestBody.create(null, value));
|
|
}
|
|
|
|
/** Add a form data part to the body. */
|
|
public MultipartBuilder addFormDataPart(String name, String filename, RequestBody value) {
|
|
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 addPart(Headers.of("Content-Disposition", disposition.toString()), value);
|
|
}
|
|
|
|
/** Assemble the specified parts into a request body. */
|
|
public RequestBody build() {
|
|
if (partHeaders.isEmpty()) {
|
|
throw new IllegalStateException("Multipart body must have at least one part.");
|
|
}
|
|
return new MultipartRequestBody(type, boundary, partHeaders, partBodies);
|
|
}
|
|
|
|
private static final class MultipartRequestBody extends RequestBody {
|
|
private final ByteString boundary;
|
|
private final MediaType contentType;
|
|
private final List<Headers> partHeaders;
|
|
private final List<RequestBody> partBodies;
|
|
private long contentLength = -1L;
|
|
|
|
public MultipartRequestBody(MediaType type, ByteString boundary, List<Headers> partHeaders,
|
|
List<RequestBody> partBodies) {
|
|
if (type == null) throw new NullPointerException("type == null");
|
|
|
|
this.boundary = boundary;
|
|
this.contentType = MediaType.parse(type + "; boundary=" + boundary.utf8());
|
|
this.partHeaders = Util.immutableList(partHeaders);
|
|
this.partBodies = Util.immutableList(partBodies);
|
|
}
|
|
|
|
@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);
|
|
}
|
|
|
|
/**
|
|
* 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(BufferedSink sink, boolean countBytes) throws IOException {
|
|
long byteCount = 0L;
|
|
|
|
Buffer byteCountBuffer = null;
|
|
if (countBytes) {
|
|
sink = byteCountBuffer = new Buffer();
|
|
}
|
|
|
|
for (int p = 0, partCount = partHeaders.size(); p < partCount; p++) {
|
|
Headers headers = partHeaders.get(p);
|
|
RequestBody body = partBodies.get(p);
|
|
|
|
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) {
|
|
sink.writeUtf8("Content-Length: ")
|
|
.writeDecimalLong(contentLength)
|
|
.write(CRLF);
|
|
} 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 {
|
|
partBodies.get(p).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;
|
|
}
|
|
|
|
@Override public void writeTo(BufferedSink sink) throws IOException {
|
|
writeOrCountBytes(sink, false);
|
|
}
|
|
}
|
|
}
|