| /* |
| * 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.squareup.okhttp; |
| |
| import com.squareup.okhttp.internal.Util; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.UUID; |
| import okio.Buffer; |
| import okio.BufferedSink; |
| import okio.ByteString; |
| |
| /** |
| * Fluent API to build <a href="http://www.ietf.org/rfc/rfc2387.txt">RFC |
| * 2387</a>-compliant request bodies. |
| */ |
| 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); |
| } |
| } |
| } |