Issue 75: Implemented HEAD, POST and (simple) GET for objects

git-svn-id: http://jclouds.googlecode.com/svn/trunk@1642 3d8758e0-26b5-11de-8745-db77d3ebf521
This commit is contained in:
jamurty 2009-07-17 23:01:31 +00:00
parent 14053f0341
commit 1e56bb6eea
9 changed files with 459 additions and 10 deletions

View File

@ -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.
* <p/>
@ -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<CFObject> 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<String, String> userMetadata);
@DELETE
@ResponseParser(ReturnTrueOn204FalseOtherwise.class)
@ExceptionParser(ReturnTrueOn404FalseOtherwise.class)

View File

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

View File

@ -0,0 +1,39 @@
/**
*
* 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 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<String, String> userMetadata = (Multimap<String, String>) entity;
request.getHeaders().putAll(userMetadata);
}
}

View File

@ -0,0 +1,83 @@
/**
*
* 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 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<HttpResponse, CFObject> {
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)));
}
}
}

View File

@ -0,0 +1,128 @@
/**
*
* 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 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<HttpResponse, CFObject.Metadata> {
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: /<api version>/<account>/<container>/<object key>
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<String, String> header : from.getHeaders().entries()) {
if (header.getKey() != null
&& header.getKey().startsWith(CloudFilesHeaders.USER_METADATA_PREFIX))
{
metadata.getUserMetadata().put(header.getKey(), header.getValue());
}
}
}
}

View File

@ -0,0 +1,43 @@
/**
*
* 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.http.HttpResponseException;
import org.jclouds.rackspace.cloudfiles.domain.CFObject.Metadata;
import com.google.common.base.Function;
public class ReturnCFObjectMetadataNotFoundOn404 implements Function<Exception, Metadata> {
public Metadata apply(Exception from) {
if (from instanceof HttpResponseException) {
HttpResponseException responseException = (HttpResponseException) from;
if (responseException.getResponse().getStatusCode() == 404) {
return Metadata.NOT_FOUND;
}
}
return null;
}
}

View File

@ -0,0 +1,43 @@
/**
*
* 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.http.HttpResponseException;
import org.jclouds.rackspace.cloudfiles.domain.CFObject;
import com.google.common.base.Function;
public class ReturnS3ObjectNotFoundOn404 implements Function<Exception, CFObject> {
public CFObject apply(Exception from) {
if (from instanceof HttpResponseException) {
HttpResponseException responseException = (HttpResponseException) from;
if (responseException.getResponse().getStatusCode() == 404) {
return CFObject.NOT_FOUND;
}
}
return null;
}
}

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.http.HttpResponse;
import com.google.common.base.Function;
public class ReturnTrueOn202FalseOtherwise implements Function<HttpResponse, Boolean> {
public Boolean apply(HttpResponse from) {
return (from.getStatusCode() == 202);
}
}

View File

@ -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<String, String> 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);