mirror of https://github.com/apache/jclouds.git
Issue 191: corrected multipart form and added cookbook signing to chef client
This commit is contained in:
parent
306bb0ebde
commit
f25100fe9b
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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" + //
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue