diff --git a/blobstore/src/main/java/org/jclouds/blobstore/binders/BindBlobToMultipartForm.java b/blobstore/src/main/java/org/jclouds/blobstore/binders/BindBlobToMultipartForm.java index a036d8a8d5..923c86f36e 100644 --- a/blobstore/src/main/java/org/jclouds/blobstore/binders/BindBlobToMultipartForm.java +++ b/blobstore/src/main/java/org/jclouds/blobstore/binders/BindBlobToMultipartForm.java @@ -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 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() + ""); } } diff --git a/blobstore/src/test/java/org/jclouds/blobstore/binders/BindBlobToMultipartFormTest.java b/blobstore/src/test/java/org/jclouds/blobstore/binders/BindBlobToMultipartFormTest.java index 25f9977ea5..7506e2cdbd 100644 --- a/blobstore/src/test/java/org/jclouds/blobstore/binders/BindBlobToMultipartFormTest.java +++ b/blobstore/src/test/java/org/jclouds/blobstore/binders/BindBlobToMultipartFormTest.java @@ -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"); diff --git a/core/src/main/java/org/jclouds/http/MultipartForm.java b/core/src/main/java/org/jclouds/http/MultipartForm.java index cf2e82a1fe..2987e87e43 100644 --- a/core/src/main/java/org/jclouds/http/MultipartForm.java +++ b/core/src/main/java/org/jclouds/http/MultipartForm.java @@ -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 headers; - private final InputStream data; - private final long size; + private final Payload delegate; - public Part(Multimap headers, InputStream data, long size) { - this.headers = headers; - this.data = data; - this.size = size; + private static class PartMap extends LinkedHashMap { + + /** 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 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 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 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 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; } } diff --git a/core/src/main/java/org/jclouds/rest/annotations/PartParam.java b/core/src/main/java/org/jclouds/rest/annotations/PartParam.java index 4b3bf877fe..0e50df7831 100644 --- a/core/src/main/java/org/jclouds/rest/annotations/PartParam.java +++ b/core/src/main/java/org/jclouds/rest/annotations/PartParam.java @@ -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 { - - @Override - public Part apply(Object from) { - return Part.class.cast(from); - } - }; - - /** - * how to convert this to a part. - */ - Class> value() default ALREADY_PART.class; + String contentType() default MediaType.TEXT_PLAIN; } diff --git a/core/src/main/java/org/jclouds/rest/internal/RestAnnotationProcessor.java b/core/src/main/java/org/jclouds/rest/internal/RestAnnotationProcessor.java index 61c634dcc3..cb5cab49df 100755 --- a/core/src/main/java/org/jclouds/rest/internal/RestAnnotationProcessor.java +++ b/core/src/main/java/org/jclouds/rest/internal/RestAnnotationProcessor.java @@ -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 { @Override public Part apply(Entry 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 { .get(method); for (Entry> 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); } } diff --git a/core/src/test/java/org/jclouds/http/MultipartFormTest.java b/core/src/test/java/org/jclouds/http/MultipartFormTest.java index 352075cec8..92690eda28 100644 --- a/core/src/test/java/org/jclouds/http/MultipartFormTest.java +++ b/core/src/test/java/org/jclouds/http/MultipartFormTest.java @@ -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) { diff --git a/core/src/test/java/org/jclouds/rest/internal/RestAnnotationProcessorTest.java b/core/src/test/java/org/jclouds/rest/internal/RestAnnotationProcessorTest.java index af4b7a58cf..8e8a396691 100755 --- a/core/src/test/java/org/jclouds/rest/internal/RestAnnotationProcessorTest.java +++ b/core/src/test/java/org/jclouds/rest/internal/RestAnnotationProcessorTest.java @@ -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 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 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 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 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 diff --git a/mezeo/pcs2/core/src/test/java/org/jclouds/mezeo/pcs2/PCSAsyncClientTest.java b/mezeo/pcs2/core/src/test/java/org/jclouds/mezeo/pcs2/PCSAsyncClientTest.java index 66f9acfb98..eafaec96bd 100644 --- a/mezeo/pcs2/core/src/test/java/org/jclouds/mezeo/pcs2/PCSAsyncClientTest.java +++ b/mezeo/pcs2/core/src/test/java/org/jclouds/mezeo/pcs2/PCSAsyncClientTest.java @@ -164,7 +164,7 @@ public class PCSAsyncClientTest extends RestClientTest { 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, diff --git a/mezeo/pcs2/core/src/test/resources/log4j.xml b/mezeo/pcs2/core/src/test/resources/log4j.xml index 20fef23aed..b6204a09e3 100755 --- a/mezeo/pcs2/core/src/test/resources/log4j.xml +++ b/mezeo/pcs2/core/src/test/resources/log4j.xml @@ -92,14 +92,14 @@ - + diff --git a/nirvanix/sdn/core/src/test/java/org/jclouds/nirvanix/sdn/SDNAsyncClientTest.java b/nirvanix/sdn/core/src/test/java/org/jclouds/nirvanix/sdn/SDNAsyncClientTest.java index 6326626a5f..f253d1ce11 100644 --- a/nirvanix/sdn/core/src/test/java/org/jclouds/nirvanix/sdn/SDNAsyncClientTest.java +++ b/nirvanix/sdn/core/src/test/java/org/jclouds/nirvanix/sdn/SDNAsyncClientTest.java @@ -84,10 +84,10 @@ public class SDNAsyncClientTest extends RestClientTest { 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");