Issue 191: corrected multipart form and added cookbook signing to chef client

This commit is contained in:
Adrian Cole 2010-06-12 17:17:42 -07:00
parent 306bb0ebde
commit f25100fe9b
18 changed files with 375 additions and 94 deletions

View File

@ -24,6 +24,7 @@ import org.jclouds.blobstore.domain.Blob;
import org.jclouds.http.HttpRequest; import org.jclouds.http.HttpRequest;
import org.jclouds.http.MultipartForm; import org.jclouds.http.MultipartForm;
import org.jclouds.http.MultipartForm.Part; import org.jclouds.http.MultipartForm.Part;
import org.jclouds.http.MultipartForm.Part.PartOptions;
import org.jclouds.rest.Binder; import org.jclouds.rest.Binder;
/** /**
@ -36,10 +37,10 @@ public class BindBlobToMultipartForm implements Binder {
public void bindToRequest(HttpRequest request, Object payload) { public void bindToRequest(HttpRequest request, Object payload) {
Blob object = (Blob) payload; Blob object = (Blob) payload;
Part part = Part.create(object.getMetadata().getName(), object.getPayload(), object Part part = Part.create(object.getMetadata().getName(), object.getPayload(),
.getMetadata().getContentType()); new PartOptions().contentType(object.getMetadata().getContentType()));
MultipartForm form = new MultipartForm(BOUNDARY, part); MultipartForm form = new MultipartForm(BOUNDARY, part);
request.setPayload(form.getInput()); request.setPayload(form.getInput());
request.getHeaders().put(HttpHeaders.CONTENT_TYPE, request.getHeaders().put(HttpHeaders.CONTENT_TYPE,

View File

@ -70,14 +70,15 @@ public interface ChefAsyncClient {
*/ */
@GET @GET
@Path("cookbooks") @Path("cookbooks")
ListenableFuture<String> listCookbooks(); @ResponseParser(ParseKeySetFromJson.class)
ListenableFuture<Set<String>> listCookbooks();
/** /**
* @see ChefClient#createCookbook(String,File) * @see ChefClient#createCookbook(String,File)
*/ */
@POST @POST
@Path("cookbooks") @Path("name")
ListenableFuture<String> createCookbook(@FormParam("name") String name, ListenableFuture<Void> createCookbook(@FormParam("name") String cookbookName,
@PartParam(name = "file", contentType = MediaType.APPLICATION_OCTET_STREAM) File content); @PartParam(name = "file", contentType = MediaType.APPLICATION_OCTET_STREAM) File content);
/** /**
@ -85,8 +86,44 @@ public interface ChefAsyncClient {
*/ */
@POST @POST
@Path("cookbooks") @Path("cookbooks")
ListenableFuture<String> createCookbook(@FormParam("name") String name, ListenableFuture<Void> createCookbook(
@PartParam(name = "file", contentType = MediaType.APPLICATION_OCTET_STREAM) byte[] content); @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<Void> 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<Void> 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<Void> deleteCookbook(@PathParam("cookbookname") String cookbookName);
/**
* @see ChefCookbook#getCookbook
*/
@GET
@Path("cookbooks/{cookbookname}")
ListenableFuture<String> getCookbook(@PathParam("cookbookname") String cookbookName);
/** /**
* @see ChefClient#createClient * @see ChefClient#createClient

View File

@ -42,7 +42,6 @@
package org.jclouds.chef; package org.jclouds.chef;
import java.io.File; import java.io.File;
import java.io.InputStream;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -60,11 +59,23 @@ import org.jclouds.rest.AuthorizationException;
*/ */
@Timeout(duration = 30, timeUnit = TimeUnit.SECONDS) @Timeout(duration = 30, timeUnit = TimeUnit.SECONDS)
public interface ChefClient { public interface ChefClient {
String listCookbooks(); Set<String> 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 * creates a new client

View File

@ -27,6 +27,7 @@ import static com.google.common.base.Preconditions.checkArgument;
import java.security.PrivateKey; import java.security.PrivateKey;
import java.util.Collections; import java.util.Collections;
import java.util.NoSuchElementException;
import javax.annotation.Resource; import javax.annotation.Resource;
import javax.inject.Inject; import javax.inject.Inject;
@ -42,13 +43,16 @@ import org.jclouds.http.HttpException;
import org.jclouds.http.HttpRequest; import org.jclouds.http.HttpRequest;
import org.jclouds.http.HttpRequestFilter; import org.jclouds.http.HttpRequestFilter;
import org.jclouds.http.HttpUtils; import org.jclouds.http.HttpUtils;
import org.jclouds.http.MultipartForm;
import org.jclouds.http.Payload; import org.jclouds.http.Payload;
import org.jclouds.http.Payloads; import org.jclouds.http.Payloads;
import org.jclouds.http.MultipartForm.Part;
import org.jclouds.http.internal.SignatureWire; import org.jclouds.http.internal.SignatureWire;
import org.jclouds.logging.Logger; import org.jclouds.logging.Logger;
import org.jclouds.util.Utils; import org.jclouds.util.Utils;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Predicate;
import com.google.common.base.Splitter; import com.google.common.base.Splitter;
import com.google.common.base.Throwables; import com.google.common.base.Throwables;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
@ -130,7 +134,7 @@ public class SignedHeaderAuth implements HttpRequestFilter {
@VisibleForTesting @VisibleForTesting
String hashPath(String path) { String hashPath(String path) {
try { try {
return encryptionService.sha1Base64(canonicalPath(path)); return encryptionService.sha1Base64(Utils.toInputStream(canonicalPath(path)));
} catch (Exception e) { } catch (Exception e) {
Throwables.propagateIfPossible(e); Throwables.propagateIfPossible(e);
throw new HttpException("error creating sigature for path: " + path, e); throw new HttpException("error creating sigature for path: " + path, e);
@ -151,16 +155,36 @@ public class SignedHeaderAuth implements HttpRequestFilter {
String hashBody(Payload payload) { String hashBody(Payload payload) {
if (payload == null) if (payload == null)
return emptyStringHash; return emptyStringHash;
payload = useTheFilePartIfForm(payload);
checkArgument(payload != null, "payload was null"); checkArgument(payload != null, "payload was null");
checkArgument(payload.isRepeatable(), "payload must be repeatable: " + payload); checkArgument(payload.isRepeatable(), "payload must be repeatable: " + payload);
try { try {
return encryptionService.sha1Base64(Utils.toStringAndClose(payload.getInput())); return encryptionService.sha1Base64(payload.getInput());
} catch (Exception e) { } catch (Exception e) {
Throwables.propagateIfPossible(e); Throwables.propagateIfPossible(e);
throw new HttpException("error creating sigature for payload: " + payload, e); throw new HttpException("error creating sigature for payload: " + payload, e);
} }
} }
private Payload useTheFilePartIfForm(Payload payload) {
if (payload instanceof MultipartForm) {
Iterable<? extends Part> parts = MultipartForm.class.cast(payload).getParts();
try {
payload = Iterables.find(parts, new Predicate<Part>() {
@Override
public boolean apply(Part input) {
return "file".equals(input.getName());
}
});
} catch (NoSuchElementException e) {
}
}
return payload;
}
public String sign(String toSign) { public String sign(String toSign) {
try { try {
byte[] encrypted = encryptionService.rsaPrivateEncrypt(toSign, privateKey); byte[] encrypted = encryptionService.rsaPrivateEncrypt(toSign, privateKey);

View File

@ -41,8 +41,8 @@ import com.google.common.base.Throwables;
* @author Adrian Cole * @author Adrian Cole
*/ */
@Singleton @Singleton
public class ParseErrorFromJsonOrNull implements Function<HttpResponse, String> { public class ParseErrorFromJsonOrReturnBody implements Function<HttpResponse, String> {
Pattern pattern = Pattern.compile(".*error\": *\"([^\"]+)\".*"); Pattern pattern = Pattern.compile(".*\\[\"([^\"]+)\"\\].*");
@Override @Override
public String apply(HttpResponse response) { public String apply(HttpResponse response) {
@ -63,9 +63,9 @@ public class ParseErrorFromJsonOrNull implements Function<HttpResponse, String>
public String parse(String in) { public String parse(String in) {
Matcher matcher = pattern.matcher(in); Matcher matcher = pattern.matcher(in);
while (matcher.find()) { if (matcher.find()) {
return matcher.group(1); return matcher.group(1);
} }
return null; return in;
} }
} }

View File

@ -22,7 +22,7 @@ import javax.annotation.Resource;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; 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.HttpCommand;
import org.jclouds.http.HttpErrorHandler; import org.jclouds.http.HttpErrorHandler;
import org.jclouds.http.HttpResponse; import org.jclouds.http.HttpResponse;
@ -43,17 +43,16 @@ import com.google.common.io.Closeables;
public class ChefErrorHandler implements HttpErrorHandler { public class ChefErrorHandler implements HttpErrorHandler {
@Resource @Resource
protected Logger logger = Logger.NULL; protected Logger logger = Logger.NULL;
private final ParseErrorFromJsonOrNull errorParser; private final ParseErrorFromJsonOrReturnBody errorParser;
@Inject @Inject
ChefErrorHandler(ParseErrorFromJsonOrNull errorParser) { ChefErrorHandler(ParseErrorFromJsonOrReturnBody errorParser) {
this.errorParser = errorParser; this.errorParser = errorParser;
} }
public void handleError(HttpCommand command, HttpResponse response) { public void handleError(HttpCommand command, HttpResponse response) {
String message = errorParser.apply(response); String message = errorParser.apply(response);
Exception exception = message != null ? new HttpResponseException(command, response, message) Exception exception = new HttpResponseException(command, response, message);
: new HttpResponseException(command, response);
try { try {
message = message != null ? message : String.format("%s -> %s", command.getRequest() message = message != null ? message : String.format("%s -> %s", command.getRequest()
.getRequestLine(), response.getStatusLine()); .getRequestLine(), response.getStatusLine());

View File

@ -26,7 +26,6 @@ package org.jclouds.chef;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertNotNull;
import java.io.ByteArrayInputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -51,26 +50,37 @@ import com.google.common.io.Files;
@Test(groups = "live", testName = "chef.ChefClientLiveTest") @Test(groups = "live", testName = "chef.ChefClientLiveTest")
public class 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<ChefClient, ChefAsyncClient> validatorConnection; private RestContext<ChefClient, ChefAsyncClient> validatorConnection;
private RestContext<ChefClient, ChefAsyncClient> clientConnection; private RestContext<ChefClient, ChefAsyncClient> clientConnection;
private RestContext<ChefClient, ChefAsyncClient> adminConnection;
private String clientKey; private String clientKey;
private String endpoint; private String endpoint;
private String validator; private String validator;
private String user;
private byte[] cookbookContent;
private File cookbookFile;
public static final String PREFIX = System.getProperty("user.name") + "-jcloudstest"; public static final String PREFIX = System.getProperty("user.name") + "-jcloudstest";
@BeforeClass(groups = { "live" }) @BeforeClass(groups = { "live" })
public void setupClient() throws IOException { public void setupClient() throws IOException {
endpoint = checkNotNull(System.getProperty("jclouds.test.endpoint"), "jclouds.test.endpoint"); 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("")) if (validator == null || validator.equals(""))
validator = "chef-validator"; 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"); String keyfile = System.getProperty("jclouds.test.key");
if (keyfile == null || keyfile.equals("")) if (keyfile == null || keyfile.equals(""))
keyfile = "/etc/chef/validation.pem"; keyfile = System.getProperty("user.home") + "/chef/" + user + ".pem";
validatorConnection = createConnection(validator, Files.toString(new File(keyfile), validatorConnection = createConnection(validator, Files.toString(new File(validatorKey),
Charsets.UTF_8)); Charsets.UTF_8));
adminConnection = createConnection(user, Files.toString(new File(keyfile), Charsets.UTF_8));
} }
private RestContext<ChefClient, ChefAsyncClient> createConnection(String identity, String key) private RestContext<ChefClient, ChefAsyncClient> createConnection(String identity, String key)
@ -110,24 +120,22 @@ public class ChefClientLiveTest {
assertNotNull(validatorConnection.getApi().clientExists(PREFIX)); assertNotNull(validatorConnection.getApi().clientExists(PREFIX));
} }
@Test(dependsOnMethods = "testGenerateKeyForClient") @Test
public void testCreateCookbooks() throws Exception { public void testCreateCookbook() throws Exception {
adminConnection.getApi().deleteCookbook(COOKBOOK_NAME);
InputStream in = null; InputStream in = null;
try { try {
in = URI in = URI.create(COOKBOOK_URI).toURL().openStream();
.create(
"https://s3.amazonaws.com/opscode-community/cookbook_versions/tarballs/194/original/java.tar.gz")
.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"); adminConnection.getApi().createCookbook(COOKBOOK_NAME, cookbookFile);
Files.write(content, file); adminConnection.getApi().deleteCookbook(COOKBOOK_NAME);
file.deleteOnExit(); adminConnection.getApi().createCookbook(COOKBOOK_NAME, cookbookContent);
System.err.println(clientConnection.getApi().createCookbook("java-file", file));
} finally { } finally {
if (in != null) 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 { 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" }) @AfterClass(groups = { "live" })
@ -146,5 +162,7 @@ public class ChefClientLiveTest {
clientConnection.close(); clientConnection.close();
if (validatorConnection != null) if (validatorConnection != null)
validatorConnection.close(); validatorConnection.close();
if (adminConnection != null)
adminConnection.close();
} }
} }

View File

@ -0,0 +1,50 @@
/**
*
* Copyright (C) 2010 Cloud Conscious, LLC. <info@cloudconscious.com>
*
* ====================================================================
* 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");
}
}

View File

@ -52,7 +52,7 @@ public interface EncryptionService {
String hmacSha256Base64(String toEncode, byte[] key) throws NoSuchAlgorithmException, String hmacSha256Base64(String toEncode, byte[] key) throws NoSuchAlgorithmException,
NoSuchProviderException, InvalidKeyException; NoSuchProviderException, InvalidKeyException;
String sha1Base64(String toEncode) throws NoSuchAlgorithmException, NoSuchProviderException, String sha1Base64(InputStream toEncode) throws NoSuchAlgorithmException, NoSuchProviderException,
InvalidKeyException; InvalidKeyException;
String hmacSha1Base64(String toEncode, byte[] key) throws NoSuchAlgorithmException, String hmacSha1Base64(String toEncode, byte[] key) throws NoSuchAlgorithmException,

View File

@ -161,11 +161,27 @@ public class JCEEncryptionService extends BaseEncryptionService {
} }
@Override @Override
public String sha1Base64(String toEncode) throws NoSuchAlgorithmException, public String sha1Base64(InputStream plainBytes) throws NoSuchAlgorithmException,
NoSuchProviderException, InvalidKeyException { NoSuchProviderException, InvalidKeyException {
MessageDigest sha1 = MessageDigest.getInstance("SHA1"); MessageDigest sha1 = MessageDigest.getInstance("SHA1");
byte[] digest = sha1.digest(toEncode.getBytes()); byte[] buffer = new byte[1024];
return toBase64String(digest); 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 @Override

View File

@ -53,6 +53,7 @@ public class MultipartForm implements Payload {
private long size; private long size;
private boolean isRepeatable; private boolean isRepeatable;
private boolean written; private boolean written;
private final Iterable<? extends Part> parts;
public MultipartForm(String boundary, Part... parts) { public MultipartForm(String boundary, Part... parts) {
this(boundary, Lists.newArrayList(parts)); this(boundary, Lists.newArrayList(parts));
@ -60,6 +61,7 @@ public class MultipartForm implements Payload {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public MultipartForm(String boundary, Iterable<? extends Part> parts) { public MultipartForm(String boundary, Iterable<? extends Part> parts) {
this.parts = parts;
String boundaryrn = boundary + rn; String boundaryrn = boundary + rn;
isRepeatable = true; isRepeatable = true;
InputSupplier<? extends InputStream> chain = ByteStreams.join(); InputSupplier<? extends InputStream> chain = ByteStreams.join();
@ -102,6 +104,7 @@ public class MultipartForm implements Payload {
} }
public static class Part implements Payload { public static class Part implements Payload {
private final String name;
private final Multimap<String, String> headers; private final Multimap<String, String> headers;
private final Payload delegate; private final Payload delegate;
@ -129,24 +132,65 @@ public class MultipartForm implements Payload {
put(HttpHeaders.CONTENT_TYPE, checkNotNull(type, "type")); put(HttpHeaders.CONTENT_TYPE, checkNotNull(type, "type"));
return this; 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.delegate = checkNotNull(delegate, "delegate");
this.headers = ImmutableMultimap.copyOf(Multimaps.forMap((checkNotNull(map, "headers")))); 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) { 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) { public static Part create(String name, Payload delegate, PartOptions options) {
return new Part(PartMap.create(name).contentType(contentType), delegate); return new Part(name, PartMap.create(name, delegate, options), 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() { public Multimap<String, String> getHeaders() {
@ -184,6 +228,7 @@ public class MultipartForm implements Payload {
int result = 1; int result = 1;
result = prime * result + ((delegate == null) ? 0 : delegate.hashCode()); result = prime * result + ((delegate == null) ? 0 : delegate.hashCode());
result = prime * result + ((headers == null) ? 0 : headers.hashCode()); result = prime * result + ((headers == null) ? 0 : headers.hashCode());
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result; return result;
} }
@ -206,8 +251,17 @@ public class MultipartForm implements Payload {
return false; return false;
} else if (!headers.equals(other.headers)) } else if (!headers.equals(other.headers))
return false; return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true; return true;
} }
public String getName() {
return name;
}
} }
@Override @Override
@ -253,4 +307,8 @@ public class MultipartForm implements Payload {
return "MultipartForm [chain=" + chain + ", isRepeatable=" + isRepeatable + ", size=" + size return "MultipartForm [chain=" + chain + ", isRepeatable=" + isRepeatable + ", size=" + size
+ ", written=" + written + "]"; + ", written=" + written + "]";
} }
public Iterable<? extends Part> getParts() {
return parts;
}
} }

View File

@ -23,24 +23,27 @@ import static com.google.common.base.Preconditions.checkNotNull;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import org.jclouds.http.Payload; import org.jclouds.http.Payload;
import com.google.common.base.Throwables;
import com.google.common.io.Closeables; import com.google.common.io.Closeables;
import com.google.common.io.Files; import com.google.common.io.Files;
import com.google.common.io.InputSupplier;
/** /**
* @author Adrian Cole * @author Adrian Cole
*/ */
public class FilePayload implements Payload { public class FilePayload implements Payload {
private final File content; private final File content;
private final InputSupplier<FileInputStream> delegate;
public FilePayload(File content) { public FilePayload(File content) {
checkArgument(checkNotNull(content, "content").exists(), "file must exist: " + content); checkArgument(checkNotNull(content, "content").exists(), "file must exist: " + content);
this.delegate = Files.newInputStreamSupplier(content);
this.content = content; this.content = content;
} }
@ -54,9 +57,10 @@ public class FilePayload implements Payload {
@Override @Override
public InputStream getInput() { public InputStream getInput() {
try { try {
return new FileInputStream(content); return delegate.getInput();
} catch (FileNotFoundException e) { } catch (IOException e) {
throw new IllegalStateException("file " + content + " does not exist", e); Throwables.propagate(e);
return null;
} }
} }

View File

@ -24,8 +24,6 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.Target; import java.lang.annotation.Target;
import javax.ws.rs.core.MediaType;
/** /**
* Designates that this parameter will be bound to a multipart form. * Designates that this parameter will be bound to a multipart form.
* *
@ -34,7 +32,13 @@ import javax.ws.rs.core.MediaType;
@Target(PARAMETER) @Target(PARAMETER)
@Retention(RUNTIME) @Retention(RUNTIME)
public @interface PartParam { 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 name();
String contentType() default MediaType.TEXT_PLAIN; String contentType() default NO_CONTENT_TYPE;
String filename() default NO_FILENAME;
} }

View File

@ -64,6 +64,7 @@ import org.jclouds.http.HttpUtils;
import org.jclouds.http.MultipartForm; import org.jclouds.http.MultipartForm;
import org.jclouds.http.Payloads; import org.jclouds.http.Payloads;
import org.jclouds.http.MultipartForm.Part; import org.jclouds.http.MultipartForm.Part;
import org.jclouds.http.MultipartForm.Part.PartOptions;
import org.jclouds.http.functions.CloseContentAndReturn; import org.jclouds.http.functions.CloseContentAndReturn;
import org.jclouds.http.functions.ParseSax; import org.jclouds.http.functions.ParseSax;
import org.jclouds.http.functions.ParseURIFromListOrLocationHeaderIf20x; import org.jclouds.http.functions.ParseURIFromListOrLocationHeaderIf20x;
@ -417,7 +418,8 @@ public class RestAnnotationProcessor<T> {
addHostHeaderIfAnnotatedWithVirtualHost(headers, request.getEndpoint().getHost(), method); addHostHeaderIfAnnotatedWithVirtualHost(headers, request.getEndpoint().getHost(), method);
addFiltersIfAnnotated(method, request); addFiltersIfAnnotated(method, request);
List<? extends Part> parts = getParts(method, args); List<? extends Part> parts = getParts(method, args, Iterables.concat(tokenValues.entries(),
formParams.entries()));
if (parts.size() > 0) { if (parts.size() > 0) {
if (formParams.size() > 0) { if (formParams.size() > 0) {
parts = Lists.newLinkedList(Iterables.concat(Iterables parts = Lists.newLinkedList(Iterables.concat(Iterables
@ -1005,15 +1007,21 @@ public class RestAnnotationProcessor<T> {
return out; return out;
} }
List<? extends Part> getParts(Method method, Object... args) { List<? extends Part> getParts(Method method, Object[] args,
Iterable<Entry<String, String>> iterable) {
List<Part> parts = Lists.newLinkedList(); List<Part> parts = Lists.newLinkedList();
Map<Integer, Set<Annotation>> indexToPartParam = methodToIndexOfParamToPartParamAnnotations Map<Integer, Set<Annotation>> indexToPartParam = methodToIndexOfParamToPartParamAnnotations
.get(method); .get(method);
for (Entry<Integer, Set<Annotation>> entry : indexToPartParam.entrySet()) { for (Entry<Integer, Set<Annotation>> entry : indexToPartParam.entrySet()) {
for (Annotation key : entry.getValue()) { for (Annotation key : entry.getValue()) {
PartParam param = (PartParam) key; PartParam param = (PartParam) key;
Part part = Part.create(param.name(), Payloads.newPayload(args[entry.getKey()]), param PartOptions options = new PartOptions();
.contentType()); 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); parts.add(part);
} }
} }

View File

@ -27,7 +27,6 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.util.Collection;
import java.util.Comparator; import java.util.Comparator;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
@ -117,7 +116,7 @@ public class Utils {
return e; return e;
} }
public static String replaceTokens(String value, Collection<Entry<String, String>> tokenValues) { public static String replaceTokens(String value, Iterable<Entry<String, String>> tokenValues) {
for (Entry<String, String> tokenValue : tokenValues) { for (Entry<String, String> tokenValue : tokenValues) {
value = replaceAll(value, TOKEN_TO_PATTERN.get(tokenValue.getKey()), tokenValue.getValue()); value = replaceAll(value, TOKEN_TO_PATTERN.get(tokenValue.getKey()), tokenValue.getValue());
} }

View File

@ -31,6 +31,7 @@ import java.io.OutputStream;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import org.jclouds.http.MultipartForm.Part; import org.jclouds.http.MultipartForm.Part;
import org.jclouds.http.MultipartForm.Part.PartOptions;
import org.jclouds.http.payloads.FilePayload; import org.jclouds.http.payloads.FilePayload;
import org.jclouds.http.payloads.StringPayload; import org.jclouds.http.payloads.StringPayload;
import org.jclouds.util.Utils; import org.jclouds.util.Utils;
@ -99,7 +100,8 @@ public class MultipartFormTest {
} }
private Part newPart(String data) { 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) { private void addData(String boundary, String data, StringBuilder builder) {

View File

@ -619,19 +619,22 @@ public class RestAnnotationProcessorTest {
@Endpoint(Localhost.class) @Endpoint(Localhost.class)
static interface TestMultipartForm { static interface TestMultipartForm {
@POST @POST
public void withStringPart(@PartParam(name = "fooble") String path); void withStringPart(@PartParam(name = "fooble") String path);
@POST @POST
public void withParamStringPart(@FormParam("name") String name, void withParamStringPart(@FormParam("name") String name, @PartParam(name = "file") String path);
@PartParam(name = "file") String path);
@POST @POST
public void withParamFilePart(@FormParam("name") String name, void withParamFilePart(@FormParam("name") String name, @PartParam(name = "file") File path);
@PartParam(name = "file") File path);
@POST @POST
public void withParamFileBinaryPart(@FormParam("name") String name, void withParamFileBinaryPart(@FormParam("name") String name,
@PartParam(name = "file", contentType = MediaType.APPLICATION_OCTET_STREAM) File path); @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, public void testMultipartWithStringPart() throws SecurityException, NoSuchMethodException,
@ -641,11 +644,10 @@ public class RestAnnotationProcessorTest {
.createRequest(method, "foobledata"); .createRequest(method, "foobledata");
assertRequestLineEquals(httpRequest, "POST http://localhost:9999 HTTP/1.1"); assertRequestLineEquals(httpRequest, "POST http://localhost:9999 HTTP/1.1");
assertHeadersEqual(httpRequest, 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,// assertPayloadEquals(httpRequest,//
"----JCLOUDS--\r\n" + // "----JCLOUDS--\r\n" + //
"Content-Disposition: form-data; name=\"fooble\"\r\n" + // "Content-Disposition: form-data; name=\"fooble\"\r\n" + //
"Content-Type: text/plain\r\n" + //
"\r\n" + // "\r\n" + //
"foobledata\r\n" + // "foobledata\r\n" + //
"----JCLOUDS----\r\n"); "----JCLOUDS----\r\n");
@ -659,7 +661,7 @@ public class RestAnnotationProcessorTest {
.createRequest(method, "name", "foobledata"); .createRequest(method, "name", "foobledata");
assertRequestLineEquals(httpRequest, "POST http://localhost:9999 HTTP/1.1"); assertRequestLineEquals(httpRequest, "POST http://localhost:9999 HTTP/1.1");
assertHeadersEqual(httpRequest, 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,// assertPayloadEquals(httpRequest,//
"----JCLOUDS--\r\n" + // "----JCLOUDS--\r\n" + //
"Content-Disposition: form-data; name=\"name\"\r\n" + // "Content-Disposition: form-data; name=\"name\"\r\n" + //
@ -667,7 +669,6 @@ public class RestAnnotationProcessorTest {
"name\r\n" + // / "name\r\n" + // /
"----JCLOUDS--\r\n" + // "----JCLOUDS--\r\n" + //
"Content-Disposition: form-data; name=\"file\"\r\n" + // "Content-Disposition: form-data; name=\"file\"\r\n" + //
"Content-Type: text/plain\r\n" + //
"\r\n" + // "\r\n" + //
"foobledata\r\n" + // "foobledata\r\n" + //
"----JCLOUDS----\r\n"); "----JCLOUDS----\r\n");
@ -685,20 +686,49 @@ public class RestAnnotationProcessorTest {
.createRequest(method, "name", file); .createRequest(method, "name", file);
assertRequestLineEquals(httpRequest, "POST http://localhost:9999 HTTP/1.1"); assertRequestLineEquals(httpRequest, "POST http://localhost:9999 HTTP/1.1");
assertHeadersEqual(httpRequest, 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,// assertPayloadEquals(httpRequest,//
"----JCLOUDS--\r\n" + // "----JCLOUDS--\r\n" + //
"Content-Disposition: form-data; name=\"name\"\r\n" + // "Content-Disposition: form-data; name=\"name\"\r\n" + //
"\r\n" + // "\r\n" + //
"name\r\n" + // / "name\r\n" + // /
"----JCLOUDS--\r\n" + // "----JCLOUDS--\r\n" + //
"Content-Disposition: form-data; name=\"file\"\r\n" + // "Content-Disposition: form-data; name=\"file\"; filename=\""
"Content-Type: text/plain\r\n" + // + file.getName() + "\"\r\n" + //
"\r\n" + // "\r\n" + //
"foobledata\r\n" + // "foobledata\r\n" + //
"----JCLOUDS----\r\n"); "----JCLOUDS----\r\n");
} }
public void testMultipartWithParamByteArrayPart() throws SecurityException,
NoSuchMethodException, IOException {
Method method = TestMultipartForm.class.getMethod("withParamByteArrayBinaryPart",
String.class, byte[].class);
GeneratedHttpRequest<TestMultipartForm> 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, public void testMultipartWithParamFileBinaryPart() throws SecurityException,
NoSuchMethodException, IOException { NoSuchMethodException, IOException {
Method method = TestMultipartForm.class.getMethod("withParamFileBinaryPart", String.class, Method method = TestMultipartForm.class.getMethod("withParamFileBinaryPart", String.class,
@ -710,15 +740,21 @@ public class RestAnnotationProcessorTest {
GeneratedHttpRequest<TestMultipartForm> httpRequest = factory(TestMultipartForm.class) GeneratedHttpRequest<TestMultipartForm> httpRequest = factory(TestMultipartForm.class)
.createRequest(method, "name", file); .createRequest(method, "name", file);
assertRequestLineEquals(httpRequest, "POST http://localhost:9999 HTTP/1.1"); assertRequestLineEquals(httpRequest, "POST http://localhost:9999 HTTP/1.1");
assertHeadersEqual(httpRequest, assertHeadersEqual(httpRequest, "Content-Length: " + (207 + file.getName().length())
"Content-Length: 194\nContent-Type: multipart/form-data; boundary=--JCLOUDS--\n"); + "\nContent-Type: multipart/form-data; boundary=--JCLOUDS--\n");
assertPayloadEquals(httpRequest,// assertPayloadEquals(httpRequest,//
"----JCLOUDS--\r\n" + // "----JCLOUDS--\r\n"
"Content-Disposition: form-data; name=\"name\"\r\n" + // + //
"\r\n" + // "Content-Disposition: form-data; name=\"name\"\r\n"
"name\r\n" + // / + //
"----JCLOUDS--\r\n" + // "\r\n"
"Content-Disposition: form-data; name=\"file\"\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" + // "Content-Type: application/octet-stream\r\n" + //
"\r\n" + // "\r\n" + //
"'(2\r\n" + // "'(2\r\n" + //

View File

@ -163,12 +163,26 @@ public class BouncyCastleEncryptionService extends BaseEncryptionService {
} }
@Override @Override
public String sha1Base64(String toEncode) throws NoSuchAlgorithmException, public String sha1Base64(InputStream plainBytes) throws NoSuchAlgorithmException,
NoSuchProviderException, InvalidKeyException { NoSuchProviderException, InvalidKeyException {
byte[] plainBytes = toEncode.getBytes();
Digest digest = new SHA1Digest(); Digest digest = new SHA1Digest();
byte[] resBuf = new byte[digest.getDigestSize()]; 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); digest.doFinal(resBuf, 0);
return toBase64String(resBuf); return toBase64String(resBuf);
} }