You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jclouds.apache.org by ad...@apache.org on 2013/09/29 20:28:42 UTC

git commit: JCLOUDS-73. Add bulk operations to swift

Updated Branches:
  refs/heads/master 0982e0005 -> 6fbc1932e


JCLOUDS-73. Add bulk operations to 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/6fbc1932
Tree: http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/tree/6fbc1932
Diff: http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/diff/6fbc1932

Branch: refs/heads/master
Commit: 6fbc1932e9c163f6894c108d7d216d06f8dd82e7
Parents: 0982e00
Author: Adrian Cole <ad...@gmail.com>
Authored: Sun Sep 29 10:55:32 2013 -0700
Committer: Adrian Cole <ad...@gmail.com>
Committed: Sun Sep 29 11:26:00 2013 -0700

----------------------------------------------------------------------
 openstack-swift/pom.xml                         |   7 +
 .../jclouds/openstack/swift/v1/SwiftApi.java    |   4 +
 .../openstack/swift/v1/SwiftApiMetadata.java    |   7 +-
 .../swift/v1/config/SwiftHttpApiModule.java     |  12 --
 .../swift/v1/config/SwiftTypeAdapters.java      | 119 ++++++++++++++++
 .../openstack/swift/v1/domain/Account.java      |   3 +-
 .../swift/v1/domain/BulkDeleteResponse.java     |  89 ++++++++++++
 .../swift/v1/domain/ExtractArchiveResponse.java |  80 +++++++++++
 .../openstack/swift/v1/features/AccountApi.java |   1 -
 .../openstack/swift/v1/features/BulkApi.java    | 110 ++++++++++++++
 .../swift/v1/config/SwiftTypeAdaptersTest.java  |  90 ++++++++++++
 .../swift/v1/features/BulkApiLiveTest.java      | 142 +++++++++++++++++++
 .../swift/v1/features/BulkApiMockTest.java      |  70 +++++++++
 .../features/UrlEncodeAndJoinOnNewlineTest.java |  45 ++++++
 14 files changed, 764 insertions(+), 15 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/blob/6fbc1932/openstack-swift/pom.xml
----------------------------------------------------------------------
diff --git a/openstack-swift/pom.xml b/openstack-swift/pom.xml
index 6159d6a..9520153 100644
--- a/openstack-swift/pom.xml
+++ b/openstack-swift/pom.xml
@@ -100,6 +100,13 @@
       <scope>test</scope>
     </dependency>
     <dependency>
+      <groupId>org.jboss.shrinkwrap</groupId>
+      <artifactId>shrinkwrap-depchain</artifactId>
+      <version>1.2.0</version>
+      <type>pom</type>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
       <groupId>org.apache.jclouds.driver</groupId>
       <artifactId>jclouds-slf4j</artifactId>
       <version>${project.parent.version}</version>

http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/blob/6fbc1932/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 f803c47..b18a521 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
@@ -26,6 +26,7 @@ import org.jclouds.javax.annotation.Nullable;
 import org.jclouds.location.Region;
 import org.jclouds.location.functions.RegionToEndpoint;
 import org.jclouds.openstack.swift.v1.features.AccountApi;
+import org.jclouds.openstack.swift.v1.features.BulkApi;
 import org.jclouds.openstack.swift.v1.features.ContainerApi;
 import org.jclouds.openstack.swift.v1.features.ObjectApi;
 import org.jclouds.rest.annotations.Delegate;
