diff --git a/rackspace/cloudfiles/core/src/main/java/org/jclouds/rackspace/cloudfiles/CloudFilesConnection.java b/rackspace/cloudfiles/core/src/main/java/org/jclouds/rackspace/cloudfiles/CloudFilesConnection.java index 6373bcbf0d..c604302fdf 100644 --- a/rackspace/cloudfiles/core/src/main/java/org/jclouds/rackspace/cloudfiles/CloudFilesConnection.java +++ b/rackspace/cloudfiles/core/src/main/java/org/jclouds/rackspace/cloudfiles/CloudFilesConnection.java @@ -30,18 +30,25 @@ import java.util.concurrent.Future; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.HEAD; +import javax.ws.rs.POST; 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.binders.UserMetadataBinder; 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.ParseObjectFromHeadersAndHttpContent; +import org.jclouds.rackspace.cloudfiles.functions.ParseObjectMetadataFromHeaders; +import org.jclouds.rackspace.cloudfiles.functions.ReturnCFObjectMetadataNotFoundOn404; +import org.jclouds.rackspace.cloudfiles.functions.ReturnS3ObjectNotFoundOn404; +import org.jclouds.rackspace.cloudfiles.functions.ReturnTrueOn202FalseOtherwise; import org.jclouds.rackspace.cloudfiles.functions.ReturnTrueOn204FalseOtherwise; import org.jclouds.rackspace.cloudfiles.functions.ReturnTrueOn404FalseOtherwise; import org.jclouds.rackspace.cloudfiles.options.ListContainerOptions; @@ -54,6 +61,8 @@ import org.jclouds.rest.RequestFilters; import org.jclouds.rest.ResponseParser; import org.jclouds.rest.SkipEncoding; +import com.google.common.collect.Multimap; + /** * Provides access to Cloud Files via their REST API. *

@@ -103,6 +112,29 @@ public interface CloudFilesConnection { @PathParam("key") @PathParamParser(CFObjectKey.class) @EntityParam(CFObjectBinder.class) CFObject object); + @HEAD + @ResponseParser(ParseObjectMetadataFromHeaders.class) + @ExceptionParser(ReturnCFObjectMetadataNotFoundOn404.class) + @Path("{container}/{key}") + CFObject.Metadata headObject(@PathParam("container") String container, + @PathParam("key") String key); + + @GET + @ResponseParser(ParseObjectFromHeadersAndHttpContent.class) + @ExceptionParser(ReturnS3ObjectNotFoundOn404.class) + @Path("{container}/{key}") + Future getObject(@PathParam("container") String container, + @PathParam("key") String key); + + // TODO: GET object with options + + @POST + @ResponseParser(ReturnTrueOn202FalseOtherwise.class) + @Path("{container}/{key}") + boolean setObjectMetadata(@PathParam("container") String container, + @PathParam("key") String key, + @EntityParam(UserMetadataBinder.class) Multimap userMetadata); + @DELETE @ResponseParser(ReturnTrueOn204FalseOtherwise.class) @ExceptionParser(ReturnTrueOn404FalseOtherwise.class) diff --git a/rackspace/cloudfiles/core/src/main/java/org/jclouds/rackspace/cloudfiles/binders/CFObjectBinder.java b/rackspace/cloudfiles/core/src/main/java/org/jclouds/rackspace/cloudfiles/binders/CFObjectBinder.java index a15a86b677..10cc2f9e7c 100644 --- a/rackspace/cloudfiles/core/src/main/java/org/jclouds/rackspace/cloudfiles/binders/CFObjectBinder.java +++ b/rackspace/cloudfiles/core/src/main/java/org/jclouds/rackspace/cloudfiles/binders/CFObjectBinder.java @@ -72,7 +72,7 @@ public class CFObjectBinder implements EntityBinder { 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? + throw new RuntimeException("Failed to encode ETag for object: " + object, e); } } diff --git a/rackspace/cloudfiles/core/src/main/java/org/jclouds/rackspace/cloudfiles/binders/UserMetadataBinder.java b/rackspace/cloudfiles/core/src/main/java/org/jclouds/rackspace/cloudfiles/binders/UserMetadataBinder.java new file mode 100644 index 0000000000..9a375b89df --- /dev/null +++ b/rackspace/cloudfiles/core/src/main/java/org/jclouds/rackspace/cloudfiles/binders/UserMetadataBinder.java @@ -0,0 +1,39 @@ +/** + * + * Copyright (C) 2009 Global Cloud Specialists, Inc. + * + * ==================================================================== + * 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 org.jclouds.http.HttpRequest; +import org.jclouds.rest.EntityBinder; + +import com.google.common.collect.Multimap; + +public class UserMetadataBinder implements EntityBinder { + + @SuppressWarnings("unchecked") + public void addEntityToRequest(Object entity, HttpRequest request) { + Multimap userMetadata = (Multimap) entity; + request.getHeaders().putAll(userMetadata); + } + +} diff --git a/rackspace/cloudfiles/core/src/main/java/org/jclouds/rackspace/cloudfiles/functions/ParseObjectFromHeadersAndHttpContent.java b/rackspace/cloudfiles/core/src/main/java/org/jclouds/rackspace/cloudfiles/functions/ParseObjectFromHeadersAndHttpContent.java new file mode 100644 index 0000000000..107fa58b37 --- /dev/null +++ b/rackspace/cloudfiles/core/src/main/java/org/jclouds/rackspace/cloudfiles/functions/ParseObjectFromHeadersAndHttpContent.java @@ -0,0 +1,83 @@ +/** + * + * Copyright (C) 2009 Global Cloud Specialists, Inc. + * + * ==================================================================== + * 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 javax.ws.rs.core.HttpHeaders; + +import org.jclouds.http.HttpException; +import org.jclouds.http.HttpResponse; +import org.jclouds.rackspace.cloudfiles.domain.CFObject; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Function; +import com.google.inject.Inject; + +/** + * Parses response headers and creates a new CFObject from them and the HTTP content. + * + * @see ParseMetadataFromHeaders + * @author Adrian Cole + */ +public class ParseObjectFromHeadersAndHttpContent implements Function { + private final ParseObjectMetadataFromHeaders metadataParser; + + @Inject + public ParseObjectFromHeadersAndHttpContent(ParseObjectMetadataFromHeaders metadataParser) { + this.metadataParser = metadataParser; + } + + /** + * First, calls {@link ParseMetadataFromHeaders}. + * + * Then, sets the object size based on the Content-Length header and adds the content to the + * {@link S3Object} result. + * + * @throws org.jclouds.http.HttpException + */ + public CFObject apply(HttpResponse from) { + CFObject.Metadata metadata = metadataParser.apply(from); + CFObject object = new CFObject(metadata, from.getContent()); + parseContentLengthOrThrowException(from, object); + return object; + } + + @VisibleForTesting + void parseContentLengthOrThrowException(HttpResponse from, CFObject object) throws HttpException { + String contentLength = from.getFirstHeaderOrNull(HttpHeaders.CONTENT_LENGTH); + String contentRange = from.getFirstHeaderOrNull("Content-Range"); + if (contentLength == null) + throw new HttpException(HttpHeaders.CONTENT_LENGTH + " header not present in headers: " + + from.getHeaders()); + object.setContentLength(Long.parseLong(contentLength)); + + if (contentRange == null) { + object.getMetadata().setSize(object.getContentLength()); + } else { + object.setContentRange(contentRange); + object.getMetadata().setSize( + Long.parseLong(contentRange.substring(contentRange.lastIndexOf('/') + 1))); + } + } + +} \ No newline at end of file diff --git a/rackspace/cloudfiles/core/src/main/java/org/jclouds/rackspace/cloudfiles/functions/ParseObjectMetadataFromHeaders.java b/rackspace/cloudfiles/core/src/main/java/org/jclouds/rackspace/cloudfiles/functions/ParseObjectMetadataFromHeaders.java new file mode 100644 index 0000000000..3b47c46f86 --- /dev/null +++ b/rackspace/cloudfiles/core/src/main/java/org/jclouds/rackspace/cloudfiles/functions/ParseObjectMetadataFromHeaders.java @@ -0,0 +1,128 @@ +/** + * + * Copyright (C) 2009 Global Cloud Specialists, Inc. + * + * ==================================================================== + * 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 java.util.Map.Entry; + +import javax.ws.rs.core.HttpHeaders; + +import org.jclouds.http.HttpException; +import org.jclouds.http.HttpResponse; +import org.jclouds.http.HttpUtils; +import org.jclouds.rackspace.cloudfiles.domain.CFObject; +import org.jclouds.rackspace.cloudfiles.domain.CFObject.Metadata; +import org.jclouds.rackspace.cloudfiles.reference.CloudFilesHeaders; +import org.jclouds.util.DateService; + +import com.google.common.base.Function; +import com.google.inject.Inject; + +/** + * This parses @{link {@link CFObject.Metadata} from HTTP headers. + * + * @author Adrian Cole + */ +public class ParseObjectMetadataFromHeaders implements Function { + private final DateService dateParser; + + @Inject + public ParseObjectMetadataFromHeaders(DateService dateParser) { + this.dateParser = dateParser; + } + + /** + * parses the http response headers to create a new + * {@link CFObject.Metadata} object. + */ + public Metadata apply(HttpResponse from) { + // URL Path components: //// + String[] pathElements = from.getRequestURL().getPath().split("/"); + String objectKey = from.getRequestURL().getPath().substring( + (pathElements[1] + pathElements[2] + pathElements[3]).length() + 4); + + CFObject.Metadata to = new CFObject.Metadata(objectKey); + addAllHeadersTo(from, to); + + addUserMetadataTo(from, to); + addETagTo(from, to); + + parseLastModifiedOrThrowException(from, to); + setContentTypeOrThrowException(from, to); + setContentLengthOrThrowException(from, to); + + to.setCacheControl(from.getFirstHeaderOrNull(HttpHeaders.CACHE_CONTROL)); + to.setContentDisposition(from.getFirstHeaderOrNull("Content-Disposition")); + to.setContentEncoding(from.getFirstHeaderOrNull(HttpHeaders.CONTENT_ENCODING)); + return to; + } + + private void addAllHeadersTo(HttpResponse from, Metadata metadata) { + metadata.getAllHeaders().putAll(from.getHeaders()); + } + + private void setContentTypeOrThrowException(HttpResponse from, Metadata metadata) + throws HttpException { + String contentType = from.getFirstHeaderOrNull(HttpHeaders.CONTENT_TYPE); + if (contentType == null) + throw new HttpException(HttpHeaders.CONTENT_TYPE + " not found in headers"); + else + metadata.setContentType(contentType); + } + + private void setContentLengthOrThrowException(HttpResponse from, Metadata metadata) + throws HttpException { + String contentLength = from.getFirstHeaderOrNull(HttpHeaders.CONTENT_LENGTH); + if (contentLength == null) + throw new HttpException(HttpHeaders.CONTENT_LENGTH + " not found in headers"); + else + metadata.setSize(Long.parseLong(contentLength)); + } + + private void parseLastModifiedOrThrowException(HttpResponse from, Metadata metadata) + throws HttpException { + String lastModified = from.getFirstHeaderOrNull(HttpHeaders.LAST_MODIFIED); + metadata.setLastModified(dateParser.rfc822DateParse(lastModified)); + if (metadata.getLastModified() == null) + throw new HttpException("could not parse: " + HttpHeaders.LAST_MODIFIED + ": " + + lastModified); + } + + private void addETagTo(HttpResponse from, Metadata metadata) { + String eTag = from.getFirstHeaderOrNull("Etag"); // TODO: Should be HttpHeaders.ETAG + if (metadata.getETag() == null && eTag != null) { + metadata.setETag(HttpUtils.fromHexString(eTag.replaceAll("\"", ""))); + } + } + + private void addUserMetadataTo(HttpResponse from, Metadata metadata) { + for (Entry header : from.getHeaders().entries()) { + if (header.getKey() != null + && header.getKey().startsWith(CloudFilesHeaders.USER_METADATA_PREFIX)) + { + metadata.getUserMetadata().put(header.getKey(), header.getValue()); + } + } + } + +} \ No newline at end of file diff --git a/rackspace/cloudfiles/core/src/main/java/org/jclouds/rackspace/cloudfiles/functions/ReturnCFObjectMetadataNotFoundOn404.java b/rackspace/cloudfiles/core/src/main/java/org/jclouds/rackspace/cloudfiles/functions/ReturnCFObjectMetadataNotFoundOn404.java new file mode 100644 index 0000000000..0e1aa1e390 --- /dev/null +++ b/rackspace/cloudfiles/core/src/main/java/org/jclouds/rackspace/cloudfiles/functions/ReturnCFObjectMetadataNotFoundOn404.java @@ -0,0 +1,43 @@ +/** + * + * Copyright (C) 2009 Global Cloud Specialists, Inc. + * + * ==================================================================== + * 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.http.HttpResponseException; +import org.jclouds.rackspace.cloudfiles.domain.CFObject.Metadata; + +import com.google.common.base.Function; + +public class ReturnCFObjectMetadataNotFoundOn404 implements Function { + + public Metadata apply(Exception from) { + if (from instanceof HttpResponseException) { + HttpResponseException responseException = (HttpResponseException) from; + if (responseException.getResponse().getStatusCode() == 404) { + return Metadata.NOT_FOUND; + } + } + return null; + } + +} diff --git a/rackspace/cloudfiles/core/src/main/java/org/jclouds/rackspace/cloudfiles/functions/ReturnS3ObjectNotFoundOn404.java b/rackspace/cloudfiles/core/src/main/java/org/jclouds/rackspace/cloudfiles/functions/ReturnS3ObjectNotFoundOn404.java new file mode 100644 index 0000000000..4d8bb17305 --- /dev/null +++ b/rackspace/cloudfiles/core/src/main/java/org/jclouds/rackspace/cloudfiles/functions/ReturnS3ObjectNotFoundOn404.java @@ -0,0 +1,43 @@ +/** + * + * Copyright (C) 2009 Global Cloud Specialists, Inc. + * + * ==================================================================== + * 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.http.HttpResponseException; +import org.jclouds.rackspace.cloudfiles.domain.CFObject; + +import com.google.common.base.Function; + +public class ReturnS3ObjectNotFoundOn404 implements Function { + + public CFObject apply(Exception from) { + if (from instanceof HttpResponseException) { + HttpResponseException responseException = (HttpResponseException) from; + if (responseException.getResponse().getStatusCode() == 404) { + return CFObject.NOT_FOUND; + } + } + return null; + } + +} diff --git a/rackspace/cloudfiles/core/src/main/java/org/jclouds/rackspace/cloudfiles/functions/ReturnTrueOn202FalseOtherwise.java b/rackspace/cloudfiles/core/src/main/java/org/jclouds/rackspace/cloudfiles/functions/ReturnTrueOn202FalseOtherwise.java new file mode 100644 index 0000000000..54c2f08273 --- /dev/null +++ b/rackspace/cloudfiles/core/src/main/java/org/jclouds/rackspace/cloudfiles/functions/ReturnTrueOn202FalseOtherwise.java @@ -0,0 +1,36 @@ +/** + * + * Copyright (C) 2009 Global Cloud Specialists, Inc. + * + * ==================================================================== + * 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.http.HttpResponse; + +import com.google.common.base.Function; + +public class ReturnTrueOn202FalseOtherwise implements Function { + + public Boolean apply(HttpResponse from) { + return (from.getStatusCode() == 202); + } + +} diff --git a/rackspace/cloudfiles/core/src/test/java/org/jclouds/rackspace/cloudfiles/CloudFilesConnectionLiveTest.java b/rackspace/cloudfiles/core/src/test/java/org/jclouds/rackspace/cloudfiles/CloudFilesConnectionLiveTest.java index a9e1f0b86c..682c2df06f 100644 --- a/rackspace/cloudfiles/core/src/test/java/org/jclouds/rackspace/cloudfiles/CloudFilesConnectionLiveTest.java +++ b/rackspace/cloudfiles/core/src/test/java/org/jclouds/rackspace/cloudfiles/CloudFilesConnectionLiveTest.java @@ -31,9 +31,11 @@ import static org.testng.Assert.assertTrue; import static org.testng.Assert.fail; import java.io.ByteArrayInputStream; +import java.io.InputStream; import java.util.List; import java.util.concurrent.TimeUnit; +import org.apache.commons.io.IOUtils; import org.jclouds.http.HttpResponseException; import org.jclouds.http.HttpUtils; import org.jclouds.logging.log4j.config.Log4JLoggingModule; @@ -45,6 +47,10 @@ import org.jclouds.rackspace.cloudfiles.reference.CloudFilesHeaders; import org.testng.annotations.BeforeGroups; import org.testng.annotations.Test; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Iterables; +import com.google.common.collect.Multimap; + /** * Tests behavior of {@code JaxrsAnnotationProcessor} * @@ -142,26 +148,65 @@ public class CloudFilesConnectionLiveTest { } @Test - public void testPutAndDeleteObjects() throws Exception { - String containerName = bucketPrefix + ".testPutAndDeleteObjects"; + public void testObjectOperations() throws Exception { + String containerName = bucketPrefix + ".testObjectOperations"; String data = "Here is my data"; assertTrue(connection.putContainer(containerName)); - // Test with string data, ETag hash, and a piece of metadata + // Test PUT 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 + assertEquals(HttpUtils.toHexString(md5), + HttpUtils.toHexString(object.getMetadata().getETag())); + + // Test HEAD of missing object + CFObject.Metadata metadata = connection.headObject(containerName, "non-existent-object"); + assertEquals(metadata, CFObject.Metadata.NOT_FOUND); + + // Test HEAD of object + metadata = connection.headObject(containerName, object.getKey()); + assertEquals(metadata.getKey(), object.getKey()); + assertEquals(metadata.getSize(), data.length()); + assertEquals(metadata.getContentType(), "text/plain"); + assertEquals(metadata.getETag(), object.getMetadata().getETag()); + assertEquals(metadata.getUserMetadata().entries().size(), 1); + // Notice the quirk where CF changes the case of returned metadata names + assertEquals(Iterables.getLast(metadata.getUserMetadata().get( + CloudFilesHeaders.USER_METADATA_PREFIX + "Metadata")), + "metadata-value"); - // Test with invalid ETag (as if object's data was corrupted in transit) + // Test POST to update object's metadata + Multimap userMetadata = HashMultimap.create(); + userMetadata.put(CloudFilesHeaders.USER_METADATA_PREFIX + "new-metadata-1", "value-1"); + userMetadata.put(CloudFilesHeaders.USER_METADATA_PREFIX + "new-metadata-2", "value-2"); + assertTrue(connection.setObjectMetadata(containerName, object.getKey(), userMetadata)); + + // Test GET of missing object + CFObject getObject = connection.getObject(containerName, "non-existent-object") + .get(10, TimeUnit.SECONDS); + assertEquals(getObject, CFObject.NOT_FOUND); + + // Test GET of object (including updated metadata) + getObject = connection.getObject(containerName, object.getKey()).get(10, TimeUnit.SECONDS); + assertEquals(IOUtils.toString((InputStream)getObject.getData()), data); + assertEquals(getObject.getKey(), object.getKey()); + assertEquals(getObject.getContentLength(), data.length()); + assertEquals(getObject.getMetadata().getContentType(), "text/plain"); + assertEquals(getObject.getMetadata().getETag(), object.getMetadata().getETag()); + assertEquals(getObject.getMetadata().getUserMetadata().entries().size(), 2); + // Notice the quirk where CF changes the case of sreturned metadata names + assertEquals(Iterables.getLast(getObject.getMetadata().getUserMetadata().get( + CloudFilesHeaders.USER_METADATA_PREFIX + "New-Metadata-1")), "value-1"); + assertEquals(Iterables.getLast(getObject.getMetadata().getUserMetadata().get( + CloudFilesHeaders.USER_METADATA_PREFIX + "New-Metadata-2")), "value-2"); + + // Test PUT 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)); @@ -172,7 +217,7 @@ public class CloudFilesConnectionLiveTest { assertEquals(((HttpResponseException) e.getCause()).getResponse().getStatusCode(), 422); } - // Test chunked/streamed upload with data of "unknown" length + // Test PUT 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);