Issue 75: Implemented object PUT and DELETE.

git-svn-id: http://jclouds.googlecode.com/svn/trunk@1634 3d8758e0-26b5-11de-8745-db77d3ebf521
This commit is contained in:
jamurty 2009-07-17 06:22:56 +00:00
parent 0c4b0203a8
commit b25f3fbed7
9 changed files with 623 additions and 8 deletions

View File

@ -40,7 +40,6 @@ import org.jclouds.aws.s3.domain.AccessControlList;
import org.jclouds.aws.s3.domain.S3Bucket;
import org.jclouds.aws.s3.domain.S3Object;
import org.jclouds.aws.s3.filters.RequestAuthorizeSignature;
import org.jclouds.aws.s3.functions.ParseETagHeader;
import org.jclouds.aws.s3.functions.ParseMetadataFromHeaders;
import org.jclouds.aws.s3.functions.ParseObjectFromHeadersAndHttpContent;
import org.jclouds.aws.s3.functions.ReturnFalseOn404;
@ -60,6 +59,7 @@ import org.jclouds.aws.s3.xml.AccessControlListHandler;
import org.jclouds.aws.s3.xml.CopyObjectHandler;
import org.jclouds.aws.s3.xml.ListAllMyBucketsHandler;
import org.jclouds.aws.s3.xml.ListBucketHandler;
import org.jclouds.http.functions.ParseETagHeader;
import org.jclouds.http.options.GetOptions;
import org.jclouds.rest.EntityParam;
import org.jclouds.rest.ExceptionParser;

View File