@@ -50,6 +51,9 @@ public interface SwiftApi extends Closeable {
    AccountApi accountApiInRegion(@EndpointParam(parser = RegionToEndpoint.class) @Nullable String region);
 
    @Delegate
+   BulkApi bulkApiInRegion(@EndpointParam(parser = RegionToEndpoint.class) @Nullable String region);
+
+   @Delegate
    ContainerApi containerApiInRegion(@EndpointParam(parser = RegionToEndpoint.class) @Nullable String region);
 
    @Delegate

http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/blob/6fbc1932/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/SwiftApiMetadata.java
----------------------------------------------------------------------
diff --git a/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/SwiftApiMetadata.java b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/SwiftApiMetadata.java
index 0cde00b..1c08c78 100644
--- a/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/SwiftApiMetadata.java
+++ b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/SwiftApiMetadata.java
@@ -18,16 +18,20 @@ package org.jclouds.openstack.swift.v1;
 
 import static org.jclouds.openstack.keystone.v2_0.config.KeystoneProperties.CREDENTIAL_TYPE;
 import static org.jclouds.openstack.keystone.v2_0.config.KeystoneProperties.SERVICE_TYPE;
+
 import java.net.URI;
 import java.util.Properties;
+
 import org.jclouds.apis.ApiMetadata;
 import org.jclouds.openstack.keystone.v2_0.config.AuthenticationApiModule;
 import org.jclouds.openstack.keystone.v2_0.config.CredentialTypes;
 import org.jclouds.openstack.keystone.v2_0.config.KeystoneAuthenticationModule;
 import org.jclouds.openstack.keystone.v2_0.config.KeystoneAuthenticationModule.RegionModule;
 import org.jclouds.openstack.swift.v1.config.SwiftHttpApiModule;
+import org.jclouds.openstack.swift.v1.config.SwiftTypeAdapters;
 import org.jclouds.openstack.v2_0.ServiceType;
 import org.jclouds.rest.internal.BaseHttpApiMetadata;
+
 import com.google.common.collect.ImmutableSet;
 import com.google.inject.Module;
 
@@ -63,7 +67,7 @@ public class SwiftApiMetadata extends BaseHttpApiMetadata<SwiftApi> {
 
       protected Builder() {
           id("openstack-swift")
-         .name("OpenStack Swift Diablo+ API")
+         .name("OpenStack Swift Grizzly+ API")
          .identityName("${tenantName}:${userName} or ${userName}, if your keystone supports a default tenant")
          .credentialName("${password}")
          .documentation(URI.create("http://docs.openstack.org/api/openstack-object-storage/1.0/content/ch_object-storage-dev-overview.html"))
@@ -75,6 +79,7 @@ public class SwiftApiMetadata extends BaseHttpApiMetadata<SwiftApi> {
                                      .add(AuthenticationApiModule.class)
                                      .add(KeystoneAuthenticationModule.class)
                                      .add(RegionModule.class)
+                                     .add(SwiftTypeAdapters.class)
                                      .add(SwiftHttpApiModule.class).build());
       }
       

http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/blob/6fbc1932/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/config/SwiftHttpApiModule.java
----------------------------------------------------------------------
diff --git a/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/config/SwiftHttpApiModule.java b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/config/SwiftHttpApiModule.java
index c0c68c7..37e4b2d 100644
--- a/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/config/SwiftHttpApiModule.java
+++ b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/config/SwiftHttpApiModule.java
@@ -19,28 +19,16 @@ import org.jclouds.http.HttpErrorHandler;
 import org.jclouds.http.annotation.ClientError;
 import org.jclouds.http.annotation.Redirection;
 import org.jclouds.http.annotation.ServerError;
-import org.jclouds.json.config.GsonModule.DateAdapter;
-import org.jclouds.json.config.GsonModule.Iso8601DateAdapter;
 import org.jclouds.openstack.swift.v1.SwiftApi;
 import org.jclouds.openstack.swift.v1.handlers.SwiftErrorHandler;
 import org.jclouds.rest.ConfiguresHttpApi;
 import org.jclouds.rest.config.HttpApiModule;
 
-/**
- * @author Adrian Cole
- * @author Zack Shoylev
- */
 @ConfiguresHttpApi
 public class SwiftHttpApiModule extends HttpApiModule<SwiftApi> {
   
    public SwiftHttpApiModule() {
    }   
-   
-   @Override
-   protected void configure() {
-      bind(DateAdapter.class).to(Iso8601DateAdapter.class);
-      super.configure();
-   }
 
    @Override
    protected void bindErrorHandlers() {

http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/blob/6fbc1932/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/config/SwiftTypeAdapters.java
----------------------------------------------------------------------
diff --git a/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/config/SwiftTypeAdapters.java b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/config/SwiftTypeAdapters.java
new file mode 100644
index 0000000..f556142
--- /dev/null
+++ b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/config/SwiftTypeAdapters.java
@@ -0,0 +1,119 @@
+/*
+ * 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.config;
+
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.net.URI;
+import java.util.Map;
+
+import org.jclouds.json.config.GsonModule.DateAdapter;
+import org.jclouds.json.config.GsonModule.Iso8601DateAdapter;
+import org.jclouds.openstack.swift.v1.domain.BulkDeleteResponse;
+import org.jclouds.openstack.swift.v1.domain.ExtractArchiveResponse;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMap.Builder;
+import com.google.gson.TypeAdapter;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import com.google.inject.AbstractModule;
+import com.google.inject.Provides;
+
+public class SwiftTypeAdapters extends AbstractModule {
+
+   @Override
+   protected void configure() {
+      bind(DateAdapter.class).to(Iso8601DateAdapter.class);
+   }
+
+   @Provides
+   public Map<Type, Object> provideCustomAdapterBindings() {
+      return ImmutableMap.<Type, Object> builder() //
+            .put(ExtractArchiveResponse.class, new ExtractArchiveResponseAdapter()) //
+            .put(BulkDeleteResponse.class, new BulkDeleteResponseAdapter()).build();
+   }
+
+   static class ExtractArchiveResponseAdapter extends TypeAdapter<ExtractArchiveResponse> {
+
+      @Override
+      public ExtractArchiveResponse read(JsonReader reader) throws IOException {
+         int created = 0;
+         Builder<String, String> errors = ImmutableMap.<String, String> builder();
+         reader.beginObject();
+         while (reader.hasNext()) {
+            String key = reader.nextName();
+            if (key.equals("Number Files Created")) {
+               created = reader.nextInt();
+            } else if (key.equals("Errors")) {
+               readErrors(reader, errors);
+            } else {
+               reader.skipValue();
+            }
+         }
+         reader.endObject();
+         return ExtractArchiveResponse.create(created, errors.build());
+      }
+
+      @Override
+      public void write(JsonWriter arg0, ExtractArchiveResponse arg1) throws IOException {
+         throw new UnsupportedOperationException();
+      }
+   }
+
+   static class BulkDeleteResponseAdapter extends TypeAdapter<BulkDeleteResponse> {
+
+      @Override
+      public BulkDeleteResponse read(JsonReader reader) throws IOException {
+         int deleted = 0;
+         int notFound = 0;
+         Builder<String, String> errors = ImmutableMap.<String, String> builder();
+         reader.beginObject();
+         while (reader.hasNext()) {
+            String key = reader.nextName();
+            if (key.equals("Number Deleted")) {
+               deleted = reader.nextInt();
+            } else if (key.equals("Number Not Found")) {
+               notFound = reader.nextInt();
+            } else if (key.equals("Errors")) {
+               readErrors(reader, errors);
+            } else {
+               reader.skipValue();
+            }
+         }
+         reader.endObject();
+         return BulkDeleteResponse.create(deleted, notFound, errors.build());
+      }
+
+      @Override
+      public void write(JsonWriter arg0, BulkDeleteResponse arg1) throws IOException {
+         throw new UnsupportedOperationException();
+      }
+   }
+
+   static void readErrors(JsonReader reader, Builder<String, String> errors) throws IOException {
+      reader.beginArray();
+      while (reader.hasNext()) {
+         reader.beginArray();
+         String decodedPath = URI.create(reader.nextString()).getPath();
+         errors.put(decodedPath, reader.nextString());
+         reader.endArray();
+      }
+      reader.endArray();
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/blob/6fbc1932/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/domain/Account.java
----------------------------------------------------------------------
diff --git a/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/domain/Account.java b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/domain/Account.java
index 02f13aa..304f329 100644
--- a/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/domain/Account.java
+++ b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/domain/Account.java
@@ -161,7 +161,8 @@ public class Account {
       public Builder fromContainer(Account from) {
          return containerCount(from.containerCount())//
                .objectCount(from.objectCount())//
-               .bytesUsed(from.bytesUsed());
+               .bytesUsed(from.bytesUsed()) //
+               .metadata(from.metadata());
       }
    }
 }

http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/blob/6fbc1932/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/domain/BulkDeleteResponse.java
----------------------------------------------------------------------
diff --git a/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/domain/BulkDeleteResponse.java b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/domain/BulkDeleteResponse.java
new file mode 100644
index 0000000..c96a66f
--- /dev/null
+++ b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/domain/BulkDeleteResponse.java
@@ -0,0 +1,89 @@
+/*
+ * 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.util.Map;
+
+import com.google.common.base.Objects;
+
+/**
+ * @see <a
+ *      href="http://docs.openstack.org/developer/swift/misc.html#module-swift.common.middleware.bulk">
+ *      Swift Bulk Middleware</a>
+ */
+public class BulkDeleteResponse {
+   public static BulkDeleteResponse create(int deleted, int notFound, Map<String, String> errors) {
+      return new BulkDeleteResponse(deleted, notFound, errors);
+   }
+
+   private final int deleted;
+   private final int notFound;
+   private final Map<String, String> errors;
+
+   private BulkDeleteResponse(int deleted, int notFound, Map<String, String> errors) {
+      this.deleted = deleted;
+      this.notFound = notFound;
+      this.errors = checkNotNull(errors, "errors");
+   }
+
+   /** number of files deleted. */
+   public int deleted() {
+      return deleted;
+   }
+
+   /** number of files not found. */
+   public int notFound() {
+      return notFound;
+   }
+
+   /** For each path that failed to delete, a corresponding error response. */
+   public Map<String, String> errors() {
+      return errors;
+   }
+
+   @Override
+   public boolean equals(Object object) {
+      if (this == object) {
+         return true;
+      }
+      if (object instanceof BulkDeleteResponse) {
+         BulkDeleteResponse that = BulkDeleteResponse.class.cast(object);
+         return equal(deleted(), that.deleted()) //
+               && equal(notFound(), that.notFound()) //
+               && equal(errors(), that.errors());
+      } else {
+         return false;
+      }
+   }
+
+   @Override
+   public int hashCode() {
+      return Objects.hashCode(deleted(), notFound(), errors());
+   }
+
+   @Override
+   public String toString() {
+      return toStringHelper("") //
+            .add("deleted", deleted()) //
+            .add("notFound", notFound()) //
+            .add("errors", errors()).toString();
+   }
+}

http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/blob/6fbc1932/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/domain/ExtractArchiveResponse.java
----------------------------------------------------------------------
diff --git a/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/domain/ExtractArchiveResponse.java b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/domain/ExtractArchiveResponse.java
new file mode 100644
index 0000000..5644377
--- /dev/null
+++ b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/domain/ExtractArchiveResponse.java
@@ -0,0 +1,80 @@
+/*
+ * 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.util.Map;
+
+import com.google.common.base.Objects;
+
+/**
+ * @see <a
+ *      href="http://docs.openstack.org/developer/swift/misc.html#module-swift.common.middleware.bulk">
+ *      Swift Bulk Middleware</a>
+ */
+public class ExtractArchiveResponse {
+   public static ExtractArchiveResponse create(int created, Map<String, String> errors) {
+      return new ExtractArchiveResponse(created, errors);
+   }
+
+   private final int created;
+   private final Map<String, String> errors;
+
+   private ExtractArchiveResponse(int created, Map<String, String> errors) {
+      this.created = created;
+      this.errors = checkNotNull(errors, "errors");
+   }
+
+   /** number of files created. */
+   public int created() {
+      return created;
+   }
+
+   /** For each path that failed to create, a corresponding error response. */
+   public Map<String, String> errors() {
+      return errors;
+   }
+
+   @Override
+   public boolean equals(Object object) {
+      if (this == object) {
+         return true;
+      }
+      if (object instanceof ExtractArchiveResponse) {
+         ExtractArchiveResponse that = ExtractArchiveResponse.class.cast(object);
+         return equal(created(), that.created()) //
+               && equal(errors(), that.errors());
+      } else {
+         return false;
+      }
+   }
+
+   @Override
+   public int hashCode() {
+      return Objects.hashCode(created(), errors());
+   }
+
+   @Override
+   public String toString() {
+      return toStringHelper("") //
+            .add("created", created()) //
+            .add("errors", errors()).toString();
+   }
+}

http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/blob/6fbc1932/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/features/AccountApi.java
----------------------------------------------------------------------
diff --git a/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/features/AccountApi.java b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/features/AccountApi.java
index 0b0c65c..5e0bc6d 100644
--- a/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/features/AccountApi.java
+++ b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/features/AccountApi.java
@@ -45,7 +45,6 @@ import org.jclouds.rest.annotations.ResponseParser;
  * appropriately using a binder/parser.
  * 
  * @see {@link Account}
- * @see metadata
  * @see <a
  *      href="http://docs.openstack.org/api/openstack-object-storage/1.0/content/storage-account-services.html">
  *      Storage Account Services API</a>

http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/blob/6fbc1932/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/features/BulkApi.java
----------------------------------------------------------------------
diff --git a/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/features/BulkApi.java b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/features/BulkApi.java
new file mode 100644
index 0000000..0cc4546
--- /dev/null
+++ b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/features/BulkApi.java
@@ -0,0 +1,110 @@
+/*
+ * 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.collect.Iterables.transform;
+import static com.google.common.net.UrlEscapers.urlFragmentEscaper;
+import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
+import static javax.ws.rs.core.MediaType.TEXT_PLAIN;
+
+import javax.inject.Named;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.QueryParam;
+
+import org.jclouds.http.HttpRequest;
+import org.jclouds.io.Payload;
+import org.jclouds.io.Payloads;
+import org.jclouds.openstack.keystone.v2_0.filters.AuthenticateRequest;
+import org.jclouds.openstack.swift.v1.domain.BulkDeleteResponse;
+import org.jclouds.openstack.swift.v1.domain.ExtractArchiveResponse;
+import org.jclouds.openstack.swift.v1.features.ObjectApi.SetPayload;
+import org.jclouds.rest.Binder;
+import org.jclouds.rest.annotations.BinderParam;
+import org.jclouds.rest.annotations.QueryParams;
+import org.jclouds.rest.annotations.RequestFilters;
+
+import com.google.common.base.Joiner;
+
+/**
+ * Provides access to the Swift Bulk API.
+ * 
+ * <h3>Note</h3>
+ * 
+ * As of the Grizzly release, these operations occur <a
+ * href="https://blueprints.launchpad.net/swift/+spec/concurrent-bulk">serially
+ * on the backend</a>.
+ * 
+ * @see <a
+ *      href="http://docs.openstack.org/developer/swift/misc.html#module-swift.common.middleware.bulk">
+ *      Swift Bulk Middleware</a>
+ */
+@RequestFilters(AuthenticateRequest.class)
+@Consumes(APPLICATION_JSON)
+public interface BulkApi {
+
+   /**
+    * Extracts a tar archive at the path specified as {@code path}.
+    * 
+    * @param path
+    *           path to extract under, if not empty string.
+    * @param tar
+    *           valid tar archive
+    * @param format
+    *           one of {@code tar}, {@code tar.gz}, or {@code tar.bz2}
+    * 
+    * @return {@link BulkDeleteResponse#errors()} are empty on success.
+    */
+   @Named("ExtractArchive")
+   @PUT
+   @Path("/{path}")
+   ExtractArchiveResponse extractArchive(@PathParam("path") String path,
+         @BinderParam(SetPayload.class) Payload payload, @QueryParam("extract-archive") String format);
+
+   /**
+    * Deletes multiple objects or containers, if present.
+    * 
+    * @param paths
+    *           format of {@code container}, for an empty container, or
+    *           {@code container/object} for an object.
+    * 
+    * @return {@link BulkDeleteResponse#errors()} are empty on success.
+    */
+   @Named("BulkDelete")
+   @DELETE
+   @Path("/")
+   @QueryParams(keys = "bulk-delete")
+   BulkDeleteResponse bulkDelete(@BinderParam(UrlEncodeAndJoinOnNewline.class) Iterable<String> paths);
+
+   // NOTE: this cannot be tested on MWS and is also brittle, as it relies on
+   // sending a body on DELETE.
+   // https://bugs.launchpad.net/swift/+bug/1232787
+   static class UrlEncodeAndJoinOnNewline implements Binder {
+      @SuppressWarnings("unchecked")
+      @Override
+      public <R extends HttpRequest> R bindToRequest(R request, Object input) {
+         String encodedAndNewlineDelimited = Joiner.on('\n').join(
+               transform(Iterable.class.cast(input), urlFragmentEscaper().asFunction()));
+         Payload payload = Payloads.newStringPayload(encodedAndNewlineDelimited);
+         payload.getContentMetadata().setContentType(TEXT_PLAIN);
+         return (R) request.toBuilder().payload(payload).build();
+      }
+   }
+}

http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/blob/6fbc1932/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/config/SwiftTypeAdaptersTest.java
----------------------------------------------------------------------
diff --git a/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/config/SwiftTypeAdaptersTest.java b/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/config/SwiftTypeAdaptersTest.java
new file mode 100644
index 0000000..30edc8f
--- /dev/null
+++ b/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/config/SwiftTypeAdaptersTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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.config;
+
+import static org.testng.Assert.assertEquals;
+
+import org.jclouds.openstack.swift.v1.config.SwiftTypeAdapters.BulkDeleteResponseAdapter;
+import org.jclouds.openstack.swift.v1.config.SwiftTypeAdapters.ExtractArchiveResponseAdapter;
+import org.jclouds.openstack.swift.v1.domain.BulkDeleteResponse;
+import org.jclouds.openstack.swift.v1.domain.ExtractArchiveResponse;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+@Test
+public class SwiftTypeAdaptersTest {
+   Gson gson = new GsonBuilder() //
+         .registerTypeAdapter(ExtractArchiveResponse.class, new ExtractArchiveResponseAdapter()) //
+         .registerTypeAdapter(BulkDeleteResponse.class, new BulkDeleteResponseAdapter()) //
+         .create();
+
+   public void extractArchiveWithoutErrors() {
+      assertEquals(gson.fromJson("" //
+            + "{\n" //
+            + "  \"Response Status\": \"201 Created\",\n" //
+            + "  \"Response Body\": \"\",\n" //
+            + "  \"Errors\": [],\n" //
+            + "  \"Number Files Created\": 10\n" //
+            + "}", ExtractArchiveResponse.class), ExtractArchiveResponse.create(10, ImmutableMap.<String, String> of()));
+   }
+
+   public void extractArchiveWithErrorsAndDecodesPaths() {
+      assertEquals(
+            gson.fromJson("" //
+                  + "{\n" //
+                  + "  \"Response Status\": \"201 Created\",\n" //
+                  + "  \"Response Body\": \"\",\n" //
+                  + "  \"Errors\": [\n" //
+                  + "    [\"/v1/12345678912345/mycontainer/home/xx%3Cyy\", \"400 Bad Request\"],\n" //
+                  + "    [\"/v1/12345678912345/mycontainer/../image.gif\", \"400 Bad Request\"]\n" //
+                  + "  ],\n" //
+                  + "  \"Number Files Created\": 8\n" //
+                  + "}", ExtractArchiveResponse.class),
+            ExtractArchiveResponse.create(
+                  8,
+                  ImmutableMap.<String, String> builder()
+                        .put("/v1/12345678912345/mycontainer/home/xx<yy", "400 Bad Request")
+                        .put("/v1/12345678912345/mycontainer/../image.gif", "400 Bad Request").build()));
+   }
+
+   public void bulkDeleteWithoutErrors() {
+      assertEquals(gson.fromJson("" //
+            + "{\n" //
+            + "  \"Response Status\": \"200 OK\",\n" //
+            + "  \"Response Body\": \"\",\n" //
+            + "  \"Errors\": [],\n" //
+            + "  \"Number Not Found\": 1,\n" //
+            + "  \"Number Deleted\": 9\n" //
+            + "}", BulkDeleteResponse.class), BulkDeleteResponse.create(9, 1, ImmutableMap.<String, String> of()));
+   }
+
+   public void bulkDeleteWithErrorsAndDecodesPaths() {
+      assertEquals(gson.fromJson("" //
+            + "{\n" //
+            + "  \"Response Status\": \"400 Bad Request\",\n" //
+            + "  \"Response Body\": \"\",\n" //
+            + "  \"Errors\": [\n" //
+            + "    [\"/v1/12345678912345/Not%20Empty\", \"409 Conflict\"]" //
+            + "  ],\n" //
+            + "  \"Number Deleted\": 0\n" //
+            + "}", BulkDeleteResponse.class),
+            BulkDeleteResponse.create(0, 0, ImmutableMap.of("/v1/12345678912345/Not Empty", "409 Conflict")));
+   }
+}

http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/blob/6fbc1932/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/BulkApiLiveTest.java
----------------------------------------------------------------------
diff --git a/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/BulkApiLiveTest.java b/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/BulkApiLiveTest.java
new file mode 100644
index 0000000..e2b17ec
--- /dev/null
+++ b/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/BulkApiLiveTest.java
@@ -0,0 +1,142 @@
+/*
+ * 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.Preconditions.checkState;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.UUID;
+
+import org.jboss.shrinkwrap.api.GenericArchive;
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.jboss.shrinkwrap.api.exporter.TarGzExporter;
+import org.jclouds.io.Payloads;
+import org.jclouds.openstack.swift.v1.domain.BulkDeleteResponse;
+import org.jclouds.openstack.swift.v1.domain.ExtractArchiveResponse;
+import org.jclouds.openstack.swift.v1.domain.SwiftObject;
+import org.jclouds.openstack.swift.v1.internal.BaseSwiftApiLiveTest;
+import org.jclouds.openstack.swift.v1.options.CreateContainerOptions;
+import org.jclouds.openstack.swift.v1.options.ListContainerOptions;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import com.google.common.base.Function;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.io.ByteStreams;
+
+@Test(groups = "live", testName = "BulkApiLiveTest")
+public class BulkApiLiveTest extends BaseSwiftApiLiveTest {
+
+   static final int OBJECT_COUNT = 10;
+
+   private String containerName = getClass().getSimpleName();
+
+   public void notPresentWhenDeleting() throws Exception {
+      for (String regionId : api.configuredRegions()) {
+         BulkDeleteResponse deleteResponse = api.bulkApiInRegion(regionId).bulkDelete(
+               ImmutableList.of(UUID.randomUUID().toString()));
+         assertEquals(deleteResponse.deleted(), 0);
+         assertEquals(deleteResponse.notFound(), 1);
+         assertTrue(deleteResponse.errors().isEmpty());
+      }
+   }
+
+   public void extractArchive() throws Exception {
+      for (String regionId : api.configuredRegions()) {
+         ExtractArchiveResponse extractResponse = api.bulkApiInRegion(regionId).extractArchive(containerName,
+               Payloads.newPayload(tarGz), "tar.gz");
+         assertEquals(extractResponse.created(), OBJECT_COUNT);
+         assertTrue(extractResponse.errors().isEmpty());
+         assertEquals(api.containerApiInRegion(regionId).get(containerName).objectCount(), OBJECT_COUNT);
+
+         // repeat the command
+         api.bulkApiInRegion(regionId).extractArchive(containerName, Payloads.newPayload(tarGz), "tar.gz");
+         assertEquals(extractResponse.created(), OBJECT_COUNT);
+         assertTrue(extractResponse.errors().isEmpty());
+      }
+   }
+
+   @Test(dependsOnMethods = "extractArchive")
+   public void bulkDelete() throws Exception {
+      for (String regionId : api.configuredRegions()) {
+         BulkDeleteResponse deleteResponse = api.bulkApiInRegion(regionId).bulkDelete(paths);
+         assertEquals(deleteResponse.deleted(), OBJECT_COUNT);
+         assertEquals(deleteResponse.notFound(), 0);
+         assertTrue(deleteResponse.errors().isEmpty());
+         assertEquals(api.containerApiInRegion(regionId).get(containerName).objectCount(), 0);
+      }
+   }
+
+   List<String> paths = Lists.newArrayList();
+   byte[] tarGz;
+
+   @Override
+   @BeforeClass(groups = "live")
+   public void setup() {
+      super.setup();
+      for (String regionId : api.configuredRegions()) {
+         boolean created = api.containerApiInRegion(regionId).createIfAbsent(containerName,
+               new CreateContainerOptions());
+         if (!created) {
+            deleteAllObjectsInContainer(regionId);
+         }
+      }
+      GenericArchive files = ShrinkWrap.create(GenericArchive.class, "files.tar.gz");
+      StringAsset content = new StringAsset("foo");
+      for (int i = 0; i < OBJECT_COUNT; i++) {
+         paths.add(containerName + "/file" + i);
+         files.add(content, "/file" + i);
+      }
+      try {
+         tarGz = ByteStreams.toByteArray(files.as(TarGzExporter.class).exportAsInputStream());
+      } catch (IOException e) {
+         throw Throwables.propagate(e);
+      }
+   }
+
+   @Override
+   @AfterClass(groups = "live")
+   public void tearDown() {
+      for (String regionId : api.configuredRegions()) {
+         deleteAllObjectsInContainer(regionId);
+         api.containerApiInRegion(regionId).deleteIfEmpty(containerName);
+      }
+      super.tearDown();
+   }
+
+   void deleteAllObjectsInContainer(String regionId) {
+      ImmutableList<String> pathsToDelete = api.objectApiInRegionForContainer(regionId, containerName)
+            .list(new ListContainerOptions()).transform(new Function<SwiftObject, String>() {
+
+               public String apply(SwiftObject input) {
+                  return containerName + "/" + input.name();
+               }
+
+            }).toList();
+      if (!pathsToDelete.isEmpty()) {
+         BulkDeleteResponse response = api.bulkApiInRegion(regionId).bulkDelete(pathsToDelete);
+         checkState(response.errors().isEmpty(), "Errors deleting paths %s: %s", pathsToDelete, response);
+      }
+   }
+}

http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/blob/6fbc1932/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/BulkApiMockTest.java
----------------------------------------------------------------------
diff --git a/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/BulkApiMockTest.java b/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/BulkApiMockTest.java
new file mode 100644
index 0000000..86832b4
--- /dev/null
+++ b/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/BulkApiMockTest.java
@@ -0,0 +1,70 @@
+/*
+ * 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 org.jclouds.io.Payloads.newByteArrayPayload;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+import org.jboss.shrinkwrap.api.GenericArchive;
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.jboss.shrinkwrap.api.exporter.TarGzExporter;
+import org.jclouds.openstack.swift.v1.SwiftApi;
+import org.jclouds.openstack.swift.v1.domain.ExtractArchiveResponse;
+import org.jclouds.openstack.swift.v1.internal.BaseSwiftMockTest;
+import org.testng.annotations.Test;
+
+import com.google.common.io.ByteStreams;
+import com.squareup.okhttp.mockwebserver.MockResponse;
+import com.squareup.okhttp.mockwebserver.MockWebServer;
+import com.squareup.okhttp.mockwebserver.RecordedRequest;
+
+// TODO: cannot yet test bulk delete offline
+@Test
+public class BulkApiMockTest extends BaseSwiftMockTest {
+
+   public void extractArchive() throws Exception {
+      GenericArchive files = ShrinkWrap.create(GenericArchive.class, "files.tar.gz");
+      StringAsset content = new StringAsset("foo");
+      for (int i = 0; i < 10; i++) {
+         files.add(content, "/file" + i);
+      }
+      byte[] tarGz = ByteStreams.toByteArray(files.as(TarGzExporter.class).exportAsInputStream());
+
+      MockWebServer server = mockSwiftServer();
+      server.enqueue(new MockResponse().setBody(access));
+      server.enqueue(new MockResponse().setResponseCode(201).setBody("{\"Number Files Created\": 10, \"Errors\": []}"));
+
+      try {
+         SwiftApi api = swiftApi(server.getUrl("/").toString());
+         ExtractArchiveResponse response = api.bulkApiInRegion("DFW").extractArchive("myContainer",
+               newByteArrayPayload(tarGz), "tar.gz");
+         assertEquals(response.created(), 10);
+         assertTrue(response.errors().isEmpty());
+
+         assertEquals(server.getRequestCount(), 2);
+         assertEquals(server.takeRequest().getRequestLine(), "POST /tokens HTTP/1.1");
+         RecordedRequest extractRequest = server.takeRequest();
+         assertEquals(extractRequest.getRequestLine(),
+               "PUT /v1/MossoCloudFS_5bcf396e-39dd-45ff-93a1-712b9aba90a9/myContainer?extract-archive=tar.gz HTTP/1.1");
+         assertEquals(extractRequest.getBody(), tarGz);
+      } finally {
+         server.shutdown();
+      }
+   }
+}

http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/blob/6fbc1932/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/UrlEncodeAndJoinOnNewlineTest.java
----------------------------------------------------------------------
diff --git a/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/UrlEncodeAndJoinOnNewlineTest.java b/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/UrlEncodeAndJoinOnNewlineTest.java
new file mode 100644
index 0000000..a45386b
--- /dev/null
+++ b/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/UrlEncodeAndJoinOnNewlineTest.java
@@ -0,0 +1,45 @@
+/*
+ * 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 org.testng.Assert.assertEquals;
+
+import org.jclouds.http.HttpRequest;
+import org.jclouds.openstack.swift.v1.features.BulkApi.UrlEncodeAndJoinOnNewline;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableList;
+
+@Test
+public class UrlEncodeAndJoinOnNewlineTest {
+   UrlEncodeAndJoinOnNewline binder = new UrlEncodeAndJoinOnNewline();
+
+   public void urlEncodesPaths() {
+      HttpRequest request = HttpRequest.builder()
+                                       .method("DELETE")
+                                       .endpoint("https://storage101.dfw1.clouddrive.com/v1/MossoCloudFS_XXXXXX/")
+                                       .addQueryParam("bulk-delete").build();
+
+      request = binder.bindToRequest(request, ImmutableList.<String> builder()
+            .add("/v1/12345678912345/mycontainer/home/xx<yy")
+            .add("/v1/12345678912345/mycontainer/../image.gif").build());
+
+      assertEquals(request.getPayload().getRawContent(), "" //
+            + "/v1/12345678912345/mycontainer/home/xx%3Cyy\n" //
+            + "/v1/12345678912345/mycontainer/../image.gif");
+   }
+}