提交 737b844b 编写于 作者: J Jake Wharton

Roll our own multi-part implementation.

Drop Apache mime4j dependency.

Closes #141.
上级 2fcabb79
...@@ -51,7 +51,6 @@ ...@@ -51,7 +51,6 @@
<android.version>4.1.1.4</android.version> <android.version>4.1.1.4</android.version>
<android.platform>16</android.platform> <android.platform>16</android.platform>
<gson.version>2.2.2</gson.version> <gson.version>2.2.2</gson.version>
<httpmime.version>4.2.3</httpmime.version>
<javax.inject.version>1</javax.inject.version> <javax.inject.version>1</javax.inject.version>
<!-- Test Dependencies --> <!-- Test Dependencies -->
...@@ -111,11 +110,6 @@ ...@@ -111,11 +110,6 @@
<artifactId>gson</artifactId> <artifactId>gson</artifactId>
<version>${gson.version}</version> <version>${gson.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpmime</artifactId>
<version>${httpmime.version}</version>
</dependency>
<dependency> <dependency>
<groupId>junit</groupId> <groupId>junit</groupId>
......
...@@ -20,10 +20,6 @@ ...@@ -20,10 +20,6 @@
<groupId>com.google.code.gson</groupId> <groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId> <artifactId>gson</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpmime</artifactId>
</dependency>
<dependency> <dependency>
<groupId>com.google.android</groupId> <groupId>com.google.android</groupId>
<artifactId>android</artifactId> <artifactId>android</artifactId>
......
...@@ -3,64 +3,81 @@ package retrofit.http; ...@@ -3,64 +3,81 @@ package retrofit.http;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import org.apache.http.entity.mime.MIME; import java.util.LinkedHashMap;
import org.apache.http.entity.mime.MultipartEntity; import java.util.Map;
import org.apache.http.entity.mime.content.AbstractContentBody; import java.util.UUID;
import retrofit.http.mime.TypedFile;
import retrofit.http.mime.TypedOutput; import retrofit.http.mime.TypedOutput;
final class MultipartTypedOutput implements TypedOutput { final class MultipartTypedOutput implements TypedOutput {
// TODO implement our own Multipart logic instead! final Map<String, TypedOutput> parts = new LinkedHashMap<String, TypedOutput>();
final MultipartEntity cheat = new MultipartEntity(); private final String boundary;
MultipartTypedOutput() {
boundary = UUID.randomUUID().toString();
}
void addPart(String name, TypedOutput body) { void addPart(String name, TypedOutput body) {
cheat.addPart(name, new TypedOutputBody(body)); if (name == null) {
throw new NullPointerException("Part name must not be null.");
}
if (body == null) {
throw new NullPointerException("Part body must not be null.");
}
parts.put(name, body);
} }
@Override public String mimeType() { @Override public String mimeType() {
return cheat.getContentType().getValue(); return "multipart/form-data; boundary=" + boundary;
} }
@Override public long length() { @Override public long length() {
return cheat.getContentLength(); return -1;
} }
@Override public void writeTo(OutputStream out) throws IOException { @Override public void writeTo(OutputStream out) throws IOException {
cheat.writeTo(out); boolean first = true;
} for (Map.Entry<String, TypedOutput> part : parts.entrySet()) {
writeBoundary(out, boundary, first, false);
/** Adapts {@link org.apache.http.entity.mime.content.ContentBody} to {@link TypedOutput}. */ writePart(out, part);
static class TypedOutputBody extends AbstractContentBody { first = false;
final TypedOutput typedBytes;
TypedOutputBody(TypedOutput typedBytes) {
super(typedBytes.mimeType());
this.typedBytes = typedBytes;
} }
writeBoundary(out, boundary, false, true);
}
@Override public long getContentLength() { private static void writeBoundary(OutputStream out, String boundary, boolean first, boolean last)
return typedBytes.length(); throws IOException {
StringBuilder sb = new StringBuilder();
if (!first) {
sb.append("\r\n");
} }
sb.append("--");
@Override public String getFilename() { sb.append(boundary);
return null; if (last) {
sb.append("--");
} else {
sb.append("\r\n");
} }
out.write(sb.toString().getBytes("UTF-8"));
}
@Override public String getCharset() { private static void writePart(OutputStream out, Map.Entry<String, TypedOutput> part)
return null; throws IOException {
} String name = part.getKey();
TypedOutput value = part.getValue();
@Override public String getTransferEncoding() { StringBuilder headers = new StringBuilder();
return MIME.ENC_BINARY; headers.append("Content-Disposition: form-data; name=\"");
headers.append(name);
if (value instanceof TypedFile) {
headers.append("; filename=\"");
headers.append(((TypedFile) value).file().getName());
} }
headers.append("\"\r\nContent-Type: ");
headers.append(value.mimeType());
headers.append("\r\nContent-Transfer-Encoding: binary\r\n\r\n");
out.write(headers.toString().getBytes("UTF-8"));
@Override public void writeTo(OutputStream out) throws IOException { value.writeTo(out);
// Note: We probably want to differentiate I/O errors that occur while reading a file from
// network errors. Network operations can be retried. File operations will probably continue
// to fail.
//
// In the case of photo uploads, we at least check that the file exists before we even try to
// upload it.
typedBytes.writeTo(out);
}
} }
} }
...@@ -8,10 +8,10 @@ import java.lang.reflect.Method; ...@@ -8,10 +8,10 @@ import java.lang.reflect.Method;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import org.apache.http.entity.mime.FormBodyPart;
import org.apache.http.entity.mime.HttpMultipart;
import org.junit.Test; import org.junit.Test;
import retrofit.http.client.Request; import retrofit.http.client.Request;
import retrofit.http.mime.TypedOutput; import retrofit.http.mime.TypedOutput;
...@@ -20,7 +20,6 @@ import retrofit.http.mime.TypedString; ...@@ -20,7 +20,6 @@ import retrofit.http.mime.TypedString;
import static org.fest.assertions.api.Assertions.assertThat; import static org.fest.assertions.api.Assertions.assertThat;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static retrofit.http.MultipartTypedOutput.TypedOutputBody;
import static retrofit.http.RestMethodInfo.NO_SINGLE_ENTITY; import static retrofit.http.RestMethodInfo.NO_SINGLE_ENTITY;
public class RequestBuilderTest { public class RequestBuilderTest {
...@@ -76,7 +75,7 @@ public class RequestBuilderTest { ...@@ -76,7 +75,7 @@ public class RequestBuilderTest {
assertThat(request.getUrl()).isEqualTo("http://example.com/foo/bar/pong/?kit=kat&riff=raff"); assertThat(request.getUrl()).isEqualTo("http://example.com/foo/bar/pong/?kit=kat&riff=raff");
assertThat(request.getBody()).isNull(); assertThat(request.getBody()).isNull();
} }
@Test public void getWithPathAndQueryQuestionMarkParam() throws Exception { @Test public void getWithPathAndQueryQuestionMarkParam() throws Exception {
Request request = new Helper() // Request request = new Helper() //
.setMethod("GET") // .setMethod("GET") //
...@@ -90,7 +89,7 @@ public class RequestBuilderTest { ...@@ -90,7 +89,7 @@ public class RequestBuilderTest {
assertThat(request.getUrl()).isEqualTo("http://example.com/foo/bar/pong%3F/?kit=kat%3F"); assertThat(request.getUrl()).isEqualTo("http://example.com/foo/bar/pong%3F/?kit=kat%3F");
assertThat(request.getBody()).isNull(); assertThat(request.getBody()).isNull();
} }
@Test public void getWithPathAndQueryAmpersandParam() throws Exception { @Test public void getWithPathAndQueryAmpersandParam() throws Exception {
Request request = new Helper() // Request request = new Helper() //
.setMethod("GET") // .setMethod("GET") //
...@@ -104,7 +103,7 @@ public class RequestBuilderTest { ...@@ -104,7 +103,7 @@ public class RequestBuilderTest {
assertThat(request.getUrl()).isEqualTo("http://example.com/foo/bar/pong%26/?kit=kat%26"); assertThat(request.getUrl()).isEqualTo("http://example.com/foo/bar/pong%26/?kit=kat%26");
assertThat(request.getBody()).isNull(); assertThat(request.getBody()).isNull();
} }
@Test public void getWithPathAndQueryHashParam() throws Exception { @Test public void getWithPathAndQueryHashParam() throws Exception {
Request request = new Helper() // Request request = new Helper() //
.setMethod("GET") // .setMethod("GET") //
...@@ -222,18 +221,18 @@ public class RequestBuilderTest { ...@@ -222,18 +221,18 @@ public class RequestBuilderTest {
assertThat(request.getHeaders()).isEmpty(); assertThat(request.getHeaders()).isEmpty();
assertThat(request.getUrl()).isEqualTo("http://example.com/foo/bar/"); assertThat(request.getUrl()).isEqualTo("http://example.com/foo/bar/");
HttpMultipart body = TestingUtils.extractEntity(request.getBody()); MultipartTypedOutput body = (MultipartTypedOutput) request.getBody();
assertThat(body.getBodyParts()).hasSize(2); assertThat(body.parts).hasSize(2);
Iterator<Map.Entry<String, TypedOutput>> iterator = body.parts.entrySet().iterator();
FormBodyPart part1 = (FormBodyPart) body.getBodyParts().get(0); Map.Entry<String, TypedOutput> one = iterator.next();
assertThat(part1.getName()).isEqualTo("ping"); assertThat(one.getKey()).isEqualTo("ping");
TypedOutputBody body1 = (TypedOutputBody) part1.getBody(); assertTypedBytes(one.getValue(), "pong");
assertTypedBytes(body1.typedBytes, "pong");
FormBodyPart part2 = (FormBodyPart) body.getBodyParts().get(1); Map.Entry<String, TypedOutput> two = iterator.next();
assertThat(part2.getName()).isEqualTo("kit"); assertThat(two.getKey()).isEqualTo("kit");
TypedOutputBody body2 = (TypedOutputBody) part2.getBody(); assertTypedBytes(two.getValue(), "kat");
assertTypedBytes(body2.typedBytes, "kat");
} }
@Test public void simpleHeaders() throws Exception { @Test public void simpleHeaders() throws Exception {
...@@ -325,7 +324,8 @@ public class RequestBuilderTest { ...@@ -325,7 +324,8 @@ public class RequestBuilderTest {
if (singleEntityArgumentIndex != NO_SINGLE_ENTITY) { if (singleEntityArgumentIndex != NO_SINGLE_ENTITY) {
throw new IllegalStateException("Single entity param already added."); throw new IllegalStateException("Single entity param already added.");
} }
singleEntityArgumentIndex = namedParams.size(); // Relying on the fact that this is already less one. // Relying on the fact that this is already less one.
singleEntityArgumentIndex = namedParams.size();
namedParams.add(null); namedParams.add(null);
args.add(value); args.add(value);
return this; return this;
...@@ -377,7 +377,7 @@ public class RequestBuilderTest { ...@@ -377,7 +377,7 @@ public class RequestBuilderTest {
methodInfo.isMultipart = isMultipart; methodInfo.isMultipart = isMultipart;
methodInfo.loaded = true; methodInfo.loaded = true;
return new RequestBuilder(GSON) return new RequestBuilder(GSON) //
.setApiUrl(url) .setApiUrl(url)
.setHeaders(headers) .setHeaders(headers)
.setArgs(args.toArray(new Object[args.size()])) .setArgs(args.toArray(new Object[args.size()]))
......
// Copyright 2013 Square, Inc. // Copyright 2013 Square, Inc.
package retrofit.http; package retrofit.http;
import java.lang.reflect.Field;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.Map; import java.util.Map;
import org.apache.http.entity.mime.HttpMultipart;
import org.apache.http.entity.mime.MultipartEntity;
import retrofit.http.mime.TypedOutput; import retrofit.http.mime.TypedOutput;
import static org.fest.assertions.api.Assertions.assertThat; import static org.fest.assertions.api.Assertions.assertThat;
...@@ -28,17 +25,6 @@ public abstract class TestingUtils { ...@@ -28,17 +25,6 @@ public abstract class TestingUtils {
return typedOutput; return typedOutput;
} }
public static HttpMultipart extractEntity(TypedOutput output)
throws NoSuchFieldException, IllegalAccessException {
if (!(output instanceof MultipartTypedOutput)) {
throw new IllegalArgumentException("TypedOutput was not a MultipartTypedOutput.");
}
MultipartEntity entity = ((MultipartTypedOutput) output).cheat;
Field httpMultipartField = MultipartEntity.class.getDeclaredField("multipart");
httpMultipartField.setAccessible(true);
return (HttpMultipart) httpMultipartField.get(entity);
}
public static void assertMultipart(TypedOutput typedOutput) { public static void assertMultipart(TypedOutput typedOutput) {
assertThat(typedOutput).isInstanceOf(MultipartTypedOutput.class); assertThat(typedOutput).isInstanceOf(MultipartTypedOutput.class);
} }
......
...@@ -66,9 +66,8 @@ public class UrlConnectionClientTest { ...@@ -66,9 +66,8 @@ public class UrlConnectionClientTest {
assertThat(connection.getRequestMethod()).isEqualTo("POST"); assertThat(connection.getRequestMethod()).isEqualTo("POST");
assertThat(connection.getURL().toString()).isEqualTo(HOST + "/that/"); assertThat(connection.getURL().toString()).isEqualTo(HOST + "/that/");
assertThat(connection.getRequestProperties()).hasSize(2); assertThat(connection.getRequestProperties()).hasSize(1);
assertThat(connection.getRequestProperty("Content-Type")).startsWith("multipart/form-data;"); assertThat(connection.getRequestProperty("Content-Type")).startsWith("multipart/form-data;");
assertThat(connection.getRequestProperty("Content-Length")).isNotNull();
assertThat(connection.getOutputStream().toByteArray().length).isGreaterThan(0); assertThat(connection.getOutputStream().toByteArray().length).isGreaterThan(0);
} }
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册