@ -21,12 +21,11 @@
* under the License.
* ====================================================================
*/
package org.jclouds.aws.s3.functions;
package org.jclouds.http.functions;
import javax.ws.rs.core.HttpHeaders;
import org.apache.commons.io.IOUtils;
import org.jclouds.aws.s3.reference.S3Headers;
import org.jclouds.http.HttpException;
import org.jclouds.http.HttpResponse;
import org.jclouds.http.HttpUtils;
@ -34,7 +33,7 @@ import org.jclouds.http.HttpUtils;
import com.google.common.base.Function;
/**
* Parses an MD5 checksum from the header {@link S3Headers#ETAG}.
* Parses an MD5 checksum from the header {@link HttpHeaders#ETAG}.
*
* @author Adrian Cole
*/
@ -44,6 +43,10 @@ public class ParseETagHeader implements Function<HttpResponse, byte[]> {
IOUtils.closeQuietly(from.getContent());
String eTag = from.getFirstHeaderOrNull(HttpHeaders.ETAG);
if (eTag == null) {
// TODO: Cloud Files sends incorrectly cased ETag header... Remove this when fixed.
eTag = from.getFirstHeaderOrNull("Etag");
}
if (eTag != null) {
return HttpUtils.fromHexString(eTag.replaceAll("\"", ""));
}

View File

@ -126,9 +126,14 @@ public class JavaUrlHttpCommandExecutorService extends
connection.setInstanceFollowRedirects(false);
connection.setRequestMethod(request.getMethod().toString());
for (String header : request.getHeaders().keySet()) {
for (String value : request.getHeaders().get(header))
for (String value : request.getHeaders().get(header)) {
connection.setRequestProperty(header, value);
}
if ("Transfer-Encoding".equals(header) && "chunked".equals(value)) {
connection.setChunkedStreamingMode(8192);
}
}
}
connection.setRequestProperty(HttpHeaders.HOST, request.getEndpoint().getHost());
if (request.getEntity() != null) {
OutputStream out = connection.getOutputStream();

View File

@ -34,15 +34,21 @@ import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import org.jclouds.http.functions.ParseETagHeader;
import org.jclouds.rackspace.cloudfiles.binders.CFObjectBinder;
import org.jclouds.rackspace.cloudfiles.domain.AccountMetadata;
import org.jclouds.rackspace.cloudfiles.domain.CFObject;
import org.jclouds.rackspace.cloudfiles.domain.ContainerMetadata;
import org.jclouds.rackspace.cloudfiles.functions.CFObjectKey;
import org.jclouds.rackspace.cloudfiles.functions.ParseAccountMetadataResponseFromHeaders;
import org.jclouds.rackspace.cloudfiles.functions.ParseContainerListFromGsonResponse;
import org.jclouds.rackspace.cloudfiles.functions.ReturnTrueOn204FalseOtherwise;
import org.jclouds.rackspace.cloudfiles.functions.ReturnTrueOn404FalseOtherwise;
import org.jclouds.rackspace.cloudfiles.options.ListContainerOptions;
import org.jclouds.rackspace.filters.AuthenticateRequest;
import org.jclouds.rest.EntityParam;
import org.jclouds.rest.ExceptionParser;
import org.jclouds.rest.PathParamParser;
import org.jclouds.rest.Query;
import org.jclouds.rest.RequestFilters;
import org.jclouds.rest.ResponseParser;
@ -89,4 +95,18 @@ public interface CloudFilesConnection {
@Path("{container}")
boolean deleteContainerIfEmpty(@PathParam("container") String container);
@PUT
@Path("{container}/{key}")
@ResponseParser(ParseETagHeader.class)
Future<byte[]> putObject(
@PathParam("container") String container,
@PathParam("key") @PathParamParser(CFObjectKey.class) @EntityParam(CFObjectBinder.class)
CFObject object);
@DELETE
@ResponseParser(ReturnTrueOn204FalseOtherwise.class)
@ExceptionParser(ReturnTrueOn404FalseOtherwise.class)
@Path("{container}/{key}")
boolean deleteObject(@PathParam("container") String container, @PathParam("key") String key);
}

View File

@ -0,0 +1,82 @@
/**
*
* Copyright (C) 2009 Global Cloud Specialists, Inc. <info@globalcloudspecialists.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.rackspace.cloudfiles.binders;
import static com.google.common.base.Preconditions.checkNotNull;
import java.io.UnsupportedEncodingException;
import javax.ws.rs.core.HttpHeaders;
import org.jclouds.http.HttpRequest;
import org.jclouds.http.HttpUtils;
import org.jclouds.rackspace.cloudfiles.domain.CFObject;
import org.jclouds.rest.EntityBinder;
public class CFObjectBinder implements EntityBinder {
public void addEntityToRequest(Object entity, HttpRequest request) {
CFObject object = (CFObject) entity;
request.setEntity(checkNotNull(object.getData(), "object.getData()"));
if (object.getMetadata().getSize() >= 0) {
request.getHeaders().put(HttpHeaders.CONTENT_LENGTH, object.getMetadata().getSize() + "");
} else {
// Enable "chunked"/"streamed" data, where the size needn't be known in advance.
request.getHeaders().put("Transfer-Encoding", "chunked");
}
request.getHeaders()
.put(
HttpHeaders.CONTENT_TYPE,
checkNotNull(object.getMetadata().getContentType(),
"object.metadata.contentType()"));
if (object.getMetadata().getCacheControl() != null) {
request.getHeaders()
.put(HttpHeaders.CACHE_CONTROL, object.getMetadata().getCacheControl());
}
if (object.getMetadata().getContentDisposition() != null) {
request.getHeaders().put("Content-Disposition",
object.getMetadata().getContentDisposition());
}
if (object.getMetadata().getContentEncoding() != null) {
request.getHeaders().put(HttpHeaders.CONTENT_ENCODING,
object.getMetadata().getContentEncoding());
}
if (object.getMetadata().getETag() != null) {
try {
String hexETag = HttpUtils.toHexString(object.getMetadata().getETag());
request.getHeaders().put(HttpHeaders.ETAG, hexETag);
} catch (UnsupportedEncodingException e) {
// TODO: Any sane way to recover? Should EntityBinder#addEntityToRequest throw errors?
}
}
request.getHeaders().putAll(object.getMetadata().getUserMetadata());
}
}

View File

@ -0,0 +1,418 @@
/**
*
* Copyright (C) 2009 Global Cloud Specialists, Inc. <info@globalcloudspecialists.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.rackspace.cloudfiles.domain;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import javax.ws.rs.core.MediaType;
import org.jclouds.http.HttpUtils;
import org.jclouds.http.HttpUtils.ETagInputStreamResult;
import org.joda.time.DateTime;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
/**
* Rackspace Cloud Files is designed to store objects. Objects are stored in
* {@link ContainerMetadata containers} and consist of a
* {@link CFObject#getData() value}, a {@link CFObject#getKey() key}, and
* {@link CFObject.Metadata#getUserMetadata() metadata}.
*
* @author Adrian Cole
* @author James Murty
*/
public class CFObject {
public static final CFObject NOT_FOUND = new CFObject(Metadata.NOT_FOUND);
private Object data;
private final Metadata metadata;
private long contentLength = -1;
private String contentRange;
public CFObject(String key) {
this(new Metadata(key));
}
public CFObject(Metadata metadata) {
this.metadata = metadata;
}
public CFObject(Metadata metadata, Object data) {
this(metadata);
setData(data);
}
public CFObject(String key, Object data) {
this(key);
setData(data);
}
/**
* System and user Metadata for the {@link CFObject}.
*
* @author Adrian Cole
*/
public static class Metadata implements Comparable<Metadata> {
public static final Metadata NOT_FOUND = new Metadata("NOT_FOUND");
// parsed during list, head, or get
private String key;
private byte[] eTag;
private volatile long size = -1;
// only parsed during head or get
private Multimap<String, String> allHeaders = HashMultimap.create();
private Multimap<String, String> userMetadata = HashMultimap.create();
private DateTime lastModified;
private String dataType = MediaType.APPLICATION_OCTET_STREAM;
private String cacheControl;
private String dataDisposition;
private String dataEncoding;
public Metadata() {
super();
}
/**
* @param key
* @see #getKey()
*/
public Metadata(String key) {
setKey(key);
}
public void setKey(String key) {
checkNotNull(key, "key");
checkArgument(!key.startsWith("/"), "keys cannot start with /");
this.key = key;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
sb.append("Metadata");
sb.append("{key='").append(key).append('\'');
sb.append(", lastModified=").append(lastModified);
sb.append(", eTag=").append(
getETag() == null ? "null" : Arrays.asList(getETag()).toString());
sb.append(", size=").append(size);
sb.append(", dataType='").append(dataType).append('\'');
sb.append('}');
return sb.toString();
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (!(o instanceof Metadata))
return false;
Metadata metadata = (Metadata) o;
if (size != metadata.size)
return false;
if (dataType != null ? !dataType.equals(metadata.dataType) : metadata.dataType != null)
return false;
if (!key.equals(metadata.key))
return false;
if (lastModified != null ? !lastModified.equals(metadata.lastModified)
: metadata.lastModified != null)
return false;
if (!Arrays.equals(getETag(), metadata.getETag()))
return false;
return true;
}
@Override
public int hashCode() {
int result = key.hashCode();
result = 31 * result + (lastModified != null ? lastModified.hashCode() : 0);
result = 31 * result + (getETag() != null ? Arrays.hashCode(getETag()) : 0);
result = 31 * result + (int) (size ^ (size >>> 32));
result = 31 * result + (dataType != null ? dataType.hashCode() : 0);
return result;
}
/**
* The key is the handle that you assign to an object that allows you retrieve it later. A key
* is a sequence of Unicode characters whose UTF-8 encoding is at most 1024 bytes long. Each
* object in a bucket must have a unique key.
*/
public String getKey() {
return key;
}
public DateTime getLastModified() {
return lastModified;
}
public void setLastModified(DateTime lastModified) {
this.lastModified = lastModified;
}
/**
* The size of the object, in bytes.
*
* @see <a href= "http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html?sec14.13." />
*/
public long getSize() {
return size;
}
public void setSize(long size) {
this.size = size;
}
/**
* A standard MIME type describing the format of the contents. If none is provided, the
* default is binary/octet-stream.
*
* @see <a href= "http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html?sec14.17." />
*/
public String getContentType() {
return dataType;
}
public void setContentType(String dataType) {
this.dataType = dataType;
}
public void setETag(byte[] eTag) {
this.eTag = new byte[eTag.length];
System.arraycopy(eTag, 0, this.eTag, 0, eTag.length);
}
/**
* @return the eTag value stored in the Etag header returned by the service.
*/
public byte[] getETag() {
if (eTag != null) {
byte[] retval = new byte[eTag.length];
System.arraycopy(this.eTag, 0, retval, 0, eTag.length);
return retval;
} else {
return null;
}
}
public void setUserMetadata(Multimap<String, String> userMetadata) {
this.userMetadata = userMetadata;
}
/**
* Any header starting with <code>X-Object-Meta-</code> is considered user metadata. It will
* be stored with the object and returned when you retrieve the object. The total size of the
* HTTP request, not including the body, must be less than 8 KB.
*/
public Multimap<String, String> getUserMetadata() {
return userMetadata;
}
public void setCacheControl(String cacheControl) {
this.cacheControl = cacheControl;
}
/**
* Can be used to specify caching behavior along the request/reply chain.
*
* @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html?sec14.9.
*/
public String getCacheControl() {
return cacheControl;
}
public void setContentDisposition(String dataDisposition) {
this.dataDisposition = dataDisposition;
}
/**
* Specifies presentational information for the object.
*
* @see <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html?sec19.5.1."/>
*/
public String getContentDisposition() {
return dataDisposition;
}
public void setContentEncoding(String dataEncoding) {
this.dataEncoding = dataEncoding;
}
/**
* Specifies what content encodings have been applied to the object and thus what decoding
* mechanisms must be applied in order to obtain the media-type referenced by the Content-Type
* header field.
*
* @see <a href= "http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html?sec14.11" />
*/
public String getContentEncoding() {
return dataEncoding;
}
public void setAllHeaders(Multimap<String, String> allHeaders) {
this.allHeaders = allHeaders;
}
/**
* @return all http response headers associated with this object
*/
public Multimap<String, String> getAllHeaders() {
return allHeaders;
}
public int compareTo(Metadata o) {
return (this == o) ? 0 : getKey().compareTo(o.getKey());
}
}
/**
* @see Metadata#getKey()
*/
public String getKey() {
return metadata.getKey();
}
/**
* Sets entity for the request or the content from the response.
*
* @param data
* typically InputStream for downloads, or File, byte [], String, or InputStream for
* uploads.
*/
public void setData(Object data) {
this.data = checkNotNull(data, "data");
}
/**
* generate an MD5 Hash for the current data.
* <p/>
* <h2>Note</h2>
* <p/>
* If this is an InputStream, it will be converted to a byte array first.
*
* @throws IOException
* if there is a problem generating the hash.
*/
public void generateETag() throws IOException {
checkState(data != null, "data");
if (data instanceof InputStream) {
ETagInputStreamResult result = HttpUtils.generateETagResult((InputStream) data);
getMetadata().setSize(result.length);
getMetadata().setETag(result.eTag);
setData(result.data);
} else {
getMetadata().setETag(HttpUtils.eTag(data));
}
}
/**
* @return InputStream, if downloading, or whatever was set during {@link #setData(Object)}
*/
public Object getData() {
return data;
}
/**
* @return System and User metadata relevant to this object.
*/
public Metadata getMetadata() {
return metadata;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
sb.append("CFObject");
sb.append("{metadata=").append(metadata);
sb.append('}');
return sb.toString();
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (!(o instanceof CFObject))
return false;
CFObject cfObject = (CFObject) o;
if (data != null ? !data.equals(cfObject.data) : cfObject.data != null)
return false;
if (!metadata.equals(cfObject.metadata))
return false;
return true;
}
@Override
public int hashCode() {
int result = data != null ? data.hashCode() : 0;
result = 31 * result + metadata.hashCode();
return result;
}
public void setContentLength(long contentLength) {
this.contentLength = contentLength;
}
/**
* Returns the total size of the downloaded object, or the chunk that's available.
* <p/>
* Chunking is only used when
* TODO: Does Cloud Files support content ranges?
* is called with options like tail, range, or startAt.
*
* @return the length in bytes that can be be obtained from {@link #getData()}
* @see org.jclouds.http.HttpHeaders#CONTENT_LENGTH
* @see GetObjectOptions
*/
public long getContentLength() {
return contentLength;
}
public void setContentRange(String contentRange) {
this.contentRange = contentRange;
}
/**
* If this is not-null, {@link #getContentLength() } will the size of chunk of the object
* available via {@link #getData()}
*
* @see org.jclouds.http.HttpHeaders#CONTENT_RANGE
* @see GetObjectOptions
*/
public String getContentRange() {
return contentRange;
}
}

View File

@ -0,0 +1,36 @@
/**
*
* Copyright (C) 2009 Global Cloud Specialists, Inc. <info@globalcloudspecialists.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.rackspace.cloudfiles.functions;
import org.jclouds.rackspace.cloudfiles.domain.CFObject;
import com.google.common.base.Function;
public class CFObjectKey implements Function<Object, String> {
public String apply(Object from) {
return ((CFObject) from).getKey();
}
}

View File

@ -42,5 +42,5 @@ public interface CloudFilesHeaders {
public static final String CDN_USER_AGENT_ACL = "X-User-Agent-ACL";
public static final String CONTAINER_BYTES_USED = "X-Container-Bytes-Used";
public static final String CONTAINER_OBJECT_COUNT = "X-Container-Object-Count";
public static final String USER_METADATA_PREFIX = "X-Object-Meta-";
}

View File

@ -30,11 +30,17 @@ import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;
import java.io.ByteArrayInputStream;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.jclouds.http.HttpResponseException;
import org.jclouds.http.HttpUtils;
import org.jclouds.rackspace.cloudfiles.domain.AccountMetadata;
import org.jclouds.rackspace.cloudfiles.domain.CFObject;
import org.jclouds.rackspace.cloudfiles.domain.ContainerMetadata;
import org.jclouds.rackspace.cloudfiles.options.ListContainerOptions;
import org.jclouds.rackspace.cloudfiles.reference.CloudFilesHeaders;
import org.testng.annotations.Test;
/**
@ -134,5 +140,50 @@ public class CloudFilesConnectionLiveTest {
assertTrue(connection.deleteContainerIfEmpty(containerName1));
assertTrue(connection.deleteContainerIfEmpty(containerName2));
}
@Test
public void testPutAndDeleteObjects() throws Exception {
CloudFilesConnection connection = CloudFilesContextBuilder.newBuilder(sysRackspaceUser,
sysRackspaceKey).withJsonDebug().buildContext().getConnection();
String containerName = bucketPrefix + ".testPutAndDeleteObjects";
String data = "Here is my data";
assertTrue(connection.putContainer(containerName));
// Test with string data, ETag hash, and a piece of metadata
CFObject object = new CFObject("object", data);
object.setContentLength(data.length());
object.generateETag();
object.getMetadata().setContentType("text/plain");
// TODO: Metadata values aren't being stored by CF, but the names are. Odd...
object.getMetadata().getUserMetadata().put(
CloudFilesHeaders.USER_METADATA_PREFIX + "metadata", "metadata-value");
byte[] md5 = connection.putObject(containerName, object).get(10, TimeUnit.SECONDS);
assertEquals(HttpUtils.toHexString(md5),
HttpUtils.toHexString(object.getMetadata().getETag()));
// TODO: Get and confirm data
// Test with invalid ETag (as if object's data was corrupted in transit)
String correctEtag = HttpUtils.toHexString(object.getMetadata().getETag());
String incorrectEtag = "0" + correctEtag.substring(1);
object.getMetadata().setETag(HttpUtils.fromHexString(incorrectEtag));
try {
connection.putObject(containerName, object).get(10, TimeUnit.SECONDS);
} catch (Throwable e) {
assertEquals(e.getCause().getClass(), HttpResponseException.class);
assertEquals(((HttpResponseException)e.getCause()).getResponse().getStatusCode(), 422);
}
// Test chunked/streamed upload with data of "unknown" length
ByteArrayInputStream bais = new ByteArrayInputStream(data.getBytes("UTF-8"));
object = new CFObject("chunked-object", bais);
md5 = connection.putObject(containerName, object).get(10, TimeUnit.SECONDS);
assertEquals(HttpUtils.toHexString(md5), correctEtag);
// TODO: Get and confirm data
assertTrue(connection.deleteObject(containerName, "object"));
assertTrue(connection.deleteObject(containerName, "chunked-object"));
assertTrue(connection.deleteContainerIfEmpty(containerName));
}
}