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.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,

View File

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

View File

@ -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<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

View File

@ -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<? 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) {
try {
byte[] encrypted = encryptionService.rsaPrivateEncrypt(toSign, privateKey);

View File

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

View File

@ -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<ChefClient, ChefAsyncClient> validatorConnection;
private RestContext<ChefClient, ChefAsyncClient> clientConnection;
private RestContext<ChefClient, ChefAsyncClient> 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<ChefClient, ChefAsyncClient> 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();
}
}

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,
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,

View File

@ -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

View File

@ -53,6 +53,7 @@ public class MultipartForm implements Payload {
private long size;
private boolean isRepeatable;
private boolean written;
private final Iterable<? extends Part> 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<? extends Part> parts) {
this.parts = parts;
String boundaryrn = boundary + rn;
isRepeatable = true;
InputSupplier<? extends InputStream> 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<String, String> 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<String, String> 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<? 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.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<FileInputStream> 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;
}
}

View File

@ -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;
}

View File

@ -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<T> {
addHostHeaderIfAnnotatedWithVirtualHost(headers, request.getEndpoint().getHost(), method);
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 (formParams.size() > 0) {
parts = Lists.newLinkedList(Iterables.concat(Iterables
@ -1005,15 +1007,21 @@ public class RestAnnotationProcessor<T> {
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();
Map<Integer, Set<Annotation>> indexToPartParam = methodToIndexOfParamToPartParamAnnotations
.get(method);
for (Entry<Integer, Set<Annotation>> 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);
}
}

View File

@ -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<Entry<String, String>> tokenValues) {
public static String replaceTokens(String value, Iterable<Entry<String, String>> tokenValues) {
for (Entry<String, String> tokenValue : tokenValues) {
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 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) {

View File

@ -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<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,
NoSuchMethodException, IOException {
Method method = TestMultipartForm.class.getMethod("withParamFileBinaryPart", String.class,
@ -710,15 +740,21 @@ public class RestAnnotationProcessorTest {
GeneratedHttpRequest<TestMultipartForm> httpRequest = factory(TestMultipartForm.class)
.createRequest(method, "name", file);
assertRequestLineEquals(httpRequest, "POST http://localhost:9999 HTTP/1.1");
assertHeadersEqual(httpRequest,
"Content-Length: 194\nContent-Type: multipart/form-data; boundary=--JCLOUDS--\n");
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" + //

View File

@ -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);
}