private @Nullable ResourceStatus toStatus(@Nullable String container)
      throws CloudException, InternalException {
    if (container == null) {
      return null;
    }
    NovaMethod method = new NovaMethod(provider);
    Map<String, String> headers = method.headResource(SERVICE, RESOURCE, container);

    if (headers == null) {
      return null;
    }
    String enabled = null, uriString = null;
    for (String key : headers.keySet()) {
      if (key.equalsIgnoreCase("X-CDN-Enabled")) {
        enabled = headers.get(key);
      } else if (key.equalsIgnoreCase("X-CDN-URI")) {
        if (uriString == null) {
          uriString = headers.get(key);
        }
      } else if (key.equalsIgnoreCase("X-CDN-SSL-URI")) {
        uriString = headers.get(key);
      }
    }
    if (uriString == null) {
      return null;
    }
    return new ResourceStatus(container, enabled != null && enabled.equalsIgnoreCase("true"));
  }
  private CompleteDNS getCompleteDNS(@Nonnull String providerDnsZoneId, boolean withSubdomains)
      throws CloudException, InternalException {
    Logger std = NovaOpenStack.getLogger(RackspaceCloudDNS.class, "std");

    if (std.isTraceEnabled()) {
      std.trace("ENTER: " + RackspaceCloudDNS.class.getName() + ".getCompleteDNS()");
    }
    try {
      ProviderContext ctx = provider.getContext();

      if (ctx == null) {
        std.error("No context exists for this request");
        throw new InternalException("No context exists for this request");
      }
      String query = providerDnsZoneId + "?showRecords=true";

      if (withSubdomains) {
        query = query + "&showSubdomains=true";
      }
      NovaMethod method = new NovaMethod(provider);
      JSONObject response = method.getResource(SERVICE, RESOURCE, query, false);

      if (response == null) {
        return null;
      }
      try {
        DNSZone zone = toZone(ctx, response);

        if (zone != null) {
          CompleteDNS dns = new CompleteDNS();

          dns.domain = zone;
          dns.subdomains = new ArrayList<DNSZone>();

          JSONObject subdomains =
              (response.has("subdomains") ? response.getJSONObject("subdomains") : null);

          if (subdomains != null) {
            JSONArray domains =
                (subdomains.has("domains") ? subdomains.getJSONArray("domains") : null);

            if (domains != null) {
              listSubdomains(ctx, dns.subdomains, zone, domains);
            }
          }
          return dns;
        }
      } catch (JSONException e) {
        std.error("getCompleteDNS(): JSON error parsing response: " + e.getMessage());
        e.printStackTrace();
        throw new CloudException(
            CloudErrorType.COMMUNICATION, 200, "invalidResponse", "JSON error parsing " + response);
      }
      return null;
    } finally {
      if (std.isTraceEnabled()) {
        std.trace("exit - " + RackspaceCloudDNS.class.getName() + ".getCompleteDNS()");
      }
    }
  }
  @Override
  public void deleteDnsRecords(@Nonnull DNSRecord... dnsRecords)
      throws CloudException, InternalException {
    APITrace.begin(provider, "DNS.deleteDnsRecords");
    try {
      ProviderContext ctx = provider.getContext();

      if (ctx == null) {
        logger.error("No context exists for this request");
        throw new InternalException("No context exists for this request");
      }
      for (DNSRecord record : dnsRecords) {
        NovaMethod method = new NovaMethod(provider);

        List<String> ids = lookupRecord(record);

        for (String id : ids) {
          method.deleteResource(
              SERVICE, RESOURCE, record.getProviderZoneId() + "/records/" + id, null);
        }
      }
    } finally {
      APITrace.end();
    }
  }
  @Override
  public @Nonnull Iterable<ResourceStatus> listDistributionStatus()
      throws InternalException, CloudException {
    APITrace.begin(provider, "CDN.listDistributionStatus");
    try {
      ProviderContext ctx = provider.getContext();

      if (ctx == null) {
        throw new InternalException("No context exists for this request");
      }
      ArrayList<ResourceStatus> distributions = new ArrayList<ResourceStatus>();
      NovaMethod method = new NovaMethod(provider);
      String[] list = method.getItemList(SERVICE, RESOURCE, false);

      if (list != null) {
        for (String container : list) {
          ResourceStatus d = toStatus(container);

          if (d != null) {
            distributions.add(d);
          }
        }
      }
      return distributions;
    } finally {
      APITrace.end();
    }
  }
  private @Nullable Distribution toDistribution(
      @Nonnull ProviderContext ctx, @Nullable String container)
      throws CloudException, InternalException {
    if (container == null) {
      return null;
    }
    NovaMethod method = new NovaMethod(provider);
    Map<String, String> headers = method.headResource(SERVICE, RESOURCE, container);

    if (headers == null) {
      return null;
    }
    String enabled = null, uriString = null;
    for (String key : headers.keySet()) {
      if (key.equalsIgnoreCase("X-CDN-Enabled")) {
        enabled = headers.get(key);
      } else if (key.equalsIgnoreCase("X-CDN-URI")) {
        if (uriString == null) {
          uriString = headers.get(key);
        }
      } else if (key.equalsIgnoreCase("X-CDN-SSL-URI")) {
        uriString = headers.get(key);
      }
    }
    if (uriString == null) {
      return null;
    }
    String dns;

    try {
      URI uri = new URI(uriString);

      dns = uri.getHost();
      if (uri.getPort() > 0) {
        if (dns.startsWith("https:") && uri.getPort() != 443) {
          dns = dns + ":" + uri.getPort();
        }
        if (dns.startsWith("http:") && uri.getPort() != 80) {
          dns = dns + ":" + uri.getPort();
        }
      }
    } catch (URISyntaxException e) {
      throw new CloudException(e);
    }

    Distribution distribution = new Distribution();

    distribution.setName(container);
    distribution.setActive(enabled != null && enabled.equalsIgnoreCase("true"));
    distribution.setAliases(new String[0]);
    distribution.setDeployed(enabled != null && enabled.equalsIgnoreCase("true"));
    distribution.setDnsName(dns);
    distribution.setLocation(uriString);
    distribution.setLogDirectory(null);
    distribution.setProviderDistributionId(container);
    distribution.setProviderOwnerId(getTenantId());
    return distribution;
  }
  @Override
  public @Nonnull String createDnsZone(
      @Nonnull String domainName, @Nonnull String name, @Nonnull String description)
      throws CloudException, InternalException {
    APITrace.begin(provider, "DNS.createDnsZone");
    try {
      ProviderContext ctx = provider.getContext();

      if (ctx == null) {
        logger.error("No context exists for this request");
        throw new InternalException("No context exists for this request");
      }
      NovaMethod method = new NovaMethod(provider);

      HashMap<String, Object> wrapper = new HashMap<String, Object>();
      ArrayList<Map<String, Object>> domains = new ArrayList<Map<String, Object>>();

      HashMap<String, Object> domain = new HashMap<String, Object>();

      domain.put("name", domainName);
      domain.put("comment", description);
      domain.put("emailAddress", "postmaster@" + domainName);

      domains.add(domain);

      wrapper.put("domains", domains);

      JSONObject response =
          method.postString(SERVICE, RESOURCE, null, new JSONObject(wrapper), false);

      try {
        if (response != null && response.has("jobId")) {
          response = waitForJob(response.getString("jobId"));
          if (response != null && response.has("domains")) {
            JSONArray list = response.getJSONArray("domains");

            for (int i = 0; i < list.length(); i++) {
              DNSZone zone = toZone(ctx, list.getJSONObject(i));

              if (zone != null) {
                return zone.getProviderDnsZoneId();
              }
            }
          }
        }
      } catch (JSONException e) {
        logger.error("createDnsZone(): JSON error parsing response: " + e.getMessage());
        e.printStackTrace();
        throw new CloudException(
            CloudErrorType.COMMUNICATION, 200, "invalidResponse", "JSON error parsing " + response);
      }
      logger.error("createDnsZone(): No zone was created, but no error specified");
      throw new CloudException("No zone was created, but no error specified");
    } finally {
      APITrace.end();
    }
  }
  @Override
  public @Nonnull Iterable<ResourceStatus> listDnsZoneStatus()
      throws CloudException, InternalException {
    APITrace.begin(provider, "DNS.listDnsZoneStatus");
    try {
      ProviderContext ctx = provider.getContext();

      if (ctx == null) {
        throw new InternalException("No context exists for this request");
      }
      NovaMethod method = new NovaMethod(provider);
      JSONObject response = method.getResource(SERVICE, RESOURCE, null, false);

      if (response == null) {
        return Collections.emptyList();
      }
      ArrayList<ResourceStatus> zones = new ArrayList<ResourceStatus>();

      try {
        int count = 0, total = 0;

        if (response.has("totalEntries")) {
          total = response.getInt("totalEntries");
        }
        while (response != null) {
          int current = 0;

          if (response.has("domains")) {
            JSONArray list = response.getJSONArray("domains");

            current = list.length();
            count += current;
            for (int i = 0; i < list.length(); i++) {
              JSONObject item = list.getJSONObject(i);

              if (item != null && item.has("id")) {
                zones.add(new ResourceStatus(item.getString("id"), true));
              }
            }
          }
          response = null;
          if (current > 0 && count < total) {
            response = method.getResource(SERVICE, RESOURCE, "?offset=" + count, false);
          }
        }
      } catch (JSONException e) {
        throw new CloudException(
            CloudErrorType.COMMUNICATION, 200, "invalidResponse", "JSON error parsing " + response);
      }
      return zones;
    } finally {
      APITrace.end();
    }
  }
  private @Nonnull List<String> lookupRecord(DNSRecord record)
      throws CloudException, InternalException {
    APITrace.begin(provider, "DNS.lookupRecord");
    try {
      ProviderContext ctx = provider.getContext();

      if (ctx == null) {
        logger.error("No context exists for this request");
        throw new InternalException("No context exists for this request");
      }
      NovaMethod method = new NovaMethod(provider);
      JSONObject response =
          method.getResource(SERVICE, RESOURCE, record.getProviderZoneId() + "/records", false);

      if (response == null) {
        return null;
      }
      ArrayList<String> ids = new ArrayList<String>();

      try {
        if (response.has("records")) {
          JSONArray list = response.getJSONArray("records");

          for (int i = 0; i < list.length(); i++) {
            JSONObject item = list.getJSONObject(i);

            if (item != null) {
              String n = (item.has("name") ? item.getString("name") : null);
              String id = (item.has("id") ? item.getString("id") : null);

              if (n == null || id == null) {
                continue;
              }
              if (record.getName().equals(n) || record.getName().equals(n + ".")) {
                ids.add(id);
              }
            }
          }
        }
      } catch (JSONException e) {
        logger.error("lookupRecord(): JSON error parsing response: " + e.getMessage());
        e.printStackTrace();
        throw new CloudException(
            CloudErrorType.COMMUNICATION, 200, "invalidResponse", "JSON error parsing " + response);
      }
      return ids;
    } finally {
      APITrace.end();
    }
  }
  private JSONObject waitForJob(String jobId) throws CloudException, InternalException {
    long timeout = System.currentTimeMillis() + (CalendarWrapper.MINUTE * 20);

    ProviderContext ctx = provider.getContext();

    if (ctx == null) {
      throw new InternalException("No context exists for this request");
    }
    while (System.currentTimeMillis() < timeout) {
      try {
        NovaMethod method = new NovaMethod(provider);
        JSONObject response =
            method.getResource(SERVICE, "/status", jobId + "?showDetails=true", false);

        if (response == null) {
          throw new CloudException("Job disappeared");
        }
        String status = (response.has("status") ? response.getString("status") : null);

        if (status == null) {
          throw new CloudException("No job status");
        }
        if (status.equalsIgnoreCase("completed")) {
          if (response.has("response")) {
            return response.getJSONObject("response");
          }
        } else if (status.equalsIgnoreCase("error")) {
          if (response.has("error")) {
            JSONObject error = response.getJSONObject("error");

            if (error == null) {
              throw new CloudException("Unknown error");
            }
            int code = (error.has("code") ? error.getInt("code") : 418);

            throw new NovaException(NovaException.parseException(code, error.toString()));
          }
          throw new CloudException("Unknown error");
        }
      } catch (JSONException e) {
        throw new CloudException("Invalid JSON from server: " + e.getMessage());
      }
      try {
        Thread.sleep(CalendarWrapper.SECOND * 30);
      } catch (InterruptedException ignore) {
      }
    }
    throw new CloudException("Operation timed out");
  }
  @Override
  public void delete(@Nonnull String distributionId) throws InternalException, CloudException {
    APITrace.begin(provider, "CDN.delete");
    try {
      HashMap<String, String> customHeaders = new HashMap<String, String>();

      customHeaders.put("X-Log-Retention", "True");
      customHeaders.put("X-CDN-Enabled", "False");

      NovaMethod method = new NovaMethod(provider);

      method.putResourceHeaders(SERVICE, RESOURCE, distributionId, customHeaders);
    } finally {
      APITrace.end();
    }
  }
  @Override
  public void deleteDnsZone(@Nonnull String providerDnsZoneId)
      throws CloudException, InternalException {
    APITrace.begin(provider, "DNS.deleteDnsZone");
    try {
      ProviderContext ctx = provider.getContext();

      if (ctx == null) {
        logger.error("No context exists for this request");
        throw new InternalException("No context exists for this request");
      }
      NovaMethod method = new NovaMethod(provider);

      method.deleteResource(SERVICE, RESOURCE, providerDnsZoneId, null);
    } finally {
      APITrace.end();
    }
  }
  @Override
  public @Nonnull String create(
      @Nonnull String origin, @Nonnull String name, boolean active, @Nullable String... aliases)
      throws InternalException, CloudException {
    APITrace.begin(provider, "CDN.create");
    try {
      HashMap<String, String> customHeaders = new HashMap<String, String>();

      customHeaders.put("X-Log-Retention", "True");
      customHeaders.put("X-CDN-Enabled", "True");
      NovaMethod method = new NovaMethod(provider);

      method.putResourceHeaders(SERVICE, RESOURCE, origin, customHeaders);
      return origin;
    } finally {
      APITrace.end();
    }
  }
  @Override
  public @Nonnull DNSRecord addDnsRecord(
      @Nonnull String providerDnsZoneId,
      @Nonnull DNSRecordType recordType,
      @Nonnull String name,
      @Nonnegative int ttl,
      @Nonnull String... values)
      throws CloudException, InternalException {
    APITrace.begin(provider, "DNS.addDnsRecord");
    try {
      DNSZone zone = getDnsZone(providerDnsZoneId);

      if (zone == null) {
        throw new CloudException("No such zone: " + providerDnsZoneId);
      }
      if (recordType.equals(DNSRecordType.A)
          || recordType.equals(DNSRecordType.AAAA)
          || recordType.equals(DNSRecordType.CNAME)
          || recordType.equals(DNSRecordType.MX)) {
        if (name.endsWith(zone.getDomainName() + ".")) {
          name = name.substring(0, name.length() - 1);
        } else if (!name.endsWith(zone.getDomainName())) {
          name = name + "." + zone.getDomainName();
        }
      }
      ProviderContext ctx = provider.getContext();

      if (ctx == null) {
        logger.error("No context exists for this request");
        throw new InternalException("No context exists for this request");
      }
      DNSRecord lastRecord = null;

      for (String value : values) {
        if (value != null) {
          NovaMethod method = new NovaMethod(provider);

          HashMap<String, Object> wrapper = new HashMap<String, Object>();
          ArrayList<Map<String, Object>> records = new ArrayList<Map<String, Object>>();

          HashMap<String, Object> record = new HashMap<String, Object>();

          record.put("name", name);
          record.put("data", value);
          record.put("type", recordType.name());
          record.put("ttl", ttl > 0 ? ttl : 3600);

          records.add(record);

          wrapper.put("records", records);

          JSONObject response =
              method.postString(
                  SERVICE,
                  RESOURCE,
                  providerDnsZoneId + "/records",
                  new JSONObject(wrapper),
                  false);

          try {
            if (response != null && response.has("jobId")) {
              response = waitForJob(response.getString("jobId"));
              if (response != null && response.has("records")) {
                JSONArray list = response.getJSONArray("records");

                for (int i = 0; i < list.length(); i++) {
                  DNSRecord r = toRecord(ctx, zone, list.getJSONObject(i));

                  if (r != null) {
                    lastRecord = r;
                  }
                }
              }
            }
          } catch (JSONException e) {
            logger.error("createDnsZone(): JSON error parsing response: " + e.getMessage());
            e.printStackTrace();
            throw new CloudException(
                CloudErrorType.COMMUNICATION,
                200,
                "invalidResponse",
                "JSON error parsing " + response);
          }
        }
      }
      if (lastRecord == null) {
        logger.error("addDnsRecord(): No record was created, but no error specified");
        throw new CloudException("No record was created, but no error specified");
      }
      return lastRecord;
    } finally {
      APITrace.end();
    }
  }
  @Override
  public @Nonnull Iterable<DNSRecord> listDnsRecords(
      @Nonnull String providerDnsZoneId, @Nullable DNSRecordType forType, @Nullable String name)
      throws CloudException, InternalException {
    APITrace.begin(provider, "DNS.listDnsRecords");
    try {
      DNSZone zone = getDnsZone(providerDnsZoneId);

      if (zone == null) {
        throw new CloudException("No such zone: " + providerDnsZoneId);
      }
      ProviderContext ctx = provider.getContext();

      if (ctx == null) {
        logger.error("No context exists for this request");
        throw new InternalException("No context exists for this request");
      }
      NovaMethod method = new NovaMethod(provider);
      JSONObject response =
          method.getResource(SERVICE, RESOURCE, providerDnsZoneId + "/records", false);

      if (response == null) {
        return Collections.emptyList();
      }
      ArrayList<DNSRecord> records = new ArrayList<DNSRecord>();
      try {
        int count = 0, total = 0;

        if (response.has("totalEntries")) {
          total = response.getInt("totalEntries");
        }
        while (response != null) {
          int current = 0;

          if (response.has("records")) {
            JSONArray list = response.getJSONArray("records");

            current = list.length();
            count += current;
            for (int i = 0; i < list.length(); i++) {
              DNSRecord record = toRecord(ctx, zone, list.getJSONObject(i));

              if (record != null) {
                if (forType == null || forType.equals(record.getType())) {
                  if (name == null || name.equals(record.getName())) {
                    records.add(record);
                  }
                }
              }
            }
          }
          response = null;
          if (current > 0 && count < total) {
            response =
                method.getResource(
                    SERVICE, RESOURCE, providerDnsZoneId + "/records?offset=" + count, false);
          }
        }
      } catch (JSONException e) {
        logger.error("listDnsRecords(): JSON error parsing response: " + e.getMessage());
        e.printStackTrace();
        throw new CloudException(
            CloudErrorType.COMMUNICATION, 200, "invalidResponse", "JSON error parsing " + response);
      }
      return records;
    } finally {
      APITrace.end();
    }
  }
  @Override
  public @Nullable Distribution getDistribution(@Nonnull String distributionId)
      throws InternalException, CloudException {
    APITrace.begin(provider, "CDN.getDistribution");
    try {
      ProviderContext ctx = provider.getContext();

      if (ctx == null) {
        throw new InternalException("No context exists for this request");
      }
      NovaMethod method = new NovaMethod(provider);
      Map<String, String> metaData = method.headResource(SERVICE, RESOURCE, distributionId);

      if (metaData == null) {
        return null;
      }
      Distribution distribution = new Distribution();

      distribution.setAliases(new String[0]);

      String dnsName = null, cdnUri = null;
      boolean enabled = false;

      for (String key : metaData.keySet()) {
        if (key.equalsIgnoreCase("X-CDN-Enabled")) {
          String value = metaData.get(key);

          enabled = (value != null && value.equalsIgnoreCase("true"));
        } else if (key.equalsIgnoreCase("X-CDN-SSL-URI")) {
          dnsName = metaData.get(key);
        } else if (key.equalsIgnoreCase("X-CDN-URI")) {
          cdnUri = metaData.get(key);
        }
      }
      distribution.setActive(enabled);
      distribution.setDeployed(enabled);

      String prefix = "http://";

      if (dnsName == null) {
        dnsName = cdnUri;
        if (dnsName == null) {
          return null;
        }
        if (dnsName.startsWith("http://")) {
          dnsName = dnsName.substring("http://".length());
        }
      } else {
        if (dnsName.startsWith("https://")) {
          dnsName = dnsName.substring("https://".length());
          prefix = "https://";
        }
      }
      distribution.setDnsName(dnsName);
      distribution.setLocation(prefix + distribution.getDnsName() + "/" + distributionId);
      distribution.setLogDirectory(null);
      distribution.setLogName(null);
      distribution.setName(distributionId);
      distribution.setProviderDistributionId(distributionId);
      distribution.setProviderOwnerId(getTenantId());
      return distribution;
    } finally {
      APITrace.end();
    }
  }