Updated Branches: refs/heads/master f5e4012f9 -> 2150c8b65
JCLOUDS-298. Add ObjectApi to openstack-swift Project: http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/commit/2150c8b6 Tree: http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/tree/2150c8b6 Diff: http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/diff/2150c8b6 Branch: refs/heads/master Commit: 2150c8b65c15aaf0d2865ad894e9cdf86d0a8cb4 Parents: f5e4012 Author: Adrian Cole <[email protected]> Authored: Sat Sep 28 09:50:40 2013 -0700 Committer: Adrian Cole <[email protected]> Committed: Sat Sep 28 12:10:53 2013 -0700 ---------------------------------------------------------------------- .../jclouds/openstack/swift/v1/SwiftApi.java | 7 +- .../swift/v1/binders/BindMetadataToHeaders.java | 12 + .../openstack/swift/v1/domain/SwiftObject.java | 219 +++++++++++++ .../openstack/swift/v1/features/ObjectApi.java | 202 +++++++++++- .../swift/v1/functions/ETagHeader.java | 31 ++ .../v1/functions/ParseAccountFromHeaders.java | 4 +- .../v1/functions/ParseContainerFromHeaders.java | 2 +- .../v1/functions/ParseObjectFromResponse.java | 59 ++++ .../swift/v1/features/ObjectApiLiveTest.java | 139 +++++++++ .../swift/v1/features/ObjectApiMockTest.java | 309 +++++++++++++++++++ 10 files changed, 970 insertions(+), 14 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/blob/2150c8b6/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/SwiftApi.java ---------------------------------------------------------------------- diff --git a/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/SwiftApi.java b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/SwiftApi.java index 547c356..f803c47 100644 --- a/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/SwiftApi.java +++ b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/SwiftApi.java @@ -19,6 +19,9 @@ package org.jclouds.openstack.swift.v1; import java.io.Closeable; import java.util.Set; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; + import org.jclouds.javax.annotation.Nullable; import org.jclouds.location.Region; import org.jclouds.location.functions.RegionToEndpoint; @@ -50,5 +53,7 @@ public interface SwiftApi extends Closeable { ContainerApi containerApiInRegion(@EndpointParam(parser = RegionToEndpoint.class) @Nullable String region); @Delegate - ObjectApi objectApiInRegion(@EndpointParam(parser = RegionToEndpoint.class) @Nullable String region); + @Path("/{containerName}") + ObjectApi objectApiInRegionForContainer(@EndpointParam(parser = RegionToEndpoint.class) @Nullable String region, + @PathParam("containerName") String containerName); } http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/blob/2150c8b6/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/binders/BindMetadataToHeaders.java ---------------------------------------------------------------------- diff --git a/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/binders/BindMetadataToHeaders.java b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/binders/BindMetadataToHeaders.java index dd3f803..262938e 100644 --- a/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/binders/BindMetadataToHeaders.java +++ b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/binders/BindMetadataToHeaders.java @@ -78,6 +78,18 @@ public abstract class BindMetadataToHeaders implements Binder { } } + public static class BindObjectMetadataToHeaders extends BindMetadataToHeaders { + BindObjectMetadataToHeaders() { + super("x-object-meta-"); + } + } + + public static class BindRemoveObjectMetadataToHeaders extends BindMetadataToHeaders.ForRemoval { + BindRemoveObjectMetadataToHeaders() { + super("x-object-meta-"); + } + } + /** * @see <a * href="http://docs.openstack.org/api/openstack-object-storage/1.0/content/delete-account-metadata.html">documentation</a> http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/blob/2150c8b6/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/domain/SwiftObject.java ---------------------------------------------------------------------- diff --git a/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/domain/SwiftObject.java b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/domain/SwiftObject.java new file mode 100644 index 0000000..6904436 --- /dev/null +++ b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/domain/SwiftObject.java @@ -0,0 +1,219 @@ +/* + * 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.openstack.swift.v1.domain; + +import static com.google.common.base.Objects.equal; +import static com.google.common.base.Objects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; + +import java.beans.ConstructorProperties; +import java.util.Date; +import java.util.Map; +import java.util.Map.Entry; + +import org.jclouds.io.Payload; +import org.jclouds.io.Payloads; +import org.jclouds.openstack.swift.v1.features.ObjectApi; + +import com.google.common.base.Objects; +import com.google.common.base.Objects.ToStringHelper; +import com.google.common.collect.ImmutableMap; + +/** + * @see <a + * href="http://docs.openstack.org/api/openstack-object-storage/1.0/content/retrieve-object.html">api + * doc</a> + */ +public class SwiftObject implements Comparable<SwiftObject> { + + private final String name; + private final String hash; + private final Date lastModified; + private final Map<String, String> metadata; + private final Payload payload; + + @ConstructorProperties({ "name", "hash", "bytes", "content_type", "last_modified" }) + protected SwiftObject(String name, String hash, long bytes, String contentType, Date lastModified) { + this(name, hash, lastModified, ImmutableMap.<String, String> of(), payload(bytes, contentType)); + } + + protected SwiftObject(String name, String hash, Date lastModified, Map<String, String> metadata, Payload payload) { + this.name = checkNotNull(name, "name"); + this.hash = checkNotNull(hash, "hash of %s", name); + this.lastModified = checkNotNull(lastModified, "lastModified of %s", name); + this.metadata = metadata == null ? ImmutableMap.<String, String> of() : metadata; + this.payload = checkNotNull(payload, "payload of %s", name); + } + + public String name() { + return name; + } + + public String hash() { + return hash; + } + + public Date lastModified() { + return lastModified; + } + + /** + * Empty except in {@link ObjectApi#head(String) GetObjectMetadata} or + * {@link ObjectApi#get(String) GetObject} commands. + * + * <h3>Note</h3> + * + * In current swift implementations, headers keys are lower-cased. This means + * characters such as turkish will probably not work out well. + */ + public Map<String, String> metadata() { + return metadata; + } + + /** + * Only has a {@link Payload#getInput()} when retrieved via the + * {@link ObjectApi#get(String) GetObject} command. + */ + public Payload payload() { + return payload; + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (object instanceof SwiftObject) { + final SwiftObject that = SwiftObject.class.cast(object); + return equal(name(), that.name()) // + && equal(hash(), that.hash()); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hashCode(name(), hash()); + } + + @Override + public String toString() { + return string().toString(); + } + + protected ToStringHelper string() { + return toStringHelper("") // + .add("name", name()) // + .add("hash", hash()) // + .add("lastModified", lastModified()) // + .add("metadata", metadata()); + } + + @Override + public int compareTo(SwiftObject that) { + if (that == null) + return 1; + if (this == that) + return 0; + return this.name().compareTo(that.name()); + } + + public static Builder builder() { + return new Builder(); + } + + public Builder toBuilder() { + return builder().fromObject(this); + } + + public static class Builder { + protected String name; + protected String hash; + protected Date lastModified; + protected Payload payload; + protected Map<String, String> metadata = ImmutableMap.of(); + + /** + * @see SwiftObject#name() + */ + public Builder name(String name) { + this.name = checkNotNull(name, "name"); + return this; + } + + /** + * @see SwiftObject#hash() + */ + public Builder hash(String hash) { + this.hash = hash; + return this; + } + + /** + * @see SwiftObject#lastModified() + */ + public Builder lastModified(Date lastModified) { + this.lastModified = lastModified; + return this; + } + + /** + * @see SwiftObject#payload() + */ + public Builder payload(Payload payload) { + this.payload = payload; + return this; + } + + /** + * Will lower-case all metadata keys due to a swift implementation + * decision. + * + * @see SwiftObject#metadata() + */ + public Builder metadata(Map<String, String> metadata) { + ImmutableMap.Builder<String, String> builder = ImmutableMap.<String, String> builder(); + for (Entry<String, String> entry : checkNotNull(metadata, "metadata").entrySet()) { + builder.put(entry.getKey().toLowerCase(), entry.getValue()); + } + this.metadata = builder.build(); + return this; + } + + public SwiftObject build() { + return new SwiftObject(name, hash, lastModified, metadata, payload); + } + + public Builder fromObject(SwiftObject from) { + return name(from.name()) // + .hash(from.hash()) // + .lastModified(from.lastModified()) // + .metadata(from.metadata()) // + .payload(from.payload()); + } + } + + private static final byte[] NO_CONTENT = new byte[] {}; + + private static Payload payload(long bytes, String contentType) { + Payload payload = Payloads.newByteArrayPayload(NO_CONTENT); + payload.getContentMetadata().setContentLength(bytes); + payload.getContentMetadata().setContentType(contentType); + return payload; + } +} http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/blob/2150c8b6/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/features/ObjectApi.java ---------------------------------------------------------------------- diff --git a/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/features/ObjectApi.java b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/features/ObjectApi.java index 93775a4..2a3ee1e 100644 --- a/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/features/ObjectApi.java +++ b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/features/ObjectApi.java @@ -16,25 +16,207 @@ */ package org.jclouds.openstack.swift.v1.features; +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; + +import java.util.Map; + +import javax.inject.Named; +import javax.ws.rs.Consumes; +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 javax.ws.rs.QueryParam; + +import org.jclouds.Fallbacks.EmptyFluentIterableOnNotFoundOr404; +import org.jclouds.Fallbacks.FalseOnNotFoundOr404; +import org.jclouds.Fallbacks.NullOnNotFoundOr404; +import org.jclouds.Fallbacks.VoidOnNotFoundOr404; +import org.jclouds.http.HttpRequest; +import org.jclouds.http.options.GetOptions; +import org.jclouds.io.Payload; +import org.jclouds.javax.annotation.Nullable; import org.jclouds.openstack.keystone.v2_0.filters.AuthenticateRequest; +import org.jclouds.openstack.swift.v1.binders.BindMetadataToHeaders.BindObjectMetadataToHeaders; +import org.jclouds.openstack.swift.v1.binders.BindMetadataToHeaders.BindRemoveObjectMetadataToHeaders; +import org.jclouds.openstack.swift.v1.domain.SwiftObject; +import org.jclouds.openstack.swift.v1.functions.ETagHeader; +import org.jclouds.openstack.swift.v1.functions.ParseObjectFromResponse; +import org.jclouds.rest.Binder; +import org.jclouds.rest.annotations.BinderParam; +import org.jclouds.rest.annotations.Fallback; +import org.jclouds.rest.annotations.QueryParams; import org.jclouds.rest.annotations.RequestFilters; +import org.jclouds.rest.annotations.ResponseParser; + +import com.google.common.collect.FluentIterable; /** - * Storage Object Services An object represents the data and any metadata for the files stored in - * the system. Through the ReST interface, metadata for an object can be included by adding custom - * HTTP headers to the request and the data payload as the request body. Objects cannot exceed 5GB - * and must have names that do not exceed 1024 bytes after URL encoding. However, objects larger - * than 5GB can be segmented and then concatenated together so that you can upload 5 GB segments and - * download a single concatenated object. You can work with the segments and manifests directly with - * HTTP requests. - * - * @author Adrian Cole - * @author Zack Shoylev * @see <a href= * "http://docs.openstack.org/api/openstack-object-storage/1.0/content/storage-object-services.html" * >api doc</a> */ @RequestFilters(AuthenticateRequest.class) +@Consumes(APPLICATION_JSON) public interface ObjectApi { + /** + * Lists up to 10,000 objects. + * + * @return a list of existing storage objects ordered by name. + */ + @Named("ListObjects") + @GET + @QueryParams(keys = "format", values = "json") + @Fallback(EmptyFluentIterableOnNotFoundOr404.class) + @Path("/") + FluentIterable<SwiftObject> listFirstPage(); + + /** + * Lists up to 10,000 objects, starting at {@code marker}. + * + * @param marker + * lexicographic position to start list. + * + * @return a list of existing storage objects ordered by name. + */ + @Named("ListObjects") + @GET + @QueryParams(keys = "format", values = "json") + @Fallback(EmptyFluentIterableOnNotFoundOr404.class) + @Path("/") + FluentIterable<SwiftObject> listAt(@QueryParam("marker") String marker); + + /** + * Creates or updates an object. + * + * @param objectName + * corresponds to {@link SwiftObject#name()}. + * @param payload + * corresponds to {@link SwiftObject#payload()}. + * @param metadata + * corresponds to {@link SwiftObject#metadata()}. + * @see <a + * href="http://docs.openstack.org/api/openstack-object-storage/1.0/content/create-update-object.html"> + * Create or Update Object API</a> + * + * @return {@link SwiftObject#hash()} of the object. + */ + @Named("CreateOrUpdateObject") + @PUT + @ResponseParser(ETagHeader.class) + @Path("/{objectName}") + String createOrUpdate(@PathParam("objectName") String objectName, @BinderParam(SetPayload.class) Payload payload, + @BinderParam(BindObjectMetadataToHeaders.class) Map<String, String> metadata); + + static class SetPayload implements Binder { + @SuppressWarnings("unchecked") + @Override + public <R extends HttpRequest> R bindToRequest(R request, Object input) { + return (R) request.toBuilder().payload(Payload.class.cast(input)).build(); + } + } + + /** + * Gets the {@link SwiftObject} metadata without its + * {@link Payload#getInput() body}. + * + * @param objectName + * corresponds to {@link SwiftObject#name()}. + * @return the Object or null, if not found. + * + * @see <a + * href="http://docs.openstack.org/api/openstack-object-storage/1.0/content/retrieve-object-metadata.html"> + * Get Object Metadata API</a> + */ + @Named("GetObjectMetadata") + @HEAD + @ResponseParser(ParseObjectFromResponse.class) + @Fallback(NullOnNotFoundOr404.class) + @Path("/{objectName}") + @Nullable + SwiftObject head(@PathParam("objectName") String objectName); + + /** + * Gets the {@link SwiftObject} including its {@link Payload#getInput() body}. + * + * @param objectName + * corresponds to {@link SwiftObject#name()}. + * @param options options to control the download. + * + * @return the Object or null, if not found. + * + * @see <a + * href="http://docs.openstack.org/api/openstack-object-storage/1.0/content/retrieve-object.html"> + * Get Object API</a> + */ + @Named("GetObject") + @GET + @ResponseParser(ParseObjectFromResponse.class) + @Fallback(NullOnNotFoundOr404.class) + @Path("/{objectName}") + @Nullable + SwiftObject get(@PathParam("objectName") String objectName, GetOptions options); + + /** + * Creates or updates the Object metadata. + * + * @param objectName + * corresponds to {@link SwiftObject#name()}. + * @param metadata + * the Object metadata to create or update. + * + * @see <a + * href="http://docs.openstack.org/api/openstack-object-storage/1.0/content/Update_Container_Metadata-d1e1900.html"> + * Create or Update Object Metadata API</a> + * + * @return <code>true</code> if the Object Metadata was successfully created + * or updated, false if not. + */ + @Named("UpdateObjectMetadata") + @POST + @Fallback(FalseOnNotFoundOr404.class) + @Path("/{objectName}") + boolean updateMetadata(@PathParam("objectName") String objectName, + @BinderParam(BindObjectMetadataToHeaders.class) Map<String, String> metadata); + + /** + * Deletes Object metadata. + * + * @param objectName + * corresponds to {@link SwiftObject#name()}. + * @param metadata + * the Object metadata to delete. + * + * @return <code>true</code> if the Object Metadata was successfully deleted, + * false if not. + * + * @see <a + * href="http://docs.openstack.org/api/openstack-object-storage/1.0/content/delete-object-metadata.html"> + * Delete Object Metadata API</a> + */ + @Named("DeleteObjectMetadata") + @POST + @Fallback(FalseOnNotFoundOr404.class) + @Path("/{objectName}") + boolean deleteMetadata(@PathParam("objectName") String objectName, + @BinderParam(BindRemoveObjectMetadataToHeaders.class) Map<String, String> metadata); + + /** + * Deletes a object, if present. + * + * @param objectName + * corresponds to {@link SwiftObject#name()}. + * @see <a + * href="http://docs.openstack.org/api/openstack-object-storage/1.0/content/delete-object.html"> + * Delete Object API</a> + */ + @Named("DeleteObject") + @DELETE + @Fallback(VoidOnNotFoundOr404.class) + @Path("/{objectName}") + void delete(@PathParam("objectName") String objectName); } http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/blob/2150c8b6/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/functions/ETagHeader.java ---------------------------------------------------------------------- diff --git a/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/functions/ETagHeader.java b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/functions/ETagHeader.java new file mode 100644 index 0000000..f5c9dd2 --- /dev/null +++ b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/functions/ETagHeader.java @@ -0,0 +1,31 @@ +/* + * 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.openstack.swift.v1.functions; + +import static com.google.common.net.HttpHeaders.ETAG; + +import org.jclouds.http.HttpResponse; + +import com.google.common.base.Function; + +public class ETagHeader implements Function<HttpResponse, String> { + + @Override + public String apply(HttpResponse from) { + return from.getFirstHeaderOrNull(ETAG); + } +} http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/blob/2150c8b6/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/functions/ParseAccountFromHeaders.java ---------------------------------------------------------------------- diff --git a/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/functions/ParseAccountFromHeaders.java b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/functions/ParseAccountFromHeaders.java index b03c8ec..f31cbac 100644 --- a/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/functions/ParseAccountFromHeaders.java +++ b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/functions/ParseAccountFromHeaders.java @@ -27,8 +27,8 @@ public class ParseAccountFromHeaders implements Function<HttpResponse, Account> public Account apply(HttpResponse from) { return Account.builder() // .bytesUsed(Long.parseLong(from.getFirstHeaderOrNull("X-Account-Bytes-Used"))) // - .containerCount(Integer.parseInt(from.getFirstHeaderOrNull("X-Account-Container-Count"))) // - .objectCount(Integer.parseInt(from.getFirstHeaderOrNull("X-Account-Object-Count"))) // + .containerCount(Long.parseLong(from.getFirstHeaderOrNull("X-Account-Container-Count"))) // + .objectCount(Long.parseLong(from.getFirstHeaderOrNull("X-Account-Object-Count"))) // .metadata(EntriesWithoutMetaPrefix.INSTANCE.apply(from.getHeaders())).build(); } } http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/blob/2150c8b6/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/functions/ParseContainerFromHeaders.java ---------------------------------------------------------------------- diff --git a/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/functions/ParseContainerFromHeaders.java b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/functions/ParseContainerFromHeaders.java index 5e66202..e246af5 100644 --- a/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/functions/ParseContainerFromHeaders.java +++ b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/functions/ParseContainerFromHeaders.java @@ -34,7 +34,7 @@ public class ParseContainerFromHeaders implements Function<HttpResponse, Contain return Container.builder() // .name(name) // .bytesUsed(Long.parseLong(from.getFirstHeaderOrNull("X-Container-Bytes-Used"))) // - .objectCount(Integer.parseInt(from.getFirstHeaderOrNull("X-Container-Object-Count"))) // + .objectCount(Long.parseLong(from.getFirstHeaderOrNull("X-Container-Object-Count"))) // .metadata(EntriesWithoutMetaPrefix.INSTANCE.apply(from.getHeaders())).build(); } http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/blob/2150c8b6/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/functions/ParseObjectFromResponse.java ---------------------------------------------------------------------- diff --git a/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/functions/ParseObjectFromResponse.java b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/functions/ParseObjectFromResponse.java new file mode 100644 index 0000000..c5533a4 --- /dev/null +++ b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/functions/ParseObjectFromResponse.java @@ -0,0 +1,59 @@ +/* + * 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.openstack.swift.v1.functions; + +import static com.google.common.net.HttpHeaders.ETAG; +import static com.google.common.net.HttpHeaders.LAST_MODIFIED; + +import javax.inject.Inject; + +import org.jclouds.date.DateService; +import org.jclouds.http.HttpRequest; +import org.jclouds.http.HttpResponse; +import org.jclouds.openstack.swift.v1.domain.SwiftObject; +import org.jclouds.rest.InvocationContext; +import org.jclouds.rest.internal.GeneratedHttpRequest; + +import com.google.common.base.Function; + +public class ParseObjectFromResponse implements Function<HttpResponse, SwiftObject>, + InvocationContext<ParseObjectFromResponse> { + private final DateService dates; + + @Inject + ParseObjectFromResponse(DateService dates) { + this.dates = dates; + } + + private String name; + + @Override + public SwiftObject apply(HttpResponse from) { + return SwiftObject.builder() // + .name(name) // + .hash(from.getFirstHeaderOrNull(ETAG)) // + .payload(from.getPayload()) // + .lastModified(dates.rfc822DateParse(from.getFirstHeaderOrNull(LAST_MODIFIED))) // + .metadata(EntriesWithoutMetaPrefix.INSTANCE.apply(from.getHeaders())).build(); + } + + @Override + public ParseObjectFromResponse setContext(HttpRequest request) { + this.name = GeneratedHttpRequest.class.cast(request).getInvocation().getArgs().get(0).toString(); + return this; + } +} http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/blob/2150c8b6/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/ObjectApiLiveTest.java ---------------------------------------------------------------------- diff --git a/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/ObjectApiLiveTest.java b/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/ObjectApiLiveTest.java index a9e83e7..4960fed 100644 --- a/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/ObjectApiLiveTest.java +++ b/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/ObjectApiLiveTest.java @@ -16,13 +16,152 @@ */ package org.jclouds.openstack.swift.v1.features; +import static org.jclouds.http.options.GetOptions.Builder.tail; +import static org.jclouds.io.Payloads.newStringPayload; +import static org.jclouds.util.Strings2.toStringAndClose; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.TimeUnit; + +import org.jclouds.http.options.GetOptions; +import org.jclouds.openstack.swift.v1.domain.SwiftObject; import org.jclouds.openstack.swift.v1.internal.BaseSwiftApiLiveTest; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableMap; + /** * @author Adrian Cole */ @Test(groups = "live", testName = "ObjectApiLiveTest") public class ObjectApiLiveTest extends BaseSwiftApiLiveTest { + private String name = getClass().getSimpleName(); + private String containerName = getClass().getSimpleName() + "Container"; + + @Test + public void list() throws Exception { + for (String regionId : api.configuredRegions()) { + ObjectApi objectApi = api.objectApiInRegionForContainer(regionId, containerName); + FluentIterable<SwiftObject> response = objectApi.listFirstPage(); + assertNotNull(response); + for (SwiftObject object : response) { + checkObject(object); + } + } + } + + static void checkObject(SwiftObject object) { + assertNotNull(object.name()); + assertNotNull(object.hash()); + assertTrue(object.lastModified().getTime() <= System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(5)); + assertNotNull(object.payload().getContentMetadata().getContentLength()); + assertNotNull(object.payload().getContentMetadata().getContentType()); + } + + public void metadata() throws Exception { + for (String regionId : api.configuredRegions()) { + SwiftObject object = api.objectApiInRegionForContainer(regionId, containerName).head(name); + assertEquals(object.name(), name); + checkObject(object); + assertEquals(toStringAndClose(object.payload().getInput()), ""); + } + } + + public void get() throws Exception { + for (String regionId : api.configuredRegions()) { + SwiftObject object = api.objectApiInRegionForContainer(regionId, containerName).get(name, GetOptions.NONE); + assertEquals(object.name(), name); + checkObject(object); + assertEquals(toStringAndClose(object.payload().getInput()), "swifty"); + } + } + + public void getOptions() throws Exception { + for (String regionId : api.configuredRegions()) { + SwiftObject object = api.objectApiInRegionForContainer(regionId, containerName).get(name, tail(1)); + assertEquals(object.name(), name); + checkObject(object); + assertEquals(toStringAndClose(object.payload().getInput()), "y"); + } + } + + public void listAt() throws Exception { + String lexicographicallyBeforeName = name.substring(0, name.length() - 1); + for (String regionId : api.configuredRegions()) { + SwiftObject object = api.objectApiInRegionForContainer(regionId, containerName) + .listAt(lexicographicallyBeforeName).get(0); + assertEquals(object.name(), name); + checkObject(object); + } + } + + public void updateMetadata() throws Exception { + for (String regionId : api.configuredRegions()) { + ObjectApi objectApi = api.objectApiInRegionForContainer(regionId, containerName); + ; + + Map<String, String> meta = ImmutableMap.of("MyAdd1", "foo", "MyAdd2", "bar"); + + assertTrue(objectApi.updateMetadata(name, meta)); + + containerHasMetadata(objectApi, name, meta); + } + } + + public void deleteMetadata() throws Exception { + for (String regionId : api.configuredRegions()) { + ObjectApi objectApi = api.objectApiInRegionForContainer(regionId, containerName); + + Map<String, String> meta = ImmutableMap.of("MyDelete1", "foo", "MyDelete2", "bar"); + + assertTrue(objectApi.updateMetadata(name, meta)); + containerHasMetadata(objectApi, name, meta); + + assertTrue(objectApi.deleteMetadata(name, meta)); + SwiftObject object = objectApi.head(name); + for (Entry<String, String> entry : meta.entrySet()) { + // note keys are returned in lower-case! + assertFalse(object.metadata().containsKey(entry.getKey().toLowerCase())); + } + } + } + + static void containerHasMetadata(ObjectApi objectApi, String name, Map<String, String> meta) { + SwiftObject object = objectApi.head(name); + for (Entry<String, String> entry : meta.entrySet()) { + // note keys are returned in lower-case! + assertEquals(object.metadata().get(entry.getKey().toLowerCase()), entry.getValue(), // + object + " didn't have metadata: " + entry); + } + } + + @Override + @BeforeClass(groups = "live") + public void setup() { + super.setup(); + for (String regionId : api.configuredRegions()) { + api.containerApiInRegion(regionId).createIfAbsent(containerName); + api.objectApiInRegionForContainer(regionId, containerName).createOrUpdate(name, newStringPayload("swifty"), + ImmutableMap.<String, String> of()); + } + } + + @Override + @AfterClass(groups = "live") + public void tearDown() { + for (String regionId : api.configuredRegions()) { + api.objectApiInRegionForContainer(regionId, containerName).delete(name); + api.containerApiInRegion(regionId).deleteIfEmpty(containerName); + } + super.tearDown(); + } } http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/blob/2150c8b6/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/ObjectApiMockTest.java ---------------------------------------------------------------------- diff --git a/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/ObjectApiMockTest.java b/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/ObjectApiMockTest.java new file mode 100644 index 0000000..63c9974 --- /dev/null +++ b/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/ObjectApiMockTest.java @@ -0,0 +1,309 @@ +/* + * 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.openstack.swift.v1.features; + +import static com.google.common.base.Charsets.US_ASCII; +import static com.google.common.net.HttpHeaders.RANGE; +import static org.jclouds.http.options.GetOptions.Builder.tail; +import static org.jclouds.io.Payloads.newStringPayload; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +import java.util.Map; +import java.util.Map.Entry; + +import org.jclouds.date.internal.SimpleDateFormatDateService; +import org.jclouds.io.Payload; +import org.jclouds.io.Payloads; +import org.jclouds.openstack.swift.v1.SwiftApi; +import org.jclouds.openstack.swift.v1.domain.SwiftObject; +import org.jclouds.openstack.swift.v1.internal.BaseSwiftMockTest; +import org.jclouds.util.Strings2; +import org.testng.annotations.Test; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.squareup.okhttp.mockwebserver.MockResponse; +import com.squareup.okhttp.mockwebserver.MockWebServer; +import com.squareup.okhttp.mockwebserver.RecordedRequest; + +@Test +public class ObjectApiMockTest extends BaseSwiftMockTest { + SimpleDateFormatDateService dates = new SimpleDateFormatDateService(); + + String objectList = "" // + + "[\n" // + + " {\"name\":\"test_obj_1\",\n" // + + " \"hash\":\"4281c348eaf83e70ddce0e07221c3d28\",\n" // + + " \"bytes\":14,\n" // + + " \"content_type\":\"application\\/octet-stream\",\n" // + + " \"last_modified\":\"2009-02-03T05:26:32.612278\"},\n" // + + " {\"name\":\"test_obj_2\",\n" // + + " \"hash\":\"b039efe731ad111bc1b0ef221c3849d0\",\n" // + + " \"bytes\":64,\n" // + + " \"content_type\":\"application\\/octet-stream\",\n" // + + " \"last_modified\":\"2009-02-03T05:26:32.612278\"},\n" // + + "]"; + + ImmutableList<SwiftObject> parsedObjects = ImmutableList.of(// + SwiftObject.builder() // + .name("test_obj_1") // + .hash("4281c348eaf83e70ddce0e07221c3d28") // + .payload(payload(14, "application/octet-stream")) // + .lastModified(dates.iso8601DateParse("2009-02-03T05:26:32.612278")).build(), // + SwiftObject.builder() // + .name("test_obj_2") // + .hash("b039efe731ad111bc1b0ef221c3849d0") // + .payload(payload(64l, "application/octet-stream")) // + .lastModified(dates.iso8601DateParse("2009-02-03T05:26:32.612278")).build()); + + public void listFirstPage() throws Exception { + MockWebServer server = mockSwiftServer(); + server.enqueue(new MockResponse().setBody(access)); + server.enqueue(new MockResponse().setBody(objectList)); + + try { + SwiftApi api = swiftApi(server.getUrl("/").toString()); + ImmutableList<SwiftObject> objects = api.objectApiInRegionForContainer("DFW", "myContainer").listFirstPage() + .toList(); + assertEquals(objects, parsedObjects); + + assertEquals(server.getRequestCount(), 2); + assertEquals(server.takeRequest().getRequestLine(), "POST /tokens HTTP/1.1"); + assertEquals(server.takeRequest().getRequestLine(), + "GET /v1/MossoCloudFS_5bcf396e-39dd-45ff-93a1-712b9aba90a9/myContainer/?format=json HTTP/1.1"); + } finally { + server.shutdown(); + } + } + + public void listAt() throws Exception { + MockWebServer server = mockSwiftServer(); + server.enqueue(new MockResponse().setBody(access)); + server.enqueue(new MockResponse().setBody(objectList)); + + try { + SwiftApi api = swiftApi(server.getUrl("/").toString()); + ImmutableList<SwiftObject> objects = api.objectApiInRegionForContainer("DFW", "myContainer").listAt("test") + .toList(); + assertEquals(objects, parsedObjects); + + assertEquals(server.getRequestCount(), 2); + assertEquals(server.takeRequest().getRequestLine(), "POST /tokens HTTP/1.1"); + assertEquals(server.takeRequest().getRequestLine(), + "GET /v1/MossoCloudFS_5bcf396e-39dd-45ff-93a1-712b9aba90a9/myContainer/?format=json&marker=test HTTP/1.1"); + } finally { + server.shutdown(); + } + } + + public void createOrUpdate() throws Exception { + MockWebServer server = mockSwiftServer(); + server.enqueue(new MockResponse().setBody(access)); + server.enqueue(new MockResponse() // + .setResponseCode(201) // + .addHeader("ETag", "d9f5eb4bba4e2f2f046e54611bc8196b")); + + try { + SwiftApi api = swiftApi(server.getUrl("/").toString()); + assertEquals( + api.objectApiInRegionForContainer("DFW", "myContainer").createOrUpdate("myObject", + newStringPayload("swifty"), metadata), "d9f5eb4bba4e2f2f046e54611bc8196b"); + + assertEquals(server.getRequestCount(), 2); + assertEquals(server.takeRequest().getRequestLine(), "POST /tokens HTTP/1.1"); + RecordedRequest createOrUpdate = server.takeRequest(); + assertEquals(createOrUpdate.getRequestLine(), + "PUT /v1/MossoCloudFS_5bcf396e-39dd-45ff-93a1-712b9aba90a9/myContainer/myObject HTTP/1.1"); + assertEquals(new String(createOrUpdate.getBody()), "swifty"); + for (Entry<String, String> entry : metadata.entrySet()) { + assertEquals(createOrUpdate.getHeader("x-object-meta-" + entry.getKey().toLowerCase()), entry.getValue()); + } + } finally { + server.shutdown(); + } + } + + /** upper-cases first char, and lower-cases rest!! **/ + public void headKnowingServerMessesWithMetadataKeyCaseFormat() throws Exception { + MockWebServer server = mockSwiftServer(); + server.enqueue(new MockResponse().setBody(access)); + server.enqueue(objectResponse() // + // note silly casing + .addHeader("X-Object-Meta-Apiname", "swift") // + .addHeader("X-Object-Meta-Apiversion", "v1.1")); + + try { + SwiftApi api = swiftApi(server.getUrl("/").toString()); + SwiftObject object = api.objectApiInRegionForContainer("DFW", "myContainer").head("myObject"); + assertEquals(object.name(), "myObject"); + assertEquals(object.hash(), "8a964ee2a5e88be344f36c22562a6486"); + assertEquals(object.lastModified(), dates.rfc822DateParse("Fri, 12 Jun 2010 13:40:18 GMT")); + for (Entry<String, String> entry : object.metadata().entrySet()) { + assertEquals(object.metadata().get(entry.getKey().toLowerCase()), entry.getValue()); + } + assertEquals(object.payload().getContentMetadata().getContentLength(), new Long(4)); + assertEquals(object.payload().getContentMetadata().getContentType(), "text/plain; charset=UTF-8"); + assertEquals(Strings2.toStringAndClose(object.payload().getInput()), ""); + + assertEquals(server.getRequestCount(), 2); + assertEquals(server.takeRequest().getRequestLine(), "POST /tokens HTTP/1.1"); + assertEquals(server.takeRequest().getRequestLine(), + "HEAD /v1/MossoCloudFS_5bcf396e-39dd-45ff-93a1-712b9aba90a9/myContainer/myObject HTTP/1.1"); + } finally { + server.shutdown(); + } + } + + public void get() throws Exception { + MockWebServer server = mockSwiftServer(); + server.enqueue(new MockResponse().setBody(access)); + server.enqueue(objectResponse() // + // note silly casing + .addHeader("X-Object-Meta-Apiname", "swift") // + .addHeader("X-Object-Meta-Apiversion", "v1.1")); + + try { + SwiftApi api = swiftApi(server.getUrl("/").toString()); + SwiftObject object = api.objectApiInRegionForContainer("DFW", "myContainer").get("myObject", tail(1)); + assertEquals(object.name(), "myObject"); + assertEquals(object.hash(), "8a964ee2a5e88be344f36c22562a6486"); + assertEquals(object.lastModified(), dates.rfc822DateParse("Fri, 12 Jun 2010 13:40:18 GMT")); + for (Entry<String, String> entry : object.metadata().entrySet()) { + assertEquals(object.metadata().get(entry.getKey().toLowerCase()), entry.getValue()); + } + assertEquals(object.payload().getContentMetadata().getContentLength(), new Long(4)); + assertEquals(object.payload().getContentMetadata().getContentType(), "text/plain; charset=UTF-8"); + // note MWS doesn't process Range header at the moment + assertEquals(Strings2.toStringAndClose(object.payload().getInput()), "ABCD"); + + assertEquals(server.getRequestCount(), 2); + assertEquals(server.takeRequest().getRequestLine(), "POST /tokens HTTP/1.1"); + RecordedRequest get = server.takeRequest(); + assertEquals(get.getRequestLine(), + "GET /v1/MossoCloudFS_5bcf396e-39dd-45ff-93a1-712b9aba90a9/myContainer/myObject HTTP/1.1"); + assertEquals(get.getHeader(RANGE), "bytes=-1"); + } finally { + server.shutdown(); + } + } + + public void updateMetadata() throws Exception { + MockWebServer server = mockSwiftServer(); + server.enqueue(new MockResponse().setBody(access)); + server.enqueue(objectResponse() // + .addHeader("X-Object-Meta-ApiName", "swift") // + .addHeader("X-Object-Meta-ApiVersion", "v1.1")); + + try { + SwiftApi api = swiftApi(server.getUrl("/").toString()); + assertTrue(api.objectApiInRegionForContainer("DFW", "myContainer").updateMetadata("myObject", metadata)); + + assertEquals(server.getRequestCount(), 2); + assertEquals(server.takeRequest().getRequestLine(), "POST /tokens HTTP/1.1"); + RecordedRequest replaceRequest = server.takeRequest(); + assertEquals(replaceRequest.getRequestLine(), + "POST /v1/MossoCloudFS_5bcf396e-39dd-45ff-93a1-712b9aba90a9/myContainer/myObject HTTP/1.1"); + for (Entry<String, String> entry : metadata.entrySet()) { + assertEquals(replaceRequest.getHeader("x-object-meta-" + entry.getKey().toLowerCase()), entry.getValue()); + } + } finally { + server.shutdown(); + } + } + + public void deleteMetadata() throws Exception { + MockWebServer server = mockSwiftServer(); + server.enqueue(new MockResponse().setBody(access)); + server.enqueue(objectResponse()); + + try { + SwiftApi api = swiftApi(server.getUrl("/").toString()); + assertTrue(api.objectApiInRegionForContainer("DFW", "myContainer").deleteMetadata("myObject", metadata)); + + assertEquals(server.getRequestCount(), 2); + assertEquals(server.takeRequest().getRequestLine(), "POST /tokens HTTP/1.1"); + RecordedRequest deleteRequest = server.takeRequest(); + assertEquals(deleteRequest.getRequestLine(), + "POST /v1/MossoCloudFS_5bcf396e-39dd-45ff-93a1-712b9aba90a9/myContainer/myObject HTTP/1.1"); + for (String key : metadata.keySet()) { + assertEquals(deleteRequest.getHeader("x-remove-object-meta-" + key.toLowerCase()), "ignored"); + } + } finally { + server.shutdown(); + } + } + + public void delete() throws Exception { + MockWebServer server = mockSwiftServer(); + server.enqueue(new MockResponse().setBody(access)); + server.enqueue(new MockResponse().setResponseCode(204)); + + try { + SwiftApi api = swiftApi(server.getUrl("/").toString()); + api.objectApiInRegionForContainer("DFW", "myContainer").delete("myObject"); + + assertEquals(server.getRequestCount(), 2); + assertEquals(server.takeRequest().getRequestLine(), "POST /tokens HTTP/1.1"); + RecordedRequest deleteRequest = server.takeRequest(); + assertEquals(deleteRequest.getRequestLine(), + "DELETE /v1/MossoCloudFS_5bcf396e-39dd-45ff-93a1-712b9aba90a9/myContainer/myObject HTTP/1.1"); + } finally { + server.shutdown(); + } + } + + public void alreadyDeleted() throws Exception { + MockWebServer server = mockSwiftServer(); + server.enqueue(new MockResponse().setBody(access)); + server.enqueue(new MockResponse().setResponseCode(404)); + + try { + SwiftApi api = swiftApi(server.getUrl("/").toString()); + api.objectApiInRegionForContainer("DFW", "myContainer").delete("myObject"); + + assertEquals(server.getRequestCount(), 2); + assertEquals(server.takeRequest().getRequestLine(), "POST /tokens HTTP/1.1"); + RecordedRequest deleteRequest = server.takeRequest(); + assertEquals(deleteRequest.getRequestLine(), + "DELETE /v1/MossoCloudFS_5bcf396e-39dd-45ff-93a1-712b9aba90a9/myContainer/myObject HTTP/1.1"); + } finally { + server.shutdown(); + } + } + + private final static Map<String, String> metadata = ImmutableMap.of("ApiName", "swift", "ApiVersion", "v1.1"); + + public static MockResponse objectResponse() { + return new MockResponse() // + .addHeader("Last-Modified", "Fri, 12 Jun 2010 13:40:18 GMT") // + .addHeader("ETag", "8a964ee2a5e88be344f36c22562a6486") // + // TODO: MWS doesn't allow you to return content length w/o content + // on HEAD! + .setBody("ABCD".getBytes(US_ASCII)) // + .addHeader("Content-Length", "4").addHeader("Content-Type", "text/plain; charset=UTF-8"); + } + + private static final byte[] NO_CONTENT = new byte[] {}; + + private static Payload payload(long bytes, String contentType) { + Payload payload = Payloads.newByteArrayPayload(NO_CONTENT); + payload.getContentMetadata().setContentLength(bytes); + payload.getContentMetadata().setContentType(contentType); + return payload; + } +}
