added new annotation @PartParam for multipart form support

This commit is contained in:
Adrian Cole 2010-06-09 17:00:18 -07:00
parent fae1a1930e
commit ab5e8b3ab4
10 changed files with 285 additions and 94 deletions

View File

@ -18,12 +18,6 @@
*/
package org.jclouds.blobstore.binders;
import static com.google.common.base.Preconditions.checkNotNull;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.InputStream;
import javax.ws.rs.core.HttpHeaders;
import org.jclouds.blobstore.domain.Blob;
@ -32,9 +26,6 @@ import org.jclouds.http.MultipartForm;
import org.jclouds.http.MultipartForm.Part;
import org.jclouds.rest.Binder;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.Multimap;
/**
*
* @author Adrian Cole
@ -45,36 +36,14 @@ public class BindBlobToMultipartForm implements Binder {
public void bindToRequest(HttpRequest request, Object payload) {
Blob object = (Blob) payload;
File file = new File(object.getMetadata().getName());
Multimap<String, String> partHeaders = ImmutableMultimap.of("Content-Disposition", String
.format("form-data; name=\"%s\"; filename=\"%s\"", file.getName(), file.getName()),
HttpHeaders.CONTENT_TYPE, checkNotNull(object.getMetadata().getContentType(),
"object.metadata.contentType()"));
Object data = checkNotNull(object.getPayload(), "object.getPayload()").getRawContent();
Part part;
try {
if (data instanceof byte[]) {
part = new Part(partHeaders, (byte[]) data);
} else if (data instanceof String) {
part = new Part(partHeaders, (String) data);
} else if (data instanceof File) {
part = new Part(partHeaders, (File) data);
} else if (data instanceof InputStream) {
part = new Part(partHeaders, (InputStream) data, object.getContentLength());
} else {
throw new IllegalArgumentException("type of part not supported: "
+ data.getClass().getCanonicalName() + "; " + object);
}
} catch (FileNotFoundException e) {
throw new IllegalArgumentException("file for part not found: " + object);
}
Part part = Part.create(object.getMetadata().getName(), object.getPayload(), object
.getMetadata().getContentType());
MultipartForm form = new MultipartForm(BOUNDARY, part);
request.setPayload(form.getData());
request.getHeaders().put(HttpHeaders.CONTENT_TYPE,
"multipart/form-data; boundary=" + BOUNDARY);
request.getHeaders().put(HttpHeaders.CONTENT_LENGTH, form.getSize() + "");
}
}

View File

@ -62,7 +62,7 @@ public class BindBlobToMultipartFormTest {
public void testSinglePart() throws IOException {
assertEquals(EXPECTS.length(), 131);
assertEquals(EXPECTS.length(), 113);
BindBlobToMultipartForm binder = new BindBlobToMultipartForm();
@ -71,7 +71,7 @@ public class BindBlobToMultipartFormTest {
assertEquals(Utils.toStringAndClose((InputStream) request.getPayload().getRawContent()),
EXPECTS);
assertEquals(request.getFirstHeaderOrNull(HttpHeaders.CONTENT_LENGTH), 131 + "");
assertEquals(request.getFirstHeaderOrNull(HttpHeaders.CONTENT_LENGTH), 113 + "");
assertEquals(request.getFirstHeaderOrNull(HttpHeaders.CONTENT_TYPE),
"multipart/form-data; boundary=" + BOUNDRY);
@ -80,7 +80,7 @@ public class BindBlobToMultipartFormTest {
private static void addData(String boundary, String data, StringBuilder builder) {
builder.append(boundary).append("\r\n");
builder.append("Content-Disposition").append(": ").append(
"form-data; name=\"hello\"; filename=\"hello\"").append("\r\n");
"form-data; name=\"hello\"").append("\r\n");
builder.append("Content-Type").append(": ").append("text/plain").append("\r\n");
builder.append("\r\n");
builder.append(data).append("\r\n");

View File

@ -18,18 +18,24 @@
*/
package org.jclouds.http;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import static com.google.common.base.Preconditions.checkNotNull;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.LinkedHashMap;
import java.util.Map.Entry;
import org.jclouds.util.InputStreamChain;
import org.jclouds.util.Utils;
import javax.annotation.Nullable;
import javax.ws.rs.core.HttpHeaders;
import org.jclouds.http.payloads.FilePayload;
import org.jclouds.util.InputStreamChain;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
/**
*
@ -57,9 +63,9 @@ public class MultipartForm {
}
private void addData(Part part) {
chain.addInputStream(part.getData());
chain.addInputStream(part.getContent());
chain.addAsInputStream(rn);
size += part.getSize() + rn.length();
size += part.calculateSize() + rn.length();
}
private void addHeaders(String boundaryrn, Part part) {
@ -83,39 +89,112 @@ public class MultipartForm {
this("__redrose__", parts);
}
public static class Part {
public static class Part implements Payload {
private final Multimap<String, String> headers;
private final InputStream data;
private final long size;
private final Payload delegate;
public Part(Multimap<String, String> headers, InputStream data, long size) {
this.headers = headers;
this.data = data;
this.size = size;
private static class PartMap extends LinkedHashMap<String, String> {
/** The serialVersionUID */
private static final long serialVersionUID = -287387556008320212L;
static PartMap create(String name) {
PartMap map = new PartMap();
map.put("Content-Disposition", String.format("form-data; name=\"%s\"", checkNotNull(
name, "name")));
return map;
}
static PartMap create(String name, String filename) {
PartMap map = new PartMap();
map.put("Content-Disposition", String.format("form-data; name=\"%s\"; filename=\"%s\"",
checkNotNull(name, "name"), checkNotNull(filename, "filename")));
return map;
}
PartMap contentType(@Nullable String type) {
if (type != null)
put(HttpHeaders.CONTENT_TYPE, checkNotNull(type, "type"));
return this;
}
}
public Part(Multimap<String, String> headers, String data) {
this(headers, Utils.toInputStream(data), data.length());
private Part(PartMap map, Payload delegate) {
this.delegate = checkNotNull(delegate, "delegate");
this.headers = ImmutableMultimap.copyOf(Multimaps.forMap((checkNotNull(map, "headers"))));
}
public Part(Multimap<String, String> headers, File data) throws FileNotFoundException {
this(headers, new FileInputStream(data), data.length());
public static Part create(String name, String value) {
return new Part(PartMap.create(name), Payloads.newStringPayload(value));
}
public Part(Multimap<String, String> headers, byte[] data) {
this(headers, new ByteArrayInputStream(data), data.length);
public static Part create(String name, Payload delegate, String contentType) {
return new Part(PartMap.create(name).contentType(contentType), delegate);
}
public static Part create(String name, FilePayload delegate, String contentType) {
return new Part(PartMap.create(name, delegate.getRawContent().getName()).contentType(
contentType), delegate);
}
public Multimap<String, String> getHeaders() {
return headers;
}
public InputStream getData() {
return data;
@Override
public Long calculateSize() {
return delegate.calculateSize();
}
public long getSize() {
return size;
@Override
public InputStream getContent() {
return delegate.getContent();
}
@Override
public Object getRawContent() {
return delegate.getContent();
}
@Override
public boolean isRepeatable() {
return delegate.isRepeatable();
}
@Override
public void writeTo(OutputStream outstream) throws IOException {
delegate.writeTo(outstream);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((delegate == null) ? 0 : delegate.hashCode());
result = prime * result + ((headers == null) ? 0 : headers.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Part other = (Part) obj;
if (delegate == null) {
if (other.delegate != null)
return false;
} else if (!delegate.equals(other.delegate))
return false;
if (headers == null) {
if (other.headers != null)
return false;
} else if (!headers.equals(other.headers))
return false;
return true;
}
}

View File

@ -24,9 +24,7 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import org.jclouds.http.MultipartForm.Part;
import com.google.common.base.Function;
import javax.ws.rs.core.MediaType;
/**
* Designates that this parameter will be bound to a multipart form.
@ -36,17 +34,7 @@ import com.google.common.base.Function;
@Target(PARAMETER)
@Retention(RUNTIME)
public @interface PartParam {
String name();
public static class ALREADY_PART implements Function<Object, Part> {
@Override
public Part apply(Object from) {
return Part.class.cast(from);
}
};
/**
* how to convert this to a part.
*/
Class<? extends Function<Object, Part>> value() default ALREADY_PART.class;
String contentType() default MediaType.TEXT_PLAIN;
}

View File

@ -62,6 +62,7 @@ import org.jclouds.http.HttpRequestFilter;
import org.jclouds.http.HttpResponse;
import org.jclouds.http.HttpUtils;
import org.jclouds.http.MultipartForm;
import org.jclouds.http.Payloads;
import org.jclouds.http.MultipartForm.Part;
import org.jclouds.http.functions.CloseContentAndReturn;
import org.jclouds.http.functions.ParseSax;
@ -102,7 +103,6 @@ import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.LinkedListMultimap;
@ -195,8 +195,7 @@ public class RestAnnotationProcessor<T> {
@Override
public Part apply(Entry<String, String> from) {
return new Part(ImmutableMultimap.of("Content-Disposition", String.format(
"form-data; name=\"%s\"", from.getKey())), from.getValue());
return Part.create(from.getKey(), from.getValue());
}
};
@ -1012,8 +1011,9 @@ public class RestAnnotationProcessor<T> {
.get(method);
for (Entry<Integer, Set<Annotation>> entry : indexToPartParam.entrySet()) {
for (Annotation key : entry.getValue()) {
PartParam extractor = (PartParam) key;
Part part = injector.getInstance(extractor.value()).apply(args[entry.getKey()]);
PartParam param = (PartParam) key;
Part part = Part.create(param.name(), Payloads.newPayload(args[entry.getKey()]), param
.contentType());
parts.add(part);
}
}

View File

@ -18,19 +18,24 @@
*/
package org.jclouds.http;
import static org.easymock.EasyMock.expect;
import static org.easymock.classextension.EasyMock.createMock;
import static org.easymock.classextension.EasyMock.replay;
import static org.testng.Assert.assertEquals;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import org.jclouds.http.MultipartForm.Part;
import org.jclouds.http.payloads.FilePayload;
import org.jclouds.http.payloads.StringPayload;
import org.jclouds.util.Utils;
import org.testng.annotations.Test;
import com.google.common.collect.ImmutableMultimap;
/**
* Tests parsing of a request
*
@ -54,10 +59,47 @@ public class MultipartFormTest {
assertEquals(multipartForm.getSize(), 199);
}
public static class MockFilePayload extends FilePayload {
private final StringPayload realPayload;
public MockFilePayload(String content) {
super(createMockFile(content));
this.realPayload = Payloads.newStringPayload(content);
}
private static File createMockFile(String content) {
File file = createMock(File.class);
expect(file.exists()).andReturn(true);
expect(file.getName()).andReturn("testfile.txt");
replay(file);
return file;
}
@Override
public Long calculateSize() {
return realPayload.calculateSize();
}
@Override
public InputStream getContent() {
return realPayload.getContent();
}
@Override
public boolean isRepeatable() {
return realPayload.isRepeatable();
}
@Override
public void writeTo(OutputStream outstream) throws IOException {
realPayload.writeTo(outstream);
}
}
private Part newPart(String data) {
return new MultipartForm.Part(ImmutableMultimap.of("Content-Disposition",
"form-data; name=\"file\"; filename=\"testfile.txt\"", HttpHeaders.CONTENT_TYPE,
MediaType.TEXT_PLAIN), data);
return Part.create("file", new MockFilePayload(data), MediaType.TEXT_PLAIN);
}
private void addData(String boundary, String data, StringBuilder builder) {

View File

@ -23,6 +23,7 @@ import static com.google.common.util.concurrent.MoreExecutors.sameThreadExecutor
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNull;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
@ -98,6 +99,7 @@ import org.jclouds.rest.annotations.MapPayloadParam;
import org.jclouds.rest.annotations.MatrixParams;
import org.jclouds.rest.annotations.OverrideRequestFilters;
import org.jclouds.rest.annotations.ParamParser;
import org.jclouds.rest.annotations.PartParam;
import org.jclouds.rest.annotations.QueryParams;
import org.jclouds.rest.annotations.RequestFilters;
import org.jclouds.rest.annotations.ResponseParser;
@ -114,6 +116,7 @@ import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import com.google.common.base.Charsets;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
@ -123,6 +126,7 @@ import com.google.common.collect.Iterables;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.Multimap;
import com.google.common.io.Files;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.inject.AbstractModule;
import com.google.inject.ConfigurationException;
@ -612,6 +616,115 @@ public class RestAnnotationProcessorTest {
assertEquals(httpMethod.getPayload().toString(), expected);
}
@Endpoint(Localhost.class)
static interface TestMultipartForm {
@POST
public void withStringPart(@PartParam(name = "fooble") String path);
@POST
public void withParamStringPart(@FormParam("name") String name,
@PartParam(name = "file") String path);
@POST
public void withParamFilePart(@FormParam("name") String name,
@PartParam(name = "file") File path);
@POST
public void withParamFileBinaryPart(@FormParam("name") String name,
@PartParam(name = "file", contentType = MediaType.APPLICATION_OCTET_STREAM) File path);
}
public void testMultipartWithStringPart() throws SecurityException, NoSuchMethodException,
IOException {
Method method = TestMultipartForm.class.getMethod("withStringPart", String.class);
GeneratedHttpRequest<TestMultipartForm> httpRequest = factory(TestMultipartForm.class)
.createRequest(method, "foobledata");
assertRequestLineEquals(httpRequest, "POST http://localhost:9999 HTTP/1.1");
assertHeadersEqual(httpRequest,
"Content-Length: 119\nContent-Type: multipart/form-data; boundary=--JCLOUDS--\n");
assertPayloadEquals(httpRequest,//
"----JCLOUDS--\r\n" + //
"Content-Disposition: form-data; name=\"fooble\"\r\n" + //
"Content-Type: text/plain\r\n" + //
"\r\n" + //
"foobledata\r\n" + //
"----JCLOUDS----\r\n");
}
public void testMultipartWithParamStringPart() throws SecurityException, NoSuchMethodException,
IOException {
Method method = TestMultipartForm.class.getMethod("withParamStringPart", String.class,
String.class);
GeneratedHttpRequest<TestMultipartForm> httpRequest = factory(TestMultipartForm.class)
.createRequest(method, "name", "foobledata");
assertRequestLineEquals(httpRequest, "POST http://localhost:9999 HTTP/1.1");
assertHeadersEqual(httpRequest,
"Content-Length: 185\nContent-Type: multipart/form-data; boundary=--JCLOUDS--\n");
assertPayloadEquals(httpRequest,//
"----JCLOUDS--\r\n" + //
"Content-Disposition: form-data; name=\"name\"\r\n" + //
"\r\n" + //
"name\r\n" + // /
"----JCLOUDS--\r\n" + //
"Content-Disposition: form-data; name=\"file\"\r\n" + //
"Content-Type: text/plain\r\n" + //
"\r\n" + //
"foobledata\r\n" + //
"----JCLOUDS----\r\n");
}
public void testMultipartWithParamFilePart() throws SecurityException, NoSuchMethodException,
IOException {
Method method = TestMultipartForm.class.getMethod("withParamFilePart", String.class,
File.class);
File file = File.createTempFile("foo", "bar");
Files.append("foobledata", file, Charsets.UTF_8);
file.deleteOnExit();
GeneratedHttpRequest<TestMultipartForm> httpRequest = factory(TestMultipartForm.class)
.createRequest(method, "name", file);
assertRequestLineEquals(httpRequest, "POST http://localhost:9999 HTTP/1.1");
assertHeadersEqual(httpRequest,
"Content-Length: 185\nContent-Type: multipart/form-data; boundary=--JCLOUDS--\n");
assertPayloadEquals(httpRequest,//
"----JCLOUDS--\r\n" + //
"Content-Disposition: form-data; name=\"name\"\r\n" + //
"\r\n" + //
"name\r\n" + // /
"----JCLOUDS--\r\n" + //
"Content-Disposition: form-data; name=\"file\"\r\n" + //
"Content-Type: text/plain\r\n" + //
"\r\n" + //
"foobledata\r\n" + //
"----JCLOUDS----\r\n");
}
public void testMultipartWithParamFileBinaryPart() throws SecurityException,
NoSuchMethodException, IOException {
Method method = TestMultipartForm.class.getMethod("withParamFileBinaryPart", String.class,
File.class);
File file = File.createTempFile("foo", "bar");
Files.write(new byte[] { 17, 26, 39, 40, 50 }, file);
file.deleteOnExit();
GeneratedHttpRequest<TestMultipartForm> httpRequest = factory(TestMultipartForm.class)
.createRequest(method, "name", file);
assertRequestLineEquals(httpRequest, "POST http://localhost:9999 HTTP/1.1");
assertHeadersEqual(httpRequest,
"Content-Length: 194\nContent-Type: multipart/form-data; boundary=--JCLOUDS--\n");
assertPayloadEquals(httpRequest,//
"----JCLOUDS--\r\n" + //
"Content-Disposition: form-data; name=\"name\"\r\n" + //
"\r\n" + //
"name\r\n" + // /
"----JCLOUDS--\r\n" + //
"Content-Disposition: form-data; name=\"file\"\r\n" + //
"Content-Type: application/octet-stream\r\n" + //
"\r\n" + //
"'(2\r\n" + //
"----JCLOUDS----\r\n");
}
@Endpoint(Localhost.class)
public class TestPut {
@PUT

View File

@ -164,7 +164,7 @@ public class PCSAsyncClientTest extends RestClientTest<PCSAsyncClient> {
assertRequestLineEquals(httpMethod, "POST http://localhost/mycontainer/contents HTTP/1.1");
assertHeadersEqual(httpMethod,
"Content-Length: 131\nContent-Type: multipart/form-data; boundary=--JCLOUDS--\n");
"Content-Length: 113\nContent-Type: multipart/form-data; boundary=--JCLOUDS--\n");
assertPayloadEquals(httpMethod, BindBlobToMultipartFormTest.EXPECTS);
assertResponseParserClassEquals(method, httpMethod,

View File

@ -92,14 +92,14 @@
<category name="jclouds.headers">
<priority value="DEBUG" />
<appender-ref ref="ASYNCWIRE" />
</category><!--
</category>
<category name="jclouds.wire">
<priority value="DEBUG" />
<appender-ref ref="ASYNCWIRE" />
</category>
--><!-- ======================= -->
<!-- ======================= -->
<!-- Setup the Root category -->
<!-- ======================= -->

View File

@ -84,10 +84,10 @@ public class SDNAsyncClientTest extends RestClientTest<SDNAsyncClient> {
httpMethod,
"POST http://uploader/Upload.ashx?output=json&destFolderPath=adriansmovies&uploadToken=token HTTP/1.1");
assertHeadersEqual(httpMethod,
"Content-Length: 131\nContent-Type: multipart/form-data; boundary=--JCLOUDS--\n");
"Content-Length: 113\nContent-Type: multipart/form-data; boundary=--JCLOUDS--\n");
StringBuffer expects = new StringBuffer();
expects.append("----JCLOUDS--\r\n");
expects.append("Content-Disposition: form-data; name=\"hello\"; filename=\"hello\"\r\n");
expects.append("Content-Disposition: form-data; name=\"hello\"\r\n");
expects.append("Content-Type: text/plain\r\n\r\n");
expects.append("hello\r\n");
expects.append("----JCLOUDS----\r\n");