@Override
  public @Nonnull Blob createBucket(@Nonnull String bucketName, boolean findFreeName)
      throws InternalException, CloudException {
    APITrace.begin(getProvider(), "Blob.createBucket");
    try {
      if (bucketName.contains("/")) {
        throw new OperationNotSupportedException("Nested buckets are not supported");
      }
      String regionId = getContext().getRegionId();

      if (regionId == null) {
        throw new InternalException("No region ID was specified for this request");
      }

      GlacierMethod method =
          GlacierMethod.build(getProvider(), GlacierAction.CREATE_VAULT)
              .vaultId(bucketName)
              .toMethod();
      method.invoke();
      String url = method.getUrl();
      return Blob.getInstance(regionId, url, bucketName, System.currentTimeMillis());
    } finally {
      APITrace.end();
    }
  }
  @Nullable
  @Override
  public OfflineStoreRequest getRequest(@Nonnull String bucket, @Nonnull String requestId)
      throws CloudException, InternalException {
    APITrace.begin(getProvider(), "Blob.getRequest");
    try {

      final GlacierMethod method =
          GlacierMethod.build(getProvider(), GlacierAction.DESCRIBE_JOB)
              .vaultId(bucket)
              .jobId(requestId)
              .toMethod();

      try {
        final JSONObject jsonObject = method.invokeJson();
        return loadRequestJson(jsonObject, bucket);

      } catch (GlacierException e) {
        if (e.getHttpCode() == 404) {
          return null;
        }
        throw e;
      } catch (JSONException e) {
        throw new CloudException(e);
      }
    } finally {
      APITrace.end();
    }
  }
  @Override
  public void removeBucket(@Nonnull String bucket) throws CloudException, InternalException {
    APITrace.begin(getProvider(), "Blob.removeBucket");
    try {
      String regionId = getContext().getRegionId();

      if (regionId == null) {
        throw new CloudException("No region was set for this request");
      }

      GlacierMethod method =
          GlacierMethod.build(getProvider(), GlacierAction.DELETE_VAULT).vaultId(bucket).toMethod();
      method.invoke();
    } finally {
      APITrace.end();
    }
  }
  private void listRequests(
      @Nonnull String bucket, @Nonnull Jiterator<OfflineStoreRequest> iterator)
      throws CloudException, InternalException {
    APITrace.begin(getProvider(), "Blob.listRequests");
    try {

      boolean needQuery = true;
      String marker = null;
      Map<String, String> queryParameters = new HashMap<String, String>(1);

      // glacier can paginate results. it returns a "marker" string in the JSON response
      // which indicates you should make another query. The marker string should be passed
      // as a query parameter in the next query.

      while (needQuery) {
        if (marker != null) {
          queryParameters.put("marker", marker);
        }

        GlacierMethod method =
            GlacierMethod.build(getProvider(), GlacierAction.LIST_JOBS)
                .vaultId(bucket)
                .queryParameters(queryParameters)
                .toMethod();
        final JSONObject jsonObject = method.invokeJson();

        try {
          JSONArray vaultList = jsonObject.getJSONArray("JobList");

          for (int i = 0; i < vaultList.length(); i++) {
            iterator.push(loadRequestJson(vaultList.getJSONObject(i), bucket));
          }

          marker = getPaginationMarker(jsonObject);
          if (marker == null) {
            needQuery = false;
          }
        } catch (JSONException e) {
          throw new CloudException(e);
        }
      }
    } finally {
      APITrace.end();
    }
  }
  @Nonnull
  @Override
  public OfflineStoreRequest createListRequest(@Nonnull String bucket)
      throws CloudException, InternalException {
    APITrace.begin(getProvider(), "Blob.createListRequest");
    try {
      try {

        JSONObject bodyJson = new JSONObject();
        bodyJson.put("Type", "inventory-retrieval");

        final GlacierMethod method =
            GlacierMethod.build(getProvider(), GlacierAction.CREATE_JOB)
                .vaultId(bucket)
                .bodyText(bodyJson.toString())
                .toMethod();

        Map<String, String> responseHeaders = method.invokeHeaders();
        if (!responseHeaders.containsKey(HEADER_JOB_ID)) {
          throw new CloudException("Glacier response missing " + HEADER_JOB_ID + " header");
        }
        String jobId = responseHeaders.get(HEADER_JOB_ID);

        return new OfflineStoreRequest(
            jobId,
            bucket,
            null,
            OfflineStoreRequestAction.LIST,
            ACTION_INVENTORY_RETRIEVAL,
            null,
            "",
            OfflineStoreRequestStatus.IN_PROGRESS,
            "",
            System.currentTimeMillis(),
            -1);

      } catch (JSONException e) {
        throw new CloudException(e);
      }
    } finally {
      APITrace.end();
    }
  }
  @Override
  public boolean isSubscribed() throws CloudException, InternalException {
    APITrace.begin(getProvider(), "Blob.isSubscribed");
    try {
      final String regionId = getContext().getRegionId();

      if (regionId == null) {
        throw new CloudException("No region ID was specified");
      }
      try {
        GlacierMethod method =
            GlacierMethod.build(getProvider(), GlacierAction.LIST_VAULTS).toMethod();
        method.invoke();
        return true;
      } catch (CloudException e) {
        return false;
      }
    } finally {
      APITrace.end();
    }
  }
  private void loadVaults(@Nonnull String regionId, @Nonnull Jiterator<Blob> iterator)
      throws CloudException, InternalException {

    boolean needQuery = true;
    String marker = null;
    Map<String, String> queryParameters = new HashMap<String, String>(1);

    // glacier can paginate results. it returns a "marker" string in the JSON response
    // which indicates you should make another query. The marker string should be passed
    // as a query parameter in the next query.

    while (needQuery) {
      if (marker != null) {
        queryParameters.put("marker", marker);
      }

      GlacierMethod method =
          GlacierMethod.build(getProvider(), GlacierAction.LIST_VAULTS)
              .queryParameters(queryParameters)
              .toMethod();
      String baseUrl = method.getUrl();
      JSONObject jsonObject = method.invokeJson();

      try {
        JSONArray vaultList = jsonObject.getJSONArray("VaultList");

        for (int i = 0; i < vaultList.length(); i++) {
          iterator.push(loadVaultJson(vaultList.getJSONObject(i), regionId, baseUrl));
        }

        marker = getPaginationMarker(jsonObject);
        if (marker == null) {
          needQuery = false;
        }
      } catch (JSONException e) {
        throw new CloudException(e);
      }
    }
  }
  @Nonnull
  @Override
  public Iterable<Blob> getListRequestResult(@Nonnull String bucket, @Nonnull String requestId)
      throws InternalException, CloudException {
    final String regionId = getContext().getRegionId();

    if (regionId == null) {
      throw new CloudException("No region ID was specified");
    }
    APITrace.begin(getProvider(), "Blob.getListRequestResult");
    try {

      final GlacierMethod method =
          GlacierMethod.build(getProvider(), GlacierAction.GET_JOB_OUTPUT)
              .vaultId(bucket)
              .jobId(requestId)
              .toMethod();

      try {
        JSONObject jsonObject = method.invokeJson();
        JSONArray archives = jsonObject.getJSONArray("ArchiveList");

        // not using thread here because there is no potential for pagination in the API
        List<Blob> blobs = new ArrayList<Blob>(archives.length());
        for (int i = 0; i < archives.length(); i++) {
          blobs.add(loadArchiveJson(archives.getJSONObject(i), bucket, regionId));
        }
        return blobs;

      } catch (JSONException e) {
        throw new CloudException(e);
      }
    } finally {
      APITrace.end();
    }
  }
  @Override
  public void removeObject(@Nullable String bucket, @Nonnull String name)
      throws CloudException, InternalException {
    APITrace.begin(getProvider(), "Blob.removeObject");
    try {
      if (bucket == null) {
        throw new CloudException("No bucket was specified for this request");
      }
      String regionId = getContext().getRegionId();

      if (regionId == null) {
        throw new CloudException("No region was set for this request");
      }

      GlacierMethod method =
          GlacierMethod.build(getProvider(), GlacierAction.DELETE_ARCHIVE)
              .vaultId(bucket)
              .archiveId(name)
              .toMethod();
      method.invoke();
    } finally {
      APITrace.end();
    }
  }
  @Override
  public Blob getBucket(@Nonnull String bucketName) throws InternalException, CloudException {
    APITrace.begin(getProvider(), "Blob.getBucket");
    try {
      if (bucketName.contains("/")) {
        return null;
      }

      String regionId = getContext().getRegionId();
      if (regionId == null) {
        throw new CloudException("No region was set for this request");
      }

      try {
        GlacierMethod method =
            GlacierMethod.build(getProvider(), GlacierAction.DESCRIBE_VAULT)
                .vaultId(bucketName)
                .toMethod();
        JSONObject jsonObject = method.invokeJson();
        if (jsonObject == null) {
          return null;
        }
        return loadVaultJson(jsonObject, regionId, method.getUrl());

      } catch (GlacierException e) {
        if (e.getHttpCode() == 404) {
          return null;
        }
        throw e;
      } catch (JSONException e) {
        throw new CloudException(e);
      }
    } finally {
      APITrace.end();
    }
  }