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 e32580a3a9..372df8a61d 100644 --- a/blobstore/src/main/java/org/jclouds/blobstore/binders/BindBlobToMultipartForm.java +++ b/blobstore/src/main/java/org/jclouds/blobstore/binders/BindBlobToMultipartForm.java @@ -24,6 +24,7 @@ import org.jclouds.blobstore.domain.Blob; import org.jclouds.http.HttpRequest; import org.jclouds.http.MultipartForm; import org.jclouds.http.MultipartForm.Part; +import org.jclouds.http.MultipartForm.Part.PartOptions; import org.jclouds.rest.Binder; /** @@ -36,10 +37,10 @@ public class BindBlobToMultipartForm implements Binder { public void bindToRequest(HttpRequest request, Object payload) { Blob object = (Blob) payload; - - Part part = Part.create(object.getMetadata().getName(), object.getPayload(), object - .getMetadata().getContentType()); - + + Part part = Part.create(object.getMetadata().getName(), object.getPayload(), + new PartOptions().contentType(object.getMetadata().getContentType())); + MultipartForm form = new MultipartForm(BOUNDARY, part); request.setPayload(form.getInput()); request.getHeaders().put(HttpHeaders.CONTENT_TYPE, diff --git a/chef/src/main/java/org/jclouds/chef/ChefAsyncClient.java b/chef/src/main/java/org/jclouds/chef/ChefAsyncClient.java index dcd1deb797..0da2e05cb1 100644 --- a/chef/src/main/java/org/jclouds/chef/ChefAsyncClient.java +++ b/chef/src/main/java/org/jclouds/chef/ChefAsyncClient.java @@ -70,14 +70,15 @@ public interface ChefAsyncClient { */ @GET @Path("cookbooks") - ListenableFuture listCookbooks(); + @ResponseParser(ParseKeySetFromJson.class) + ListenableFuture> listCookbooks(); /** * @see ChefClient#createCookbook(String,File) */ @POST - @Path("cookbooks") - ListenableFuture createCookbook(@FormParam("name") String name, + @Path("name") + ListenableFuture createCookbook(@FormParam("name") String cookbookName, @PartParam(name = "file", contentType = MediaType.APPLICATION_OCTET_STREAM) File content); /** @@ -85,8 +86,44 @@ public interface ChefAsyncClient { */ @POST @Path("cookbooks") - ListenableFuture createCookbook(@FormParam("name") String name, - @PartParam(name = "file", contentType = MediaType.APPLICATION_OCTET_STREAM) byte[] content); + ListenableFuture createCookbook( + @FormParam("name") String cookbookName, + @PartParam(name = "file", contentType = MediaType.APPLICATION_OCTET_STREAM, filename = "{name}.tar.gz") byte[] content); + + /** + * @see ChefClient#updateCookbook(String,File) + */ + @PUT + @Path("cookbooks/{cookbookname}/_content") + ListenableFuture updateCookbook( + @PathParam("cookbookname") @FormParam("name") String cookbookName, + @PartParam(name = "file", contentType = MediaType.APPLICATION_OCTET_STREAM) File content); + + /** + * @see ChefClient#updateCookbook(String,byte[]) + */ + @PUT + @Path("cookbooks/{cookbookname}/_content") + ListenableFuture updateCookbook( + @PathParam("cookbookname") @FormParam("name") String cookbookName, + @PartParam(name = "file", contentType = MediaType.APPLICATION_OCTET_STREAM, filename = "{name}.tar.gz") byte[] content); + + /** + * @see ChefCookbook#deleteCookbook + */ + @DELETE + @Path("cookbooks/{cookbookname}") + @ExceptionParser(ReturnVoidOnNotFoundOr404.class) + ListenableFuture deleteCookbook(@PathParam("cookbookname") String cookbookName); + + + + /** + * @see ChefCookbook#getCookbook + */ + @GET + @Path("cookbooks/{cookbookname}") + ListenableFuture getCookbook(@PathParam("cookbookname") String cookbookName); /** * @see ChefClient#createClient diff --git a/chef/src/main/java/org/jclouds/chef/ChefClient.java b/chef/src/main/java/org/jclouds/chef/ChefClient.java index bcfdd8ec51..b743a4b61b 100644 --- a/chef/src/main/java/org/jclouds/chef/ChefClient.java +++ b/chef/src/main/java/org/jclouds/chef/ChefClient.java @@ -42,7 +42,6 @@ package org.jclouds.chef; import java.io.File; -import java.io.InputStream; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -60,11 +59,23 @@ import org.jclouds.rest.AuthorizationException; */ @Timeout(duration = 30, timeUnit = TimeUnit.SECONDS) public interface ChefClient { - String listCookbooks(); + Set listCookbooks(); - String createCookbook(String name, File content); + @Timeout(duration = 10, timeUnit = TimeUnit.MINUTES) + void createCookbook(String cookbookName, File content); - String createCookbook(String name, byte[] content); + @Timeout(duration = 10, timeUnit = TimeUnit.MINUTES) + void createCookbook(String cookbookName, byte[] content); + + @Timeout(duration = 10, timeUnit = TimeUnit.MINUTES) + void updateCookbook(String cookbookName, File content); + + @Timeout(duration = 10, timeUnit = TimeUnit.MINUTES) + void updateCookbook(String cookbookName, byte[] content); + + void deleteCookbook(String cookbookName); + + String getCookbook(String cookbookName); /** * creates a new client diff --git a/chef/src/main/java/org/jclouds/chef/filters/SignedHeaderAuth.java b/chef/src/main/java/org/jclouds/chef/filters/SignedHeaderAuth.java index a7227dab64..95f02e8597 100644 --- a/chef/src/main/java/org/jclouds/chef/filters/SignedHeaderAuth.java +++ b/chef/src/main/java/org/jclouds/chef/filters/SignedHeaderAuth.java @@ -27,6 +27,7 @@ import static com.google.common.base.Preconditions.checkArgument; import java.security.PrivateKey; import java.util.Collections; +import java.util.NoSuchElementException; import javax.annotation.Resource; import javax.inject.Inject; @@ -42,13 +43,16 @@ import org.jclouds.http.HttpException; import org.jclouds.http.HttpRequest; import org.jclouds.http.HttpRequestFilter; import org.jclouds.http.HttpUtils; +import org.jclouds.http.MultipartForm; import org.jclouds.http.Payload; import org.jclouds.http.Payloads; +import org.jclouds.http.MultipartForm.Part; import org.jclouds.http.internal.SignatureWire; import org.jclouds.logging.Logger; import org.jclouds.util.Utils; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Predicate; import com.google.common.base.Splitter; import com.google.common.base.Throwables; import com.google.common.collect.Iterables; @@ -130,7 +134,7 @@ public class SignedHeaderAuth implements HttpRequestFilter { @VisibleForTesting String hashPath(String path) { try { - return encryptionService.sha1Base64(canonicalPath(path)); + return encryptionService.sha1Base64(Utils.toInputStream(canonicalPath(path))); } catch (Exception e) { Throwables.propagateIfPossible(e); throw new HttpException("error creating sigature for path: " + path, e); @@ -151,16 +155,36 @@ public class SignedHeaderAuth implements HttpRequestFilter { String hashBody(Payload payload) { if (payload == null) return emptyStringHash; + payload = useTheFilePartIfForm(payload); checkArgument(payload != null, "payload was null"); checkArgument(payload.isRepeatable(), "payload must be repeatable: " + payload); try { - return encryptionService.sha1Base64(Utils.toStringAndClose(payload.getInput())); + return encryptionService.sha1Base64(payload.getInput()); } catch (Exception e) { Throwables.propagateIfPossible(e); throw new HttpException("error creating sigature for payload: " + payload, e); } } + private Payload useTheFilePartIfForm(Payload payload) { + if (payload instanceof MultipartForm) { + Iterable parts = MultipartForm.class.cast(payload).getParts(); + try { + payload = Iterables.find(parts, new Predicate() { + + @Override + public boolean apply(Part input) { + return "file".equals(input.getName()); + } + + }); + } catch (NoSuchElementException e) { + + } + } + return payload; + } + public String sign(String toSign) { try { byte[] encrypted = encryptionService.rsaPrivateEncrypt(toSign, privateKey); diff --git a/chef/src/main/java/org/jclouds/chef/functions/ParseErrorFromJsonOrNull.java b/chef/src/main/java/org/jclouds/chef/functions/ParseErrorFromJsonOrReturnBody.java similarity index 90% rename from chef/src/main/java/org/jclouds/chef/functions/ParseErrorFromJsonOrNull.java rename to chef/src/main/java/org/jclouds/chef/functions/ParseErrorFromJsonOrReturnBody.java index f777a055d0..b15a3946a6 100644 --- a/chef/src/main/java/org/jclouds/chef/functions/ParseErrorFromJsonOrNull.java +++ b/chef/src/main/java/org/jclouds/chef/functions/ParseErrorFromJsonOrReturnBody.java @@ -41,8 +41,8 @@ import com.google.common.base.Throwables; * @author Adrian Cole */ @Singleton -public class ParseErrorFromJsonOrNull implements Function { - Pattern pattern = Pattern.compile(".*error\": *\"([^\"]+)\".*"); +public class ParseErrorFromJsonOrReturnBody implements Function { + Pattern pattern = Pattern.compile(".*\\[\"([^\"]+)\"\\].*"); @Override public String apply(HttpResponse response) { @@ -63,9 +63,9 @@ public class ParseErrorFromJsonOrNull implements Function public String parse(String in) { Matcher matcher = pattern.matcher(in); - while (matcher.find()) { + if (matcher.find()) { return matcher.group(1); } - return null; + return in; } } \ No newline at end of file diff --git a/chef/src/main/java/org/jclouds/chef/handlers/ChefErrorHandler.java b/chef/src/main/java/org/jclouds/chef/handlers/ChefErrorHandler.java index 793cb9734b..8ac4b2433f 100644 --- a/chef/src/main/java/org/jclouds/chef/handlers/ChefErrorHandler.java +++ b/chef/src/main/java/org/jclouds/chef/handlers/ChefErrorHandler.java @@ -22,7 +22,7 @@ import javax.annotation.Resource; import javax.inject.Inject; import javax.inject.Singleton; -import org.jclouds.chef.functions.ParseErrorFromJsonOrNull; +import org.jclouds.chef.functions.ParseErrorFromJsonOrReturnBody; import org.jclouds.http.HttpCommand; import org.jclouds.http.HttpErrorHandler; import org.jclouds.http.HttpResponse; @@ -43,17 +43,16 @@ import com.google.common.io.Closeables; public class ChefErrorHandler implements HttpErrorHandler { @Resource protected Logger logger = Logger.NULL; - private final ParseErrorFromJsonOrNull errorParser; + private final ParseErrorFromJsonOrReturnBody errorParser; @Inject - ChefErrorHandler(ParseErrorFromJsonOrNull errorParser) { + ChefErrorHandler(ParseErrorFromJsonOrReturnBody errorParser) { this.errorParser = errorParser; } public void handleError(HttpCommand command, HttpResponse response) { String message = errorParser.apply(response); - Exception exception = message != null ? new HttpResponseException(command, response, message) - : new HttpResponseException(command, response); + Exception exception = new HttpResponseException(command, response, message); try { message = message != null ? message : String.format("%s -> %s", command.getRequest() .getRequestLine(), response.getStatusLine()); diff --git a/chef/src/test/java/org/jclouds/chef/ChefClientLiveTest.java b/chef/src/test/java/org/jclouds/chef/ChefClientLiveTest.java index ea14d7e97f..4e75d7298d 100644 --- a/chef/src/test/java/org/jclouds/chef/ChefClientLiveTest.java +++ b/chef/src/test/java/org/jclouds/chef/ChefClientLiveTest.java @@ -26,7 +26,6 @@ package org.jclouds.chef; import static com.google.common.base.Preconditions.checkNotNull; import static org.testng.Assert.assertNotNull; -import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -51,26 +50,37 @@ import com.google.common.io.Files; @Test(groups = "live", testName = "chef.ChefClientLiveTest") public class ChefClientLiveTest { + private static final String COOKBOOK_NAME = "mysql"; + private static final String COOKBOOK_URI = "https://s3.amazonaws.com/opscode-community/cookbook_versions/tarballs/212/original/mysql.tar.gz"; private RestContext validatorConnection; private RestContext clientConnection; + private RestContext adminConnection; private String clientKey; private String endpoint; private String validator; + private String user; + private byte[] cookbookContent; + private File cookbookFile; public static final String PREFIX = System.getProperty("user.name") + "-jcloudstest"; @BeforeClass(groups = { "live" }) public void setupClient() throws IOException { endpoint = checkNotNull(System.getProperty("jclouds.test.endpoint"), "jclouds.test.endpoint"); - validator = System.getProperty("jclouds.test.user"); + validator = System.getProperty("jclouds.test.validator"); if (validator == null || validator.equals("")) validator = "chef-validator"; + String validatorKey = System.getProperty("jclouds.test.validator.key"); + if (validatorKey == null || validatorKey.equals("")) + validatorKey = "/etc/chef/validation.pem"; + user = checkNotNull(System.getProperty("jclouds.test.user")); String keyfile = System.getProperty("jclouds.test.key"); if (keyfile == null || keyfile.equals("")) - keyfile = "/etc/chef/validation.pem"; - validatorConnection = createConnection(validator, Files.toString(new File(keyfile), + keyfile = System.getProperty("user.home") + "/chef/" + user + ".pem"; + validatorConnection = createConnection(validator, Files.toString(new File(validatorKey), Charsets.UTF_8)); + adminConnection = createConnection(user, Files.toString(new File(keyfile), Charsets.UTF_8)); } private RestContext createConnection(String identity, String key) @@ -110,24 +120,22 @@ public class ChefClientLiveTest { assertNotNull(validatorConnection.getApi().clientExists(PREFIX)); } - @Test(dependsOnMethods = "testGenerateKeyForClient") - public void testCreateCookbooks() throws Exception { + @Test + public void testCreateCookbook() throws Exception { + adminConnection.getApi().deleteCookbook(COOKBOOK_NAME); InputStream in = null; try { - in = URI - .create( - "https://s3.amazonaws.com/opscode-community/cookbook_versions/tarballs/194/original/java.tar.gz") - .toURL().openStream(); + in = URI.create(COOKBOOK_URI).toURL().openStream(); - byte[] content = ByteStreams.toByteArray(in); + cookbookContent = ByteStreams.toByteArray(in); - System.err.println(clientConnection.getApi().createCookbook("java-bytearray", content)); + cookbookFile = File.createTempFile("foo", ".tar.gz"); + Files.write(cookbookContent, cookbookFile); + cookbookFile.deleteOnExit(); - File file = File.createTempFile("foo", "bar"); - Files.write(content, file); - file.deleteOnExit(); - - System.err.println(clientConnection.getApi().createCookbook("java-file", file)); + adminConnection.getApi().createCookbook(COOKBOOK_NAME, cookbookFile); + adminConnection.getApi().deleteCookbook(COOKBOOK_NAME); + adminConnection.getApi().createCookbook(COOKBOOK_NAME, cookbookContent); } finally { if (in != null) @@ -135,9 +143,17 @@ public class ChefClientLiveTest { } } - @Test(dependsOnMethods = "testCreateCookbooks") + @Test(dependsOnMethods = "testCreateCookbook") + public void testUpdateCookbook() throws Exception { + adminConnection.getApi().updateCookbook(COOKBOOK_NAME, cookbookFile); + // TODO verify timestamp or something + adminConnection.getApi().updateCookbook(COOKBOOK_NAME, cookbookContent); + } + + @Test(dependsOnMethods = "testUpdateCookbook") public void testListCookbooks() throws Exception { - System.err.println(clientConnection.getApi().listCookbooks()); + for (String cookbook : adminConnection.getApi().listCookbooks()) + System.err.println(adminConnection.getApi().getCookbook(cookbook)); } @AfterClass(groups = { "live" }) @@ -146,5 +162,7 @@ public class ChefClientLiveTest { clientConnection.close(); if (validatorConnection != null) validatorConnection.close(); + if (adminConnection != null) + adminConnection.close(); } } diff --git a/chef/src/test/java/org/jclouds/chef/functions/ParseErrorFromJsonOrReturnBodyTest.java b/chef/src/test/java/org/jclouds/chef/functions/ParseErrorFromJsonOrReturnBodyTest.java new file mode 100644 index 0000000000..90ffe3d560 --- /dev/null +++ b/chef/src/test/java/org/jclouds/chef/functions/ParseErrorFromJsonOrReturnBodyTest.java @@ -0,0 +1,50 @@ +/** + * + * Copyright (C) 2010 Cloud Conscious, LLC. + * + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.jclouds.chef.functions; +import static org.testng.Assert.assertEquals; + +import java.io.InputStream; +import java.net.UnknownHostException; + +import org.jclouds.http.HttpResponse; +import org.jclouds.util.Utils; +import org.testng.annotations.Test; + +/** + * @author Adrian Cole + */ +@Test(groups = "unit", testName = "chef.ParseErrorFromJsonOrReturnBodyTest") +public class ParseErrorFromJsonOrReturnBodyTest { + + @Test + public void testApplyInputStreamDetails() throws UnknownHostException { + InputStream is = Utils + .toInputStream("{\"error\":[\"invalid tarball: tarball root must contain java-bytearray\"]}"); + + ParseErrorFromJsonOrReturnBody parser = new ParseErrorFromJsonOrReturnBody(); + String response = parser.apply(new HttpResponse(is)); + assertEquals(response, "invalid tarball: tarball root must contain java-bytearray"); + } + +} diff --git a/core/src/main/java/org/jclouds/encryption/EncryptionService.java b/core/src/main/java/org/jclouds/encryption/EncryptionService.java index 39825c6484..d9885d2a72 100644 --- a/core/src/main/java/org/jclouds/encryption/EncryptionService.java +++ b/core/src/main/java/org/jclouds/encryption/EncryptionService.java @@ -52,7 +52,7 @@ public interface EncryptionService { String hmacSha256Base64(String toEncode, byte[] key) throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeyException; - String sha1Base64(String toEncode) throws NoSuchAlgorithmException, NoSuchProviderException, + String sha1Base64(InputStream toEncode) throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeyException; String hmacSha1Base64(String toEncode, byte[] key) throws NoSuchAlgorithmException, diff --git a/core/src/main/java/org/jclouds/encryption/internal/JCEEncryptionService.java b/core/src/main/java/org/jclouds/encryption/internal/JCEEncryptionService.java index 3c85da8917..50f667a9f3 100644 --- a/core/src/main/java/org/jclouds/encryption/internal/JCEEncryptionService.java +++ b/core/src/main/java/org/jclouds/encryption/internal/JCEEncryptionService.java @@ -161,11 +161,27 @@ public class JCEEncryptionService extends BaseEncryptionService { } @Override - public String sha1Base64(String toEncode) throws NoSuchAlgorithmException, + public String sha1Base64(InputStream plainBytes) throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeyException { MessageDigest sha1 = MessageDigest.getInstance("SHA1"); - byte[] digest = sha1.digest(toEncode.getBytes()); - return toBase64String(digest); + byte[] buffer = new byte[1024]; + long length = 0; + int numRead = -1; + try { + do { + numRead = plainBytes.read(buffer); + if (numRead > 0) { + length += numRead; + sha1.update(buffer, 0, numRead); + } + } while (numRead != -1); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + Closeables.closeQuietly(plainBytes); + } + + return toBase64String(sha1.digest()); } @Override diff --git a/core/src/main/java/org/jclouds/http/MultipartForm.java b/core/src/main/java/org/jclouds/http/MultipartForm.java index 1455f347f8..1a1f1d21b6 100644 --- a/core/src/main/java/org/jclouds/http/MultipartForm.java +++ b/core/src/main/java/org/jclouds/http/MultipartForm.java @@ -53,6 +53,7 @@ public class MultipartForm implements Payload { private long size; private boolean isRepeatable; private boolean written; + private final Iterable parts; public MultipartForm(String boundary, Part... parts) { this(boundary, Lists.newArrayList(parts)); @@ -60,6 +61,7 @@ public class MultipartForm implements Payload { @SuppressWarnings("unchecked") public MultipartForm(String boundary, Iterable parts) { + this.parts = parts; String boundaryrn = boundary + rn; isRepeatable = true; InputSupplier chain = ByteStreams.join(); @@ -102,6 +104,7 @@ public class MultipartForm implements Payload { } public static class Part implements Payload { + private final String name; private final Multimap headers; private final Payload delegate; @@ -129,24 +132,65 @@ public class MultipartForm implements Payload { put(HttpHeaders.CONTENT_TYPE, checkNotNull(type, "type")); return this; } + + public static PartMap create(String name, Payload delegate, PartOptions options) { + String filename = options != null ? options.getFilename() : null; + if (delegate instanceof FilePayload) + filename = FilePayload.class.cast(delegate).getRawContent().getName(); + PartMap returnVal; + returnVal = (filename != null) ? create(name, filename) : create(name); + if (options != null) + returnVal.contentType(options.getContentType()); + return returnVal; + + } } - private Part(PartMap map, Payload delegate) { + private Part(String name, PartMap map, Payload delegate) { + this.name = name; this.delegate = checkNotNull(delegate, "delegate"); this.headers = ImmutableMultimap.copyOf(Multimaps.forMap((checkNotNull(map, "headers")))); } + public static class PartOptions { + private String contentType; + private String filename; + + public PartOptions contentType(String contentType) { + this.contentType = checkNotNull(contentType, "contentType"); + return this; + } + + public PartOptions filename(String filename) { + this.filename = checkNotNull(filename, "filename"); + return this; + } + + public static class Builder { + public static PartOptions contentType(String contentType) { + return new PartOptions().contentType(contentType); + } + + public static PartOptions filename(String filename) { + return new PartOptions().filename(filename); + } + } + + public String getContentType() { + return contentType; + } + + public String getFilename() { + return filename; + } + } + public static Part create(String name, String value) { - return new Part(PartMap.create(name), Payloads.newStringPayload(value)); + return new Part(name, PartMap.create(name), Payloads.newStringPayload(value)); } - 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 static Part create(String name, Payload delegate, PartOptions options) { + return new Part(name, PartMap.create(name, delegate, options), delegate); } public Multimap getHeaders() { @@ -184,6 +228,7 @@ public class MultipartForm implements Payload { int result = 1; result = prime * result + ((delegate == null) ? 0 : delegate.hashCode()); result = prime * result + ((headers == null) ? 0 : headers.hashCode()); + result = prime * result + ((name == null) ? 0 : name.hashCode()); return result; } @@ -206,8 +251,17 @@ public class MultipartForm implements Payload { return false; } else if (!headers.equals(other.headers)) return false; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; return true; } + + public String getName() { + return name; + } } @Override @@ -253,4 +307,8 @@ public class MultipartForm implements Payload { return "MultipartForm [chain=" + chain + ", isRepeatable=" + isRepeatable + ", size=" + size + ", written=" + written + "]"; } + + public Iterable getParts() { + return parts; + } } diff --git a/core/src/main/java/org/jclouds/http/payloads/FilePayload.java b/core/src/main/java/org/jclouds/http/payloads/FilePayload.java index e7747ffdc2..c998a9d98c 100644 --- a/core/src/main/java/org/jclouds/http/payloads/FilePayload.java +++ b/core/src/main/java/org/jclouds/http/payloads/FilePayload.java @@ -23,24 +23,27 @@ import static com.google.common.base.Preconditions.checkNotNull; import java.io.File; import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import org.jclouds.http.Payload; +import com.google.common.base.Throwables; import com.google.common.io.Closeables; import com.google.common.io.Files; +import com.google.common.io.InputSupplier; /** * @author Adrian Cole */ public class FilePayload implements Payload { private final File content; + private final InputSupplier delegate; public FilePayload(File content) { checkArgument(checkNotNull(content, "content").exists(), "file must exist: " + content); + this.delegate = Files.newInputStreamSupplier(content); this.content = content; } @@ -54,9 +57,10 @@ public class FilePayload implements Payload { @Override public InputStream getInput() { try { - return new FileInputStream(content); - } catch (FileNotFoundException e) { - throw new IllegalStateException("file " + content + " does not exist", e); + return delegate.getInput(); + } catch (IOException e) { + Throwables.propagate(e); + return null; } } 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 0e50df7831..e0345d6f36 100644 --- a/core/src/main/java/org/jclouds/rest/annotations/PartParam.java +++ b/core/src/main/java/org/jclouds/rest/annotations/PartParam.java @@ -24,8 +24,6 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Retention; import java.lang.annotation.Target; -import javax.ws.rs.core.MediaType; - /** * Designates that this parameter will be bound to a multipart form. * @@ -34,7 +32,13 @@ import javax.ws.rs.core.MediaType; @Target(PARAMETER) @Retention(RUNTIME) public @interface PartParam { + // hacks as nulls are not allowed as default values + public static String NO_FILENAME = "---NO_FILENAME---"; + public static String NO_CONTENT_TYPE = "---NO_CONTENT_TYPE---"; + String name(); - String contentType() default MediaType.TEXT_PLAIN; + String contentType() default NO_CONTENT_TYPE; + + String filename() default NO_FILENAME; } 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 856fd28514..a6e95bafb1 100755 --- a/core/src/main/java/org/jclouds/rest/internal/RestAnnotationProcessor.java +++ b/core/src/main/java/org/jclouds/rest/internal/RestAnnotationProcessor.java @@ -64,6 +64,7 @@ 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.MultipartForm.Part.PartOptions; import org.jclouds.http.functions.CloseContentAndReturn; import org.jclouds.http.functions.ParseSax; import org.jclouds.http.functions.ParseURIFromListOrLocationHeaderIf20x; @@ -417,7 +418,8 @@ public class RestAnnotationProcessor { addHostHeaderIfAnnotatedWithVirtualHost(headers, request.getEndpoint().getHost(), method); addFiltersIfAnnotated(method, request); - List parts = getParts(method, args); + List parts = getParts(method, args, Iterables.concat(tokenValues.entries(), + formParams.entries())); if (parts.size() > 0) { if (formParams.size() > 0) { parts = Lists.newLinkedList(Iterables.concat(Iterables @@ -1005,15 +1007,21 @@ public class RestAnnotationProcessor { return out; } - List getParts(Method method, Object... args) { + List getParts(Method method, Object[] args, + Iterable> iterable) { List parts = Lists.newLinkedList(); Map> indexToPartParam = methodToIndexOfParamToPartParamAnnotations .get(method); for (Entry> entry : indexToPartParam.entrySet()) { for (Annotation key : entry.getValue()) { PartParam param = (PartParam) key; - Part part = Part.create(param.name(), Payloads.newPayload(args[entry.getKey()]), param - .contentType()); + PartOptions options = new PartOptions(); + if (!PartParam.NO_CONTENT_TYPE.equals(param.contentType())) + options.contentType(param.contentType()); + if (!PartParam.NO_FILENAME.equals(param.filename())) + options.filename(replaceTokens(param.filename(), iterable)); + Part part = Part.create(param.name(), Payloads.newPayload(args[entry.getKey()]), + options); parts.add(part); } } diff --git a/core/src/main/java/org/jclouds/util/Utils.java b/core/src/main/java/org/jclouds/util/Utils.java index 38e878d59c..7be3a1332d 100644 --- a/core/src/main/java/org/jclouds/util/Utils.java +++ b/core/src/main/java/org/jclouds/util/Utils.java @@ -27,7 +27,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; -import java.util.Collection; import java.util.Comparator; import java.util.Iterator; import java.util.List; @@ -117,7 +116,7 @@ public class Utils { return e; } - public static String replaceTokens(String value, Collection> tokenValues) { + public static String replaceTokens(String value, Iterable> tokenValues) { for (Entry tokenValue : tokenValues) { value = replaceAll(value, TOKEN_TO_PATTERN.get(tokenValue.getKey()), tokenValue.getValue()); } diff --git a/core/src/test/java/org/jclouds/http/MultipartFormTest.java b/core/src/test/java/org/jclouds/http/MultipartFormTest.java index 6c26e9f319..8a1908c7cf 100644 --- a/core/src/test/java/org/jclouds/http/MultipartFormTest.java +++ b/core/src/test/java/org/jclouds/http/MultipartFormTest.java @@ -31,6 +31,7 @@ import java.io.OutputStream; import javax.ws.rs.core.MediaType; import org.jclouds.http.MultipartForm.Part; +import org.jclouds.http.MultipartForm.Part.PartOptions; import org.jclouds.http.payloads.FilePayload; import org.jclouds.http.payloads.StringPayload; import org.jclouds.util.Utils; @@ -99,7 +100,8 @@ public class MultipartFormTest { } private Part newPart(String data) { - return Part.create("file", new MockFilePayload(data), MediaType.TEXT_PLAIN); + return Part.create("file", new MockFilePayload(data), new PartOptions() + .contentType(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 1867d09ae7..58c110c6bb 100755 --- a/core/src/test/java/org/jclouds/rest/internal/RestAnnotationProcessorTest.java +++ b/core/src/test/java/org/jclouds/rest/internal/RestAnnotationProcessorTest.java @@ -619,19 +619,22 @@ public class RestAnnotationProcessorTest { @Endpoint(Localhost.class) static interface TestMultipartForm { @POST - public void withStringPart(@PartParam(name = "fooble") String path); + void withStringPart(@PartParam(name = "fooble") String path); @POST - public void withParamStringPart(@FormParam("name") String name, - @PartParam(name = "file") String path); + void withParamStringPart(@FormParam("name") String name, @PartParam(name = "file") String path); @POST - public void withParamFilePart(@FormParam("name") String name, - @PartParam(name = "file") File path); + void withParamFilePart(@FormParam("name") String name, @PartParam(name = "file") File path); @POST - public void withParamFileBinaryPart(@FormParam("name") String name, + void withParamFileBinaryPart(@FormParam("name") String name, @PartParam(name = "file", contentType = MediaType.APPLICATION_OCTET_STREAM) File path); + + @POST + void withParamByteArrayBinaryPart( + @FormParam("name") String name, + @PartParam(name = "file", contentType = MediaType.APPLICATION_OCTET_STREAM, filename = "{name}.tar.gz") byte[] content); } public void testMultipartWithStringPart() throws SecurityException, NoSuchMethodException, @@ -641,11 +644,10 @@ public class RestAnnotationProcessorTest { .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"); + "Content-Length: 93\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"); @@ -659,7 +661,7 @@ public class RestAnnotationProcessorTest { .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"); + "Content-Length: 159\nContent-Type: multipart/form-data; boundary=--JCLOUDS--\n"); assertPayloadEquals(httpRequest,// "----JCLOUDS--\r\n" + // "Content-Disposition: form-data; name=\"name\"\r\n" + // @@ -667,7 +669,6 @@ public class RestAnnotationProcessorTest { "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"); @@ -685,20 +686,49 @@ public class RestAnnotationProcessorTest { .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"); + "Content-Length: " + (172 + file.getName().length()) + + "\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" + // + "Content-Disposition: form-data; name=\"file\"; filename=\"" + + file.getName() + "\"\r\n" + // "\r\n" + // "foobledata\r\n" + // "----JCLOUDS----\r\n"); } + public void testMultipartWithParamByteArrayPart() throws SecurityException, + NoSuchMethodException, IOException { + Method method = TestMultipartForm.class.getMethod("withParamByteArrayBinaryPart", + String.class, byte[].class); + GeneratedHttpRequest httpRequest = factory(TestMultipartForm.class) + .createRequest(method, "name", "goo".getBytes()); + assertRequestLineEquals(httpRequest, "POST http://localhost:9999 HTTP/1.1"); + assertHeadersEqual(httpRequest, + "Content-Length: 216\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\"; filename=\"name.tar.gz\"\r\n" + + // + "Content-Type: application/octet-stream\r\n" + // + "\r\n" + // + "goo\r\n" + // + "----JCLOUDS----\r\n"); + }; + public void testMultipartWithParamFileBinaryPart() throws SecurityException, NoSuchMethodException, IOException { Method method = TestMultipartForm.class.getMethod("withParamFileBinaryPart", String.class, @@ -710,15 +740,21 @@ public class RestAnnotationProcessorTest { 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"); + assertHeadersEqual(httpRequest, "Content-Length: " + (207 + file.getName().length()) + + "\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" + // + "----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\"; filename=\"" + + file.getName() + "\"\r\n" + // "Content-Type: application/octet-stream\r\n" + // "\r\n" + // "'(2\r\n" + // diff --git a/extensions/bouncycastle/src/main/java/org/jclouds/encryption/bouncycastle/BouncyCastleEncryptionService.java b/extensions/bouncycastle/src/main/java/org/jclouds/encryption/bouncycastle/BouncyCastleEncryptionService.java index 996e60cbea..e9491aeb80 100644 --- a/extensions/bouncycastle/src/main/java/org/jclouds/encryption/bouncycastle/BouncyCastleEncryptionService.java +++ b/extensions/bouncycastle/src/main/java/org/jclouds/encryption/bouncycastle/BouncyCastleEncryptionService.java @@ -163,12 +163,26 @@ public class BouncyCastleEncryptionService extends BaseEncryptionService { } @Override - public String sha1Base64(String toEncode) throws NoSuchAlgorithmException, + public String sha1Base64(InputStream plainBytes) throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeyException { - byte[] plainBytes = toEncode.getBytes(); Digest digest = new SHA1Digest(); byte[] resBuf = new byte[digest.getDigestSize()]; - digest.update(plainBytes, 0, plainBytes.length); + byte[] buffer = new byte[1024]; + long length = 0; + int numRead = -1; + try { + do { + numRead = plainBytes.read(buffer); + if (numRead > 0) { + length += numRead; + digest.update(buffer, 0, numRead); + } + } while (numRead != -1); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + Closeables.closeQuietly(plainBytes); + } digest.doFinal(resBuf, 0); return toBase64String(resBuf); }