Package org.jets3t.service

Source Code of org.jets3t.service.CloudFrontService

/*
* JetS3t : Java S3 Toolkit
* Project hosted at http://bitbucket.org/jmurty/jets3t/
*
* Copyright 2008 - 2013 James Murty
*
* Licensed 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.jets3t.service;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpResponse;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.params.HttpProtocolParams;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.jets3t.service.impl.rest.CloudFrontXmlResponsesSaxParser;
import org.jets3t.service.impl.rest.CloudFrontXmlResponsesSaxParser.DistributionConfigHandler;
import org.jets3t.service.impl.rest.CloudFrontXmlResponsesSaxParser.DistributionHandler;
import org.jets3t.service.impl.rest.CloudFrontXmlResponsesSaxParser.DistributionListHandler;
import org.jets3t.service.impl.rest.CloudFrontXmlResponsesSaxParser.ErrorHandler;
import org.jets3t.service.impl.rest.CloudFrontXmlResponsesSaxParser.InvalidationHandler;
import org.jets3t.service.impl.rest.CloudFrontXmlResponsesSaxParser.InvalidationListHandler;
import org.jets3t.service.impl.rest.CloudFrontXmlResponsesSaxParser.OriginAccessIdentityConfigHandler;
import org.jets3t.service.impl.rest.CloudFrontXmlResponsesSaxParser.OriginAccessIdentityHandler;
import org.jets3t.service.impl.rest.CloudFrontXmlResponsesSaxParser.OriginAccessIdentityListHandler;
import org.jets3t.service.impl.rest.httpclient.JetS3tRequestAuthorizer;
import org.jets3t.service.model.S3Object;
import org.jets3t.service.model.cloudfront.*;
import org.jets3t.service.security.EncryptionUtil;
import org.jets3t.service.security.ProviderCredentials;
import org.jets3t.service.utils.RestUtils;
import org.jets3t.service.utils.ServiceUtils;

import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;

import com.jamesmurty.utils.XMLBuilder;

/**
* A service that handles communication with the Amazon CloudFront REST API, offering
* all the operations that can be performed on CloudFront distributions.
* <p>
* This class uses properties obtained through {@link Jets3tProperties}. For more information on
* these properties please refer to
* <a href="http://www.jets3t.org/toolkit/configuration.html">JetS3t Configuration</a>
* </p>
*
* @author James Murty
*/
public class CloudFrontService implements JetS3tRequestAuthorizer {
    private static final Log log = LogFactory.getLog(CloudFrontService.class);

    public static final String ENDPOINT = "https://cloudfront.amazonaws.com/";
    public static final String VERSION = "2012-05-05";
    public static final String XML_NAMESPACE = "http://cloudfront.amazonaws.com/doc/" + VERSION + "/";
    public static final String DEFAULT_BUCKET_SUFFIX = ".s3.amazonaws.com";
    public static final String ORIGIN_ACCESS_IDENTITY_URI_PATH = "/origin-access-identity/cloudfront";
    public static final String ORIGIN_ACCESS_IDENTITY_PREFIX = "origin-access-identity/cloudfront/";

    protected HttpClient httpClient;
    private CredentialsProvider credentialsProvider;

    private ProviderCredentials credentials;
    protected Jets3tProperties jets3tProperties;
    private String invokingApplicationDescription;
    protected int internalErrorRetryMax = 5;


    /**
     * The approximate difference in the current time between your computer and
     * Amazon's servers, measured in milliseconds.
     * <p>
     * This value is 0 by default. Use the {@link #getCurrentTimeWithOffset()}
     * to obtain the current time with this offset factor included, and the
     * {@link RestUtils#calculateTimeAdjustmentOffset(HttpResponse)} method to
     * calculate an offset value for your computer based on a response from an
     * AWS server.
     */
    protected long timeOffset = 0;

    /**
     * Constructs the service and initialises its properties.
     *
     * @param credentials                    the Storage Provider user credentials to use when communicating with CloudFront
     * @param invokingApplicationDescription a short description of the application using the service, suitable for inclusion in a
     *                                       user agent string for REST/HTTP requests. Ideally this would include the application's
     *                                       version number, for example: <code>Cockpit/0.7.3</code> or <code>My App Name/1.0</code>.
     *                                       May be null.
     * @param credentialsProvider            an implementation of the HttpClient CredentialsProvider interface, to provide a means for
     *                                       prompting for credentials when necessary. May be null.
     * @param jets3tProperties               JetS3t properties that will be applied within this service. May be null.
     */
    public CloudFrontService(ProviderCredentials credentials, String invokingApplicationDescription,
                             CredentialsProvider credentialsProvider, Jets3tProperties jets3tProperties) {
        this.credentials = credentials;
        this.invokingApplicationDescription = invokingApplicationDescription;
        this.credentialsProvider = credentialsProvider;
        if(jets3tProperties == null) {
            jets3tProperties = Jets3tProperties.getInstance(Constants.JETS3T_PROPERTIES_FILENAME);
        }
        this.jets3tProperties = jets3tProperties;
        this.internalErrorRetryMax = jets3tProperties.getIntProperty("cloudfront-service.internal-error-retry-max", 5);
        this.initializeDefaults();
    }

    protected void initializeDefaults() {
        // Configure the InetAddress DNS caching times to work well with CloudFront. The cached DNS will
        // timeout after 5 minutes, while failed DNS lookups will be retried after 1 second.
        System.setProperty("networkaddress.cache.ttl", "300");
        System.setProperty("networkaddress.cache.negative.ttl", "1");

        this.httpClient = initHttpConnection();
        initializeProxy();
    }

    protected HttpClient initHttpConnection() {
        return RestUtils.initHttpConnection(
                this,
                this.jets3tProperties,
                this.invokingApplicationDescription,
                this.credentialsProvider);
    }

    protected void initializeProxy() {
        // Retrieve Proxy settings.
        if(this.jets3tProperties.getBoolProperty("httpclient.proxy-autodetect", true)) {
            RestUtils.initHttpProxy(this.httpClient, this.jets3tProperties);
        }
        else {
            String proxyHostAddress = this.jets3tProperties.getStringProperty("httpclient.proxy-host", null);
            int proxyPort = this.jets3tProperties.getIntProperty("httpclient.proxy-port", -1);
            String proxyUser = this.jets3tProperties.getStringProperty("httpclient.proxy-user", null);
            String proxyPassword = this.jets3tProperties.getStringProperty("httpclient.proxy-password", null);
            String proxyDomain = this.jets3tProperties.getStringProperty("httpclient.proxy-domain", null);
            RestUtils.initHttpProxy(this.httpClient, this.jets3tProperties,
                    proxyHostAddress, proxyPort, proxyUser, proxyPassword, proxyDomain);
        }
    }

    /**
     * Constructs the service with default properties.
     *
     * @param credentials the Storage Provider user credentials to use when communicating with CloudFront
     */
    public CloudFrontService(ProviderCredentials credentials) {
        this(credentials, null, null, null);
    }

    /**
     * @return the Storage Provider Credentials identifying the AWS user.
     */
    public ProviderCredentials getAWSCredentials() {
        return credentials;
    }

    /**
     * Returns the current date and time, adjusted according to the time
     * offset between your computer and an AWS server (as set by the
     * {@link RestUtils#calculateTimeAdjustmentOffset(HttpResponse)} method).
     *
     * @return the current time, or the current time adjusted to match the AWS time
     *         if the service has experienced a RequestExpired error.
     */
    protected Date getCurrentTimeWithOffset() {
        return new Date(System.currentTimeMillis() + timeOffset);
    }

    /**
     * Sign the given HTTP method object using the AWS credentials provided
     * by {@link #getAWSCredentials()}.
     *
     * @param httpMethod the request object
     * @param context
     * @throws ServiceException
     */
    public void authorizeHttpRequest(HttpUriRequest httpMethod, HttpContext context) throws ServiceException {
        String date = ServiceUtils.formatRfc822Date(getCurrentTimeWithOffset());

        // Set/update the date timestamp to the current time
        // Note that this will be over-ridden if an "x-amz-date" header is present.
        httpMethod.setHeader("Date", date);

        // Sign the date to authenticate the request.
        // Sign the canonical string.
        String signature = ServiceUtils.signWithHmacSha1(
                getAWSCredentials().getSecretKey(), date);

        // Add encoded authorization to connection as HTTP Authorization header.
        String authorizationString = "AWS " + getAWSCredentials().getAccessKey() + ":" + signature;
        httpMethod.setHeader("Authorization", authorizationString);
    }

    public void authorizeHttpRequest(
        HttpUriRequest httpMethod, HttpContext context, String forceRequestSignatureVersion)
        throws ServiceException
    {
        authorizeHttpRequest(httpMethod, context);
    }

    /**
     * Performs an HTTP/S request by invoking the provided HttpMethod object. If the HTTP
     * response code doesn't match the expected value, an exception is thrown.
     *
     * @param httpMethod           the object containing a request target and all other information necessary to
     *                             perform the request
     * @param expectedResponseCode the HTTP response code that indicates a successful request. If the response code received
     *                             does not match this value an error must have occurred, so an exception is thrown.
     * @throws CloudFrontServiceException all exceptions are wrapped in a CloudFrontServiceException. Depending on the kind of error that
     *                                    occurred, this exception may contain additional error information available from an XML
     *                                    error response document.
     */
    protected HttpResponse performRestRequest(HttpRequestBase httpMethod, int expectedResponseCode)
            throws CloudFrontServiceException {
        // Set mandatory Request headers.
        if(httpMethod.getFirstHeader("Date") == null) {
            httpMethod.setHeader("Date", ServiceUtils.formatRfc822Date(
                    getCurrentTimeWithOffset()));
        }

        HttpResponse response = null;
        boolean completedWithoutRecoverableError;
        int internalErrorCount = 0;

        try {
            do {
                completedWithoutRecoverableError = true;
                authorizeHttpRequest(httpMethod, null);
                response = httpClient.execute(httpMethod);
                int responseCode = response.getStatusLine().getStatusCode();

                if(responseCode != expectedResponseCode) {
                    if(responseCode == 500) {
                        // Retry on Internal Server errors, up to the defined limit.
                        long delayMs = 1000;
                        if(++internalErrorCount < this.internalErrorRetryMax) {
                            log.warn("Encountered " + internalErrorCount +
                                    " CloudFront Internal Server error(s), will retry in " + delayMs + "ms");
                            Thread.sleep(delayMs);
                            completedWithoutRecoverableError = false;
                        }
                        else {
                            throw new CloudFrontServiceException("Encountered too many CloudFront Internal Server errors ("
                                    + internalErrorCount + "), aborting request.");
                        }
                    }
                    else {
                        // Parse XML error message.
                        ErrorHandler handler = new CloudFrontXmlResponsesSaxParser(
                                this.jets3tProperties).parseErrorResponse(
                                response.getEntity().getContent());

                        CloudFrontServiceException exception = new CloudFrontServiceException(
                                "Request failed with CloudFront Service error",
                                responseCode, handler.getType(), handler.getCode(),
                                handler.getMessage(), handler.getDetail(),
                                handler.getRequestId());

                        if("RequestExpired".equals(exception.getErrorCode())) {
                            // Retry on time skew errors.
                            this.timeOffset = RestUtils.calculateTimeAdjustmentOffset(response);
                            if(log.isWarnEnabled()) {
                                log.warn("Adjusted time offset in response to RequestTimeTooSkewed error. "
                                        + "Local machine and service disagree on the time by approximately "
                                        + (this.timeOffset / 1000) + " seconds, please fix your system's time."
                                        + " Retrying connection.");
                            }
                            completedWithoutRecoverableError = false;
                        }
                        else {
                            throw exception;
                        }
                    }
                } // End responseCode check
            }
            while(!completedWithoutRecoverableError);
        }
        catch(CloudFrontServiceException e) {
            releaseConnection(response);
            throw e;
        }
        catch(Exception t) {
            releaseConnection(response);
            throw new CloudFrontServiceException("CloudFront Request failed", t);
        }
        return response;
    }

    private void releaseConnection(HttpResponse pResponse) {
        if(pResponse == null) {
            return;
        }
        try {
            EntityUtils.consume(pResponse.getEntity());
        }
        catch(Exception e) {
            //ignore
        }
    }

    /**
     * List streaming or non-streaming Distributions in a CloudFront account.
     *
     * @param isStreaming Only return streaming distributions
     * @param pagingSize  the maximum number of distributions the CloudFront service will
     *                    return in each response message.
     * @return A list of {@link Distribution}s.
     * @throws CloudFrontServiceException exception
     */
    protected List<Distribution> listDistributionsImpl(boolean isStreaming, int pagingSize)
            throws CloudFrontServiceException {
        if(log.isDebugEnabled()) {
            log.debug("Listing "
                    + (isStreaming ? "streaming" : "")
                    + " distributions for AWS user: " + getAWSCredentials().getAccessKey());
        }
        try {
            List<Distribution> distributions = new ArrayList<Distribution>();
            String nextMarker = null;
            boolean incompleteListing;
            do {
                String uri = ENDPOINT + VERSION
                        + (isStreaming ? "/streaming-distribution" : "/distribution")
                        + "?MaxItems=" + pagingSize;
                if(nextMarker != null) {
                    uri += "&Marker=" + nextMarker;
                }
                HttpRequestBase httpMethod = new HttpGet(uri);
                HttpResponse response = performRestRequest(httpMethod, 200);

                DistributionListHandler handler =
                        (new CloudFrontXmlResponsesSaxParser(this.jets3tProperties))
                                .parseDistributionListResponse(response.getEntity().getContent());
                distributions.addAll(handler.getDistributions());

                incompleteListing = handler.isTruncated();
                nextMarker = handler.getNextMarker();

                // Sanity check for valid pagination values.
                if(incompleteListing && nextMarker == null) {
                    throw new CloudFrontServiceException("Unable to retrieve paginated "
                            + "DistributionList results without a valid NextMarker value.");
                }
            }
            while(incompleteListing);

            return distributions;
        }
        catch(CloudFrontServiceException e) {
            throw e;
        }
        catch(RuntimeException e) {
            throw e;
        }
        catch(Exception e) {
            throw new CloudFrontServiceException(e);
        }
    }

    /**
     * List all your standard CloudFront distributions, with a given maximum
     * number of Distribution items in each "page" of results.
     *
     * @param pagingSize the maximum number of distributions the CloudFront service will
     *                   return in each response message.
     * @return a list of your distributions.
     * @throws CloudFrontServiceException exception
     */
    public Distribution[] listDistributions(int pagingSize) throws CloudFrontServiceException {
        List<Distribution> distributions = listDistributionsImpl(false, pagingSize);
        return distributions.toArray(new Distribution[distributions.size()]);
    }

    /**
     * List all your streaming CloudFront distributions, with a given maximum
     * number of Distribution items in each "page" of results.
     *
     * @param pagingSize the maximum number of distributions the CloudFront service will
     *                   return in each response message.
     * @return a list of your distributions.
     * @throws CloudFrontServiceException exception
     */
    public StreamingDistribution[] listStreamingDistributions(int pagingSize)
            throws CloudFrontServiceException {
        List<Distribution> distributions = listDistributionsImpl(true, pagingSize);
        return distributions.toArray(
                new StreamingDistribution[distributions.size()]);
    }

    /**
     * List all your standard CloudFront distributions.
     *
     * @return a list of your distributions.
     * @throws CloudFrontServiceException exception
     */
    public Distribution[] listDistributions() throws CloudFrontServiceException {
        return listDistributions(100);
    }

    /**
     * List all your streaming CloudFront distributions.
     *
     * @return a list of your streaming distributions.
     * @throws CloudFrontServiceException exception
     */
    public StreamingDistribution[] listStreamingDistributions() throws CloudFrontServiceException {
        return listStreamingDistributions(100);
    }

    /**
     * List streaming or non-stream distributions whose origin is the given S3 bucket name.
     *
     * @param isStreaming List streaming distributions
     * @param bucketName  the name of the S3 bucket whose distributions will be returned.
     * @return a list of distributions applied to the given S3 bucket, or an empty list
     *         if there are no such distributions.
     * @throws CloudFrontServiceException exception
     */
    public List<Distribution> listDistributionsByBucketName(boolean isStreaming, String bucketName)
            throws CloudFrontServiceException {
        String s3Endpoint = this.jets3tProperties.getStringProperty(
                "s3service.s3-endpoint", Constants.S3_DEFAULT_HOSTNAME);
        if(log.isDebugEnabled()) {
            log.debug("Listing "
                    + (isStreaming ? "streaming" : "")
                    + " distributions for the S3 bucket '" + bucketName
                    + "' for AWS user: " + getAWSCredentials().getAccessKey());
        }
        ArrayList<Distribution> bucketDistributions = new ArrayList<Distribution>();
        Distribution[] allDistributions =
                (isStreaming ? listStreamingDistributions() : listDistributions());
        for(Distribution distribution : allDistributions) {
            for(Origin origin : distribution.getConfig().getOrigins()) {
                if(!(origin instanceof S3Origin)) {
                    continue;
                }
                if(origin.getDomainName().equals(bucketName)
                        || bucketName.equals(ServiceUtils.findBucketNameInHostname(origin.getDomainName(), s3Endpoint))) {
                    bucketDistributions.add(distribution);
                    break;
                }
            }
        }
        return bucketDistributions;
    }

    /**
     * List the distributions whose origin is the given S3 bucket name.
     *
     * @param bucketName the name of the S3 bucket whose distributions will be returned.
     * @return a list of distributions applied to the given S3 bucket, or an empty list
     *         if there are no such distributions.
     * @throws CloudFrontServiceException exception
     */
    public Distribution[] listDistributions(String bucketName) throws CloudFrontServiceException {
        List<Distribution> bucketDistributions = listDistributionsByBucketName(false, bucketName);
        return bucketDistributions.toArray(
                new Distribution[bucketDistributions.size()]);
    }

    /**
     * List the streaming distributions whose origin is the given S3 bucket name.
     *
     * @param bucketName the name of the S3 bucket whose distributions will be returned.
     * @return a list of distributions applied to the given S3 bucket, or an empty list
     *         if there are no such distributions.
     * @throws CloudFrontServiceException exception
     */
    public StreamingDistribution[] listStreamingDistributions(String bucketName)
            throws CloudFrontServiceException {
        List<Distribution> streamingDistributions = listDistributionsByBucketName(true, bucketName);
        return streamingDistributions.toArray(
                new StreamingDistribution[streamingDistributions.size()]);
    }

    /**
     * Generate XML representing an S3 or non-S3 (custom) origin.
     *
     * @param origin S3 or non-S3 (custom) origin.
     * @return XML document representing an origin
     * @throws TransformerException
     * @throws ParserConfigurationException
     * @throws FactoryConfigurationError
     */
    protected XMLBuilder buildOrigin(Origin origin) throws TransformerException,
            ParserConfigurationException, FactoryConfigurationError {
        XMLBuilder builder = XMLBuilder.create("Origin");
        if(origin.getId() != null) {
            builder.e("Id").t(origin.getId());
        }
        else {
            builder.e("Id").t("default-origin-id");
        }
        if(origin instanceof S3Origin) {
            builder.e("DomainName").t(sanitizeS3BucketName(origin.getDomainName()));
            S3Origin o = (S3Origin) origin;
            XMLBuilder oaiBuilder = builder
                    .e("S3OriginConfig")
                    .e("OriginAccessIdentity");
            if(o.getOriginAccessIdentity() != null) {
                oaiBuilder.t(o.getOriginAccessIdentity());
            }
        }
        else {
            CustomOrigin o = (CustomOrigin) origin;
            builder.e("DomainName").t(origin.getDomainName());
            builder.e("CustomOriginConfig")
                    .e("HTTPPort").t(String.valueOf(o.getHttpPort())).up()
                    .e("HTTPSPort").t(String.valueOf(o.getHttpsPort())).up()
                    .e("OriginProtocolPolicy").t(o.getOriginProtocolPolicy().toText());
        }
        return builder;
    }

    protected XMLBuilder buildTrustedSigners(String[] trustedSignerAwsAccountNumbers) throws ParserConfigurationException {
        XMLBuilder trustedSignersBuilder = XMLBuilder.create("TrustedSigners");
        if(trustedSignerAwsAccountNumbers == null
                || trustedSignerAwsAccountNumbers.length == 0) {
            trustedSignersBuilder
                    .e("Enabled").t(String.valueOf(false)).up()
                    .e("Quantity").t(String.valueOf(0));
        }
        else {
            XMLBuilder itemsBuilder = trustedSignersBuilder
                    .e("Enabled").t(String.valueOf(true)).up()
                    .e("Quantity").t(String.valueOf(trustedSignerAwsAccountNumbers.length)).up()
                    .e("Items");
            for(String awsAccountNumber : trustedSignerAwsAccountNumbers) {
                itemsBuilder.e("AwsAccountNumber").t(awsAccountNumber);
            }
        }
        return trustedSignersBuilder;
    }

    protected XMLBuilder buildDefaultCacheBehavior(CacheBehavior cb)
            throws TransformerException, ParserConfigurationException, FactoryConfigurationError {
        return this.buildCacheBehaviorsElement(true, new CacheBehavior[]{cb});
    }

    protected XMLBuilder buildCacheBehaviors(CacheBehavior[] cbs)
            throws TransformerException, ParserConfigurationException, FactoryConfigurationError {
        return this.buildCacheBehaviorsElement(false, cbs);
    }

    protected XMLBuilder buildCacheBehaviorsElement(boolean isDefault, CacheBehavior[] cbs)
            throws TransformerException, ParserConfigurationException, FactoryConfigurationError {
        XMLBuilder builder;
        if(isDefault) {
            builder = XMLBuilder.create("DefaultCacheBehavior");
        }
        else {
            builder = XMLBuilder.create("CacheBehaviors")
                    .e("Quantity").t(String.valueOf(cbs.length)).up();
        }
        if(!isDefault && cbs.length > 0) {
            builder = builder.e("Items");
        }
        for(CacheBehavior cb : cbs) {
            XMLBuilder itemBuilder;
            if(isDefault) {
                itemBuilder = builder;
            }
            else {
                itemBuilder = builder.e("CacheBehavior");
                itemBuilder.e("PathPattern").t(cb.getPathPattern());
            }

            if(cb.getTargetOriginId() != null) {
                itemBuilder.e("TargetOriginId").t(cb.getTargetOriginId());
            }
            else {
                itemBuilder.e("TargetOriginId").t("default-origin-id");
            }
            itemBuilder.e("ForwardedValues").e("QueryString").t(String.valueOf(cb.isForwardQueryString()));
            itemBuilder.importXMLBuilder(buildTrustedSigners(cb.getTrustedSignerAwsAccountNumbers()));
            itemBuilder.e("ViewerProtocolPolicy").t(cb.getViewerProtocolPolicy().toText());
            if(cb.getMinTTL() != null) {
                itemBuilder.e("MinTTL").t(String.valueOf(cb.getMinTTL()));
            }
            else {
                itemBuilder.e("MinTTL").t(String.valueOf(0));
            }
        }

        return builder;
    }


    /**
     * Generate a DistributionConfig or StreamingDistributionConfig XML document.
     *
     * @return XML document representing a Distribution Configuration
     * @throws TransformerException
     * @throws ParserConfigurationException
     * @throws FactoryConfigurationError
     */
    protected String buildDistributionConfigXmlDocument(DistributionConfig config)
            throws TransformerException, ParserConfigurationException, FactoryConfigurationError {
        XMLBuilder builder = XMLBuilder.create(config.isStreamingDistributionConfig()
                ? "StreamingDistributionConfig"
                : "DistributionConfig")
                .a("xmlns", XML_NAMESPACE);

        builder.e("CallerReference").t(config.getCallerReference() == null ? String.valueOf(System.currentTimeMillis()) : config.getCallerReference());

        XMLBuilder aliasesBuilder = builder.e("Aliases");
        if(config.getCNAMEs() != null && config.getCNAMEs().length > 0) {
            aliasesBuilder.e("Quantity").t(String.valueOf(config.getCNAMEs().length));
            XMLBuilder items = aliasesBuilder.e("Items");
            for(String cname : config.getCNAMEs()) {
                items.e("CNAME").t(cname);
            }
        }
        else {
            aliasesBuilder.e("Quantity").t(String.valueOf("0"));
        }

        if(config.getDefaultRootObject() != null) {
            builder.e("DefaultRootObject").t(config.getDefaultRootObject());
        }
        else {
            builder.e("DefaultRootObject");
        }

        if(config.isStreamingDistributionConfig()) {
            if(config.getOrigins().length == 1) {
                final S3Origin bucket = (S3Origin) config.getOrigins()[0];
                final XMLBuilder origin = builder.e("S3Origin");
                origin.e("DomainName").t(sanitizeS3BucketName(bucket.getDomainName()));
                origin.e("OriginAccessIdentity");
                if(bucket.getOriginAccessIdentity() != null) {
                    origin.t(bucket.getOriginAccessIdentity());
                }
            }
        }
        else {
            XMLBuilder originsBuilder = builder
                    .e("Origins")
                    .e("Quantity").t(String.valueOf(config.getOrigins().length)).up()
                    .e("Items");
            for(Origin origin : config.getOrigins()) {
                originsBuilder.importXMLBuilder(buildOrigin(origin));
            }
        }

        if(config.isStreamingDistributionConfig()) {
            builder.importXMLBuilder(buildTrustedSigners(config.getTrustedSignerAwsAccountNumbers()));
        }
        else {
            builder.importXMLBuilder(buildDefaultCacheBehavior(config.getDefaultCacheBehavior()));
            builder.importXMLBuilder(buildCacheBehaviors(config.getCacheBehaviors()));
        }

        builder.e("Comment").t(null == config.getComment() ? "" : config.getComment());

        if(config.getLoggingStatus() != null) {
            builder.e("Logging")
                    .e("Enabled").t(String.valueOf(true)).up()
                    .e("Bucket").t(config.getLoggingStatus().getBucket()).up()
                    .e("Prefix").t(config.getLoggingStatus().getPrefix());
        }
        else {
            builder.e("Logging")
                    .e("Enabled").t(String.valueOf(false)).up()
                    .e("Bucket").up()
                    .e("Prefix");
        }

        builder.e("Enabled").t(String.valueOf(config.isEnabled()));

        return builder.asString(null);
    }

    /**
     * Create a streaming or non-streaming distribution.
     *
     * @param config Configuration document
     * @return Information about the newly-created distribution.
     * @throws CloudFrontServiceException exception
     */
    protected Distribution createDistributionImpl(DistributionConfig config)
            throws CloudFrontServiceException {
        if(log.isDebugEnabled()) {
            log.debug("Creating "
                    + (config.isStreamingDistributionConfig() ? "streaming" : "")
                    + " distribution for origins: " + Arrays.asList(config.getOrigins()));
        }

        HttpPost httpMethod = new HttpPost(ENDPOINT + VERSION
                + (config.isStreamingDistributionConfig()
                ? "/streaming-distribution"
                : "/distribution"));

        try {
            String distributionConfigXml = buildDistributionConfigXmlDocument(config);

            httpMethod.setEntity(new StringEntity(
                    distributionConfigXml,
                    ContentType.create("text/xml", Constants.DEFAULT_ENCODING)));

            HttpResponse response = performRestRequest(httpMethod, 201);

            DistributionHandler handler =
                    (new CloudFrontXmlResponsesSaxParser(this.jets3tProperties))
                            .parseDistributionResponse(response.getEntity().getContent());

            return handler.getDistribution();
        }
        catch(CloudFrontServiceException e) {
            throw e;
        }
        catch(RuntimeException e) {
            throw e;
        }
        catch(Exception e) {
            throw new CloudFrontServiceException(e);
        }
    }

    /**
     * Create a public or private CloudFront distribution for an S3 bucket.
     *
     * @param origin                         the origin to associate with the distribution, either an Amazon S3 bucket or
     *                                       a custom HTTP/S-accessible location.
     * @param callerReference                A user-set unique reference value that ensures the request can't be replayed
     *                                       (max UTF-8 encoding size 128 bytes). This parameter may be null, in which
     *                                       case your computer's local epoch time in milliseconds will be used.
     * @param cnames                         A list of up to 10 CNAME aliases to associate with the distribution. This
     *                                       parameter may be a null or empty array.
     * @param comment                        An optional comment to describe the distribution in your own terms
     *                                       (max 128 characters). May be null.
     * @param enabled                        Should the distribution should be enabled and publicly accessible upon creation?
     * @param loggingStatus                  Logging status settings (bucket, prefix) for the distribution. If this value
     *                                       is null, logging will be disabled for the distribution.
     * @param trustedSignerSelf              If true the owner of the distribution (you) will be be allowed to generate
     *                                       signed URLs for a private distribution. Note: If either trustedSignerSelf or
     *                                       trustedSignerAwsAccountNumbers parameters are provided the private distribution
     *                                       will require signed URLs to access content.
     * @param trustedSignerAwsAccountNumbers Account Number identifiers for AWS account holders other than the
     *                                       distribution's owner who will be allowed to generate signed URLs for a private
     *                                       distribution. If null or empty, no additional AWS account holders may generate
     *                                       signed URLs. Note: If either trustedSignerSelf or
     *                                       trustedSignerAwsAccountNumbers parameters are provided the private distribution
     *                                       will require signed URLs to access content.
     * @param requiredProtocols              List of protocols that must be used by clients to retrieve content from the
     *                                       distribution. If this value is null or is an empty array, all protocols will be
     *                                       supported.
     * @param defaultRootObject              The name of an object that will be served when someone visits the root of a
     *                                       distribution.
     * @return an object that describes the newly-created distribution, in particular the
     *         distribution's identifier and domain name values.
     * @throws CloudFrontServiceException exception
     */
    public Distribution createDistribution(Origin origin, String callerReference,
                                           String[] cnames, String comment, boolean enabled, LoggingStatus loggingStatus,
                                           boolean trustedSignerSelf, String[] trustedSignerAwsAccountNumbers,
                                           String[] requiredProtocols, String defaultRootObject)
            throws CloudFrontServiceException {
        return this.createDistribution(
                origin, callerReference, cnames, comment, enabled, loggingStatus,
                trustedSignerSelf, trustedSignerAwsAccountNumbers, requiredProtocols, defaultRootObject,
                null // minTTL
        );
    }

    /**
     * Create a public or private CloudFront distribution for an S3 bucket.
     *
     * @param origin                         the origin to associate with the distribution, either an Amazon S3 bucket or
     *                                       a custom HTTP/S-accessible location.
     * @param callerReference                A user-set unique reference value that ensures the request can't be replayed
     *                                       (max UTF-8 encoding size 128 bytes). This parameter may be null, in which
     *                                       case your computer's local epoch time in milliseconds will be used.
     * @param cnames                         A list of up to 10 CNAME aliases to associate with the distribution. This
     *                                       parameter may be a null or empty array.
     * @param comment                        An optional comment to describe the distribution in your own terms
     *                                       (max 128 characters). May be null.
     * @param enabled                        Should the distribution should be enabled and publicly accessible upon creation?
     * @param loggingStatus                  Logging status settings (bucket, prefix) for the distribution. If this value
     *                                       is null, logging will be disabled for the distribution.
     * @param trustedSignerSelf              If true the owner of the distribution (you) will be be allowed to generate
     *                                       signed URLs for a private distribution. Note: If either trustedSignerSelf or
     *                                       trustedSignerAwsAccountNumbers parameters are provided the private distribution
     *                                       will require signed URLs to access content.
     * @param trustedSignerAwsAccountNumbers Account Number identifiers for AWS account holders other than the
     *                                       distribution's owner who will be allowed to generate signed URLs for a private
     *                                       distribution. If null or empty, no additional AWS account holders may generate
     *                                       signed URLs. Note: If either trustedSignerSelf or
     *                                       trustedSignerAwsAccountNumbers parameters are provided the private distribution
     *                                       will require signed URLs to access content.
     * @param requiredProtocols              List of protocols that must be used by clients to retrieve content from the
     *                                       distribution. If this value is null or is an empty array, all protocols will be
     *                                       supported.
     * @param defaultRootObject              The name of an object that will be served when someone visits the root of a
     *                                       distribution.
     * @param minTTL                         The time to live (TTL) to apply to objects served by this distribution.
     * @return an object that describes the newly-created distribution, in particular the
     *         distribution's identifier and domain name values.
     * @throws CloudFrontServiceException
     * @deprecated as of 2012-05-05 API version, use {@link #createDistribution(DistributionConfig)}.
     */
    @Deprecated
    public Distribution createDistribution(Origin origin, String callerReference,
                                           String[] cnames, String comment, boolean enabled, LoggingStatus loggingStatus,
                                           boolean trustedSignerSelf, String[] trustedSignerAwsAccountNumbers,
                                           String[] requiredProtocols, String defaultRootObject, Long minTTL)
            throws CloudFrontServiceException {
        DistributionConfig config = new DistributionConfig(
                origin, callerReference, cnames, comment, enabled, loggingStatus,
                trustedSignerSelf, trustedSignerAwsAccountNumbers, requiredProtocols,
                defaultRootObject, minTTL);
        return createDistributionImpl(config);
    }

    /**
     * Create a minimally-configured CloudFront distribution for an S3 bucket that will
     * be publicly available once created.
     *
     * @param origin the origin to associate with the distribution, either an Amazon S3 bucket or
     *               a custom HTTP/S-accessible location.
     * @return an object that describes the newly-created distribution, in particular the
     *         distribution's identifier and domain name values.
     * @throws CloudFrontServiceException
     */
    public Distribution createDistribution(Origin origin) throws CloudFrontServiceException {
        return this.createDistribution(origin, null, null, null, true, null);
    }

    /**
     * Create a public CloudFront distribution for an S3 bucket.
     *
     * @param origin          the origin to associate with the distribution, either an Amazon S3 bucket or
     *                        a custom HTTP/S-accessible location.
     * @param callerReference A user-set unique reference value that ensures the request can't be replayed
     *                        (max UTF-8 encoding size 128 bytes). This parameter may be null, in which
     *                        case your computer's local epoch time in milliseconds will be used.
     * @param cnames          A list of up to 10 CNAME aliases to associate with the distribution. This
     *                        parameter may be a null or empty array.
     * @param comment         An optional comment to describe the distribution in your own terms
     *                        (max 128 characters). May be null.
     * @param enabled         Should the distribution should be enabled and publicly accessible upon creation?
     * @param loggingStatus   Logging status settings (bucket, prefix) for the distribution. If this value
     *                        is null, logging will be disabled for the distribution.
     * @return an object that describes the newly-created distribution, in particular the
     *         distribution's identifier and domain name values.
     * @throws CloudFrontServiceException
     */
    public Distribution createDistribution(Origin origin, String callerReference,
                                           String[] cnames, String comment, boolean enabled, LoggingStatus loggingStatus)
            throws CloudFrontServiceException {
        return createDistribution(origin, callerReference, cnames, comment, enabled,
                loggingStatus, false, null, null, null);
    }

    /**
     * Create a public or private CloudFront distribution for an S3 bucket based
     * on a pre-configured {@link DistributionConfig}.
     *
     * @param config Configuration settings to apply to the distribution.
     * @return an object that describes the newly-created distribution, in particular the
     *         distribution's identifier and domain name values.
     * @throws CloudFrontServiceException
     */
    public Distribution createDistribution(DistributionConfig config)
            throws CloudFrontServiceException {
        return createDistributionImpl(config);
    }

    /**
     * Create a public or private streaming CloudFront distribution for an S3 bucket.
     *
     * @param origin                         the origin to associate with the distribution, either an Amazon S3 bucket or
     *                                       a custom HTTP/S-accessible location.
     * @param callerReference                A user-set unique reference value that ensures the request can't be replayed
     *                                       (max UTF-8 encoding size 128 bytes). This parameter may be null, in which
     *                                       case your computer's local epoch time in milliseconds will be used.
     * @param cnames                         A list of up to 10 CNAME aliases to associate with the distribution. This
     *                                       parameter may be a null or empty array.
     * @param comment                        An optional comment to describe the distribution in your own terms
     *                                       (max 128 characters). May be null.
     * @param enabled                        Should the distribution should be enabled and publicly accessible upon creation?
     * @param loggingStatus                  Logging status settings (bucket, prefix) for the distribution. If this value
     *                                       is null, logging will be disabled for the distribution.
     * @param trustedSignerSelf              If true the owner of the distribution (you) will be be allowed to generate
     *                                       signed URLs for a private distribution. Note: If either trustedSignerSelf or
     *                                       trustedSignerAwsAccountNumbers parameters are provided the private distribution
     *                                       will require signed URLs to access content.
     * @param trustedSignerAwsAccountNumbers Account Number identifiers for AWS account holders other than the
     *                                       distribution's owner who will be allowed to generate signed URLs for a private
     *                                       distribution. If null or empty, no additional AWS account holders may generate
     *                                       signed URLs. Note: If either trustedSignerSelf or
     *                                       trustedSignerAwsAccountNumbers parameters are provided the private distribution
     *                                       will require signed URLs to access content.
     * @return an object that describes the newly-created distribution, in particular the
     *         distribution's identifier and domain name values.
     * @throws CloudFrontServiceException
     * @deprecated as of 2012-05-05 API version, use {@link #createDistribution(DistributionConfig)}.
     */
    @Deprecated
    public StreamingDistribution createStreamingDistribution(Origin origin, String callerReference,
                                                             String[] cnames, String comment, boolean enabled, LoggingStatus loggingStatus,
                                                             boolean trustedSignerSelf, String[] trustedSignerAwsAccountNumbers)
            throws CloudFrontServiceException {
        StreamingDistributionConfig config = new StreamingDistributionConfig(
                origin, callerReference, cnames, comment, enabled, loggingStatus,
                trustedSignerSelf, trustedSignerAwsAccountNumbers, null);
        return (StreamingDistribution) createDistributionImpl(config);
    }

    /**
     * Create a public streaming CloudFront distribution for an S3 bucket.
     *
     * @param origin          the origin to associate with the distribution, either an Amazon S3 bucket or
     *                        a custom HTTP/S-accessible location.
     * @param callerReference A user-set unique reference value that ensures the request can't be replayed
     *                        (max UTF-8 encoding size 128 bytes). This parameter may be null, in which
     *                        case your computer's local epoch time in milliseconds will be used.
     * @param cnames          A list of up to 10 CNAME aliases to associate with the distribution. This
     *                        parameter may be a null or empty array.
     * @param comment         An optional comment to describe the distribution in your own terms
     *                        (max 128 characters). May be null.
     * @param enabled         Should the distribution should be enabled and publicly accessible upon creation?
     * @param loggingStatus   Logging status settings (bucket, prefix) for the distribution. If this value
     *                        is null, logging will be disabled for the distribution.
     * @return an object that describes the newly-created distribution, in particular the
     *         distribution's identifier and domain name values.
     * @throws CloudFrontServiceException
     * @deprecated as of 2012-05-05 API version, use {@link #createDistribution(DistributionConfig)}.
     */
    @Deprecated
    public StreamingDistribution createStreamingDistribution(Origin origin, String callerReference,
                                                             String[] cnames, String comment, boolean enabled, LoggingStatus loggingStatus)
            throws CloudFrontServiceException {
        StreamingDistributionConfig config = new StreamingDistributionConfig(
                origin, callerReference, cnames, comment, enabled, loggingStatus);
        return (StreamingDistribution) createDistributionImpl(config);
    }

    /**
     * @param isStreaming    Only return streaming distributions
     * @param distributionId The distribution's unique identifier.
     * @return Information about a streaming or non-streaming distribution.
     * @throws CloudFrontServiceException
     */
    protected Distribution getDistributionInfoImpl(boolean isStreaming, String distributionId)
            throws CloudFrontServiceException {
        if(log.isDebugEnabled()) {
            log.debug("Getting information for "
                    + (isStreaming ? "streaming" : "")
                    + " distribution with id: " + distributionId);
        }
        HttpGet httpMethod = new HttpGet(ENDPOINT + VERSION
                + (isStreaming ? "/streaming-distribution/" : "/distribution/")
                + distributionId);

        try {
            HttpResponse response = performRestRequest(httpMethod, 200);
            DistributionHandler handler =
                    (new CloudFrontXmlResponsesSaxParser(this.jets3tProperties))
                            .parseDistributionResponse(response.getEntity().getContent());

            return handler.getDistribution();
        }
        catch(CloudFrontServiceException e) {
            throw e;
        }
        catch(RuntimeException e) {
            throw e;
        }
        catch(Exception e) {
            throw new CloudFrontServiceException(e);
        }
    }

    /**
     * Lookup information for a standard distribution.
     *
     * @param id the distribution's unique identifier.
     * @return an object that describes the distribution, including its identifier and domain
     *         name values as well as its configuration details.
     * @throws CloudFrontServiceException
     */
    public Distribution getDistributionInfo(String id) throws CloudFrontServiceException {
        return getDistributionInfoImpl(false, id);
    }

    /**
     * Lookup information for a streaming distribution.
     *
     * @param distributionId the distribution's unique identifier.
     * @return an object that describes the distribution, including its identifier and domain
     *         name values as well as its configuration details.
     * @throws CloudFrontServiceException
     */
    public StreamingDistribution getStreamingDistributionInfo(String distributionId)
            throws CloudFrontServiceException {
        return (StreamingDistribution) getDistributionInfoImpl(true, distributionId);
    }

    /**
     * @param isStreaming    Only return streaming distributions
     * @param distributionId The distribution's unique identifier.
     * @return Information about a streaming or non-streaming distribution configuration.
     * @throws CloudFrontServiceException
     */
    protected DistributionConfig getDistributionConfigImpl(boolean isStreaming, String distributionId)
            throws CloudFrontServiceException {
        if(log.isDebugEnabled()) {
            log.debug("Getting configuration for "
                    + (isStreaming ? "streaming" : "")
                    + " distribution with id: " + distributionId);
        }
        HttpGet httpMethod = new HttpGet(ENDPOINT + VERSION
                + (isStreaming ? "/streaming-distribution/" : "/distribution/")
                + distributionId + "/config");

        try {
            HttpResponse response = performRestRequest(httpMethod, 200);
            DistributionConfigHandler handler =
                    (new CloudFrontXmlResponsesSaxParser(this.jets3tProperties))
                            .parseDistributionConfigResponse(response.getEntity().getContent());

            DistributionConfig config = handler.getDistributionConfig();
            config.setEtag(response.getFirstHeader("ETag").getValue());
            return config;
        }
        catch(CloudFrontServiceException e) {
            throw e;
        }
        catch(RuntimeException e) {
            throw e;
        }
        catch(Exception e) {
            throw new CloudFrontServiceException(e);
        }
    }

    /**
     * Lookup configuration information for a standard distribution. The configuration
     * information is a subset of the information available from the
     * {@link #getDistributionInfo(String)} method.
     *
     * @param distributionId the distribution's unique identifier.
     * @return an object that describes the distribution's configuration, including its origin bucket
     *         and CNAME aliases.
     * @throws CloudFrontServiceException
     */
    public DistributionConfig getDistributionConfig(String distributionId)
            throws CloudFrontServiceException {
        return getDistributionConfigImpl(false, distributionId);
    }

    /**
     * Lookup configuration information for a streaming distribution. The configuration
     * information is a subset of the information available from the
     * {@link #getDistributionInfo(String)} method.
     *
     * @param id the distribution's unique identifier.
     * @return an object that describes the distribution's configuration, including its origin bucket
     *         and CNAME aliases.
     * @throws CloudFrontServiceException
     */
    public StreamingDistributionConfig getStreamingDistributionConfig(String id)
            throws CloudFrontServiceException {
        return (StreamingDistributionConfig) getDistributionConfigImpl(true, id);
    }

    /**
     * Update a streaming or non-streaming distribution.
     *
     * @param config Configuration properties to apply to the distribution.
     * @return Information about the updated distribution configuration.
     * @throws CloudFrontServiceException
     */
    protected DistributionConfig updateDistributionConfigImpl(String id, DistributionConfig config)
            throws CloudFrontServiceException {
        if(log.isDebugEnabled()) {
            log.debug("Updating configuration of "
                    + (config.isStreamingDistributionConfig() ? "streaming" : "")
                    + "distribution with id: " + id);
        }

        String etag = config.getEtag();
        if(null == etag) {
            // Retrieve the old configuration.
            DistributionConfig oldConfig = (config.isStreamingDistributionConfig()
                    ? getStreamingDistributionConfig(id)
                    : getDistributionConfig(id));
            etag = oldConfig.getEtag();
        }

        HttpPut httpMethod = new HttpPut(ENDPOINT + VERSION
                + (config.isStreamingDistributionConfig()
                ? "/streaming-distribution/"
                : "/distribution/")
                + id + "/config");

        try {
            String distributionConfigXml = buildDistributionConfigXmlDocument(config);

            httpMethod.setEntity(new StringEntity(
                    distributionConfigXml,
                    ContentType.create("text/xml", Constants.DEFAULT_ENCODING)));
            httpMethod.setHeader("If-Match", etag);
            HttpResponse response = performRestRequest(httpMethod, 200);

            DistributionConfigHandler handler =
                    (new CloudFrontXmlResponsesSaxParser(this.jets3tProperties))
                            .parseDistributionConfigResponse(response.getEntity().getContent());

            DistributionConfig resultConfig = handler.getDistributionConfig();
            resultConfig.setEtag(response.getFirstHeader("ETag").getValue());
            return resultConfig;
        }
        catch(CloudFrontServiceException e) {
            throw e;
        }
        catch(RuntimeException e) {
            throw e;
        }
        catch(Exception e) {
            throw new CloudFrontServiceException(e);
        }
    }

    /**
     * Update the configuration of an existing distribution to change its properties
     * or public/private status. The new configuration properties provided
     * <strong>replace</strong> any existing configuration, and may take some time
     * to be fully applied.
     * <p>
     * This method performs all the steps necessary to update the configuration. It
     * first performs lookup on the distribution  using
     * {@link #getDistributionConfig(String)} to find its origin and caller reference
     * values, then uses this information to apply your configuration changes.
     *
     * @param id                             the distribution's unique identifier.
     * @param origin                         the origin to associate with the distribution, either an Amazon S3 bucket or
     *                                       a custom HTTP/S-accessible location.
     * @param cnames                         A list of up to 10 CNAME aliases to associate with the distribution. This
     *                                       parameter may be null, in which case the original CNAME aliases are retained.
     * @param comment                        An optional comment to describe the distribution in your own terms
     *                                       (max 128 characters). May be null, in which case the original comment is retained.
     * @param enabled                        Should the distribution should be enabled and publicly accessible after the
     *                                       configuration update?
     * @param loggingStatus                  Logging status settings (bucket, prefix) for the distribution. If this value
     *                                       is null, logging will be disabled for the distribution.
     * @param trustedSignerSelf              If true the owner of the distribution (you) will be be allowed to generate
     *                                       signed URLs for a private distribution. Note: If either trustedSignerSelf or
     *                                       trustedSignerAwsAccountNumbers parameters are provided the private distribution
     *                                       will require signed URLs to access content.
     * @param trustedSignerAwsAccountNumbers Account Number identifiers for AWS account holders other than the
     *                                       distribution's owner who will be allowed to generate signed URLs for a private
     *                                       distribution. If null or empty, no additional AWS account holders may generate
     *                                       signed URLs. Note: If either trustedSignerSelf or
     *                                       trustedSignerAwsAccountNumbers parameters are provided the private distribution
     *                                       will require signed URLs to access content.
     * @param requiredProtocols              List of protocols that must be used by clients to retrieve content from the
     *                                       distribution. If this value is null or is an empty array all protocols will be
     *                                       permitted.
     * @param defaultRootObject              The name of an object that will be served when someone visits the root of a
     *                                       distribution.
     * @param minTTL                         The time to live (TTL) to apply to objects served by this distribution.
     * @return an object that describes the distribution's updated configuration, including its
     *         origin bucket and CNAME aliases.
     * @throws CloudFrontServiceException
     * @deprecated as of 2012-05-05 API version, use {@link #updateDistributionConfig(String, DistributionConfig)}.
     */
    @Deprecated
    public DistributionConfig updateDistributionConfig(String id, Origin origin,
                                                       String[] cnames, String comment, boolean enabled, LoggingStatus loggingStatus,
                                                       boolean trustedSignerSelf, String[] trustedSignerAwsAccountNumbers,
                                                       String[] requiredProtocols, String defaultRootObject, Long minTTL)
            throws CloudFrontServiceException {
        DistributionConfig config = new DistributionConfig(
                origin, null, cnames, comment, enabled, loggingStatus,
                trustedSignerSelf, trustedSignerAwsAccountNumbers, requiredProtocols,
                defaultRootObject, minTTL);
        return updateDistributionConfigImpl(id, config);
    }

    /**
     * Update the configuration of an existing distribution to change its properties
     * or public/private status. The new configuration properties provided
     * <strong>replace</strong> any existing configuration, and may take some time
     * to be fully applied.
     * <p>
     * This method performs all the steps necessary to update the configuration. It
     * first performs lookup on the distribution  using
     * {@link #getDistributionConfig(String)} to find its origin and caller reference
     * values, then uses this information to apply your configuration changes.
     *
     * @param id                             the distribution's unique identifier.
     * @param origin                         the origin to associate with the distribution, either an Amazon S3 bucket or
     *                                       a custom HTTP/S-accessible location.
     * @param cnames                         A list of up to 10 CNAME aliases to associate with the distribution. This
     *                                       parameter may be null, in which case the original CNAME aliases are retained.
     * @param comment                        An optional comment to describe the distribution in your own terms
     *                                       (max 128 characters). May be null, in which case the original comment is retained.
     * @param enabled                        Should the distribution should be enabled and publicly accessible after the
     *                                       configuration update?
     * @param loggingStatus                  Logging status settings (bucket, prefix) for the distribution. If this value
     *                                       is null, logging will be disabled for the distribution.
     * @param trustedSignerSelf              If true the owner of the distribution (you) will be be allowed to generate
     *                                       signed URLs for a private distribution. Note: If either trustedSignerSelf or
     *                                       trustedSignerAwsAccountNumbers parameters are provided the private distribution
     *                                       will require signed URLs to access content.
     * @param trustedSignerAwsAccountNumbers Account Number identifiers for AWS account holders other than the
     *                                       distribution's owner who will be allowed to generate signed URLs for a private
     *                                       distribution. If null or empty, no additional AWS account holders may generate
     *                                       signed URLs. Note: If either trustedSignerSelf or
     *                                       trustedSignerAwsAccountNumbers parameters are provided the private distribution
     *                                       will require signed URLs to access content.
     * @param requiredProtocols              List of protocols that must be used by clients to retrieve content from the
     *                                       distribution. If this value is null or is an empty array all protocols will be
     *                                       permitted.
     * @param defaultRootObject              The name of an object that will be served when someone visits the root of a
     *                                       distribution.
     * @return an object that describes the distribution's updated configuration, including its
     *         origin bucket and CNAME aliases.
     * @throws CloudFrontServiceException
     */
    public DistributionConfig updateDistributionConfig(String id, Origin origin,
                                                       String[] cnames, String comment, boolean enabled, LoggingStatus loggingStatus,
                                                       boolean trustedSignerSelf, String[] trustedSignerAwsAccountNumbers,
                                                       String[] requiredProtocols, String defaultRootObject)
            throws CloudFrontServiceException {
        DistributionConfig config = new DistributionConfig(
                origin, null, cnames, comment, enabled, loggingStatus,
                trustedSignerSelf, trustedSignerAwsAccountNumbers, requiredProtocols,
                defaultRootObject, null);
        return this.updateDistributionConfigImpl(id, config);
    }

    /**
     * Update the configuration of an existing streaming distribution to change its
     * properties. The new configuration properties provided <strong>replace</strong>
     * any existing configuration, and may take some time to be fully applied.
     * <p>
     * This method performs all the steps necessary to update the configuration. It
     * first performs lookup on the distribution  using
     * {@link #getDistributionConfig(String)} to find its origin and caller reference
     * values, then uses this information to apply your configuration changes.
     *
     * @param id            the distribution's unique identifier.
     * @param origin        the origin to associate with the distribution, either an Amazon S3 bucket or
     *                      a custom HTTP/S-accessible location.
     * @param cnames        A list of up to 10 CNAME aliases to associate with the distribution. This
     *                      parameter may be null, in which case the original CNAME aliases are retained.
     * @param comment       An optional comment to describe the distribution in your own terms
     *                      (max 128 characters). May be null, in which case the original comment is retained.
     * @param enabled       Should the distribution should be enabled and publicly accessible after the
     *                      configuration update?
     * @param loggingStatus Logging status settings (bucket, prefix) for the distribution. If this value
     *                      is null, logging will be disabled for the distribution.
     * @return an object that describes the distribution's updated configuration, including its
     *         origin bucket and CNAME aliases.
     * @throws CloudFrontServiceException
     * @deprecated as of 2012-05-05 API version, use {@link #updateDistributionConfig(String, DistributionConfig)}.
     */
    @Deprecated
    public StreamingDistributionConfig updateStreamingDistributionConfig(
            String id, Origin origin, String[] cnames, String comment, boolean enabled,
            LoggingStatus loggingStatus)
            throws CloudFrontServiceException {
        StreamingDistributionConfig config = new StreamingDistributionConfig(
                origin, null, cnames, comment, enabled, loggingStatus,
                false, null, null);
        return (StreamingDistributionConfig) updateDistributionConfigImpl(id, config);
    }

    /**
     * Update the configuration of an existing streaming distribution to change its
     * properties. The new configuration properties provided <strong>replace</strong>
     * any existing configuration, and may take some time to be fully applied.
     * <p>
     * This method performs all the steps necessary to update the configuration. It
     * first performs lookup on the distribution  using
     * {@link #getDistributionConfig(String)} to find its origin and caller reference
     * values, then uses this information to apply your configuration changes.
     *
     * @param id                             the distribution's unique identifier.
     * @param cnames                         A list of up to 10 CNAME aliases to associate with the distribution. This
     *                                       parameter may be null, in which case the original CNAME aliases are retained.
     * @param comment                        An optional comment to describe the distribution in your own terms
     *                                       (max 128 characters). May be null, in which case the original comment is retained.
     * @param enabled                        Should the distribution should be enabled and publicly accessible after the
     *                                       configuration update?
     * @param loggingStatus                  Logging status settings (bucket, prefix) for the distribution. If this value
     *                                       is null, logging will be disabled for the distribution.
     * @param origin                         the origin to associate with the distribution, either an Amazon S3 bucket or
     *                                       a custom HTTP/S-accessible location.
     * @param trustedSignerSelf              If true the owner of the distribution (you) will be be allowed to generate
     *                                       signed URLs for a private distribution. Note: If either trustedSignerSelf or
     *                                       trustedSignerAwsAccountNumbers parameters are provided the private distribution
     *                                       will require signed URLs to access content.
     * @param trustedSignerAwsAccountNumbers Account Number identifiers for AWS account holders other than the
     *                                       distribution's owner who will be allowed to generate signed URLs for a private
     *                                       distribution. If null or empty, no additional AWS account holders may generate
     *                                       signed URLs. Note: If either trustedSignerSelf or
     *                                       trustedSignerAwsAccountNumbers parameters are provided the private distribution
     *                                       will require signed URLs to access content.
     * @return an object that describes the distribution's updated configuration, including its
     *         origin bucket and CNAME aliases.
     * @throws CloudFrontServiceException
     * @deprecated as of 2012-05-05 API version, use {@link #updateDistributionConfig(String, DistributionConfig)}.
     */
    @Deprecated
    public StreamingDistributionConfig updateStreamingDistributionConfig(
            String id, Origin origin, String[] cnames, String comment, boolean enabled,
            LoggingStatus loggingStatus, boolean trustedSignerSelf,
            String[] trustedSignerAwsAccountNumbers)
            throws CloudFrontServiceException {
        StreamingDistributionConfig config = new StreamingDistributionConfig(
                origin, null, cnames, comment, enabled, loggingStatus,
                trustedSignerSelf, trustedSignerAwsAccountNumbers, null);
        return (StreamingDistributionConfig) updateDistributionConfigImpl(id, config);
    }

    /**
     * Update the configuration of an existing distribution to change its properties.
     * If the original distribution is private this method will make it public instead.
     * The new configuration properties provided <strong>replace</strong> any existing
     * configuration, and may take some time to be fully applied.
     * <p>
     * This method performs all the steps necessary to update the configuration. It
     * first performs lookup on the distribution  using
     * {@link #getDistributionConfig(String)} to find its origin and caller reference
     * values, then uses this information to apply your configuration changes.
     *
     * @param id            the distribution's unique identifier.
     * @param origin        the origin to associate with the distribution, either an Amazon S3 bucket or
     *                      a custom HTTP/S-accessible location.
     * @param cnames        A list of up to 10 CNAME aliases to associate with the distribution. This
     *                      parameter may be null, in which case the original CNAME aliases are retained.
     * @param comment       An optional comment to describe the distribution in your own terms
     *                      (max 128 characters). May be null, in which case the original comment is retained.
     * @param enabled       Should the distribution should be enabled and publicly accessible after the
     *                      configuration update?
     * @param loggingStatus Logging status settings (bucket, prefix) for the distribution. If this value
     *                      is null, logging will be disabled for the distribution.
     * @return an object that describes the distribution's updated configuration, including its
     *         origin bucket and CNAME aliases.
     * @throws CloudFrontServiceException
     */
    public DistributionConfig updateDistributionConfig(String id, Origin origin,
                                                       String[] cnames, String comment, boolean enabled, LoggingStatus loggingStatus)
            throws CloudFrontServiceException {
        return updateDistributionConfig(id, origin, cnames, comment, enabled, loggingStatus,
                false, null, null, null);
    }

    /**
     * Update the configuration of an existing distribution to change its properties
     * or public/private status. The new configuration properties provided
     * <strong>replace</strong> any existing configuration, and may take some time
     * to be fully applied.
     * <p>
     * This method performs all the steps necessary to update the configuration. It
     * first performs lookup on the distribution  using
     * {@link #getDistributionConfig(String)} to find its origin and caller reference
     * values, then uses this information to apply your configuration changes.
     *
     * @param id     the distribution's unique identifier.
     * @param config Configuration properties to apply to the distribution.
     * @return an object that describes the distribution's updated configuration, including its
     *         origin bucket and CNAME aliases.
     * @throws CloudFrontServiceException
     */
    public DistributionConfig
    updateDistributionConfig(String id,
                             DistributionConfig config) throws CloudFrontServiceException {
        return updateDistributionConfigImpl(id, config);
    }

    /**
     * Convenience method to disable a distribution that you intend to delete.
     * This method merely calls the
     * {@link #updateDistributionConfig(String, Origin, String[], String, boolean, LoggingStatus)}
     * method with default values for most of the distribution's configuration
     * settings.
     * <p>
     * <strong>Warning</strong>: Do not use this method on distributions you
     * intend to keep, because it will reset most of the distribution's
     * configuration settings such as CNAMEs and logging status.
     *
     * @param id the distribution's unique identifier.
     * @throws CloudFrontServiceException
     */
    public void disableDistributionForDeletion(String id)
            throws CloudFrontServiceException {
        updateDistributionConfig(id, null, new String[]{}, "Disabled prior to deletion", false, null);
    }

    /**
     * Convenience method to disable a streaming distribution that you intend to delete.
     * This method merely calls the
     * {@link #updateStreamingDistributionConfig(String, Origin, String[], String, boolean, LoggingStatus)}
     * method with default values for most of the distribution's configuration
     * settings.
     * <p>
     * <strong>Warning</strong>: Do not use this method on distributions you
     * intend to keep, because it will reset most of the distribution's
     * configuration settings such as CNAMEs and logging status.
     *
     * @param id the distribution's unique identifier.
     * @throws CloudFrontServiceException
     */
    public void disableStreamingDistributionForDeletion(String id)
            throws CloudFrontServiceException {
        updateStreamingDistributionConfig(id, null, new String[]{}, "Disabled prior to deletion",
                false, // enabled?
                null // LoggingStatus
        );
    }

    /**
     * Delete a streaming or non-streaming distribution.
     *
     * @param isStreaming    Only return streaming distributions
     * @param distributionId The distribution's unique identifier.
     * @throws CloudFrontServiceException
     */
    protected void deleteDistributionImpl(boolean isStreaming, String distributionId)
            throws CloudFrontServiceException {
        if(log.isDebugEnabled()) {
            log.debug("Deleting "
                    + (isStreaming ? "streaming" : "")
                    + "distribution with id: " + distributionId);
        }

        // Get the distribution's current config.
        DistributionConfig currentConfig =
                (isStreaming ? getStreamingDistributionConfig(distributionId) : getDistributionConfig(distributionId));
        HttpDelete httpMethod = new HttpDelete(ENDPOINT + VERSION
                + (isStreaming ? "/streaming-distribution/" : "/distribution/")
                + distributionId);

        try {
            httpMethod.setHeader("If-Match", currentConfig.getEtag());
            HttpResponse response = performRestRequest(httpMethod, 204);
            releaseConnection(response);
        }
        catch(CloudFrontServiceException e) {
            throw e;
        }
        catch(RuntimeException e) {
            throw e;
        }
        catch(Exception e) {
            throw new CloudFrontServiceException(e);
        }
    }

    /**
     * Delete a disabled distribution. You can only delete a distribution that is
     * already disabled, if you delete an enabled distribution this operation will
     * fail with a <tt>DistributionNotDisabled</tt> error.
     * <p>
     * This method performs many of the steps necessary to delete a disabled
     * distribution. It first performs lookup on the distribution using
     * {@link #getDistributionConfig(String)} to find its ETag value, then uses
     * this information to delete the distribution.
     * <p>
     * Because it can take a long time (minutes) to disable a distribution, this
     * task is not performed automatically by this method. In your own code, you
     * need to verify that a distribution is disabled with a status of
     * <tt>Deployed</tt> before you invoke this method.
     *
     * @param id the distribution's unique identifier.
     * @throws CloudFrontServiceException
     */
    public void deleteDistribution(String id) throws CloudFrontServiceException {
        deleteDistributionImpl(false, id);
    }

    /**
     * Delete a disabled streaming distribution. You can only delete a distribution
     * that is already disabled, if you delete an enabled distribution this operation
     * will fail with a <tt>DistributionNotDisabled</tt> error.
     * <p>
     * This method performs many of the steps necessary to delete a disabled
     * distribution. It first performs lookup on the distribution using
     * {@link #getDistributionConfig(String)} to find its ETag value, then uses
     * this information to delete the distribution.
     * <p>
     * Because it can take a long time (minutes) to disable a distribution, this
     * task is not performed automatically by this method. In your own code, you
     * need to verify that a distribution is disabled with a status of
     * <tt>Deployed</tt> before you invoke this method.
     *
     * @param id the distribution's unique identifier.
     * @throws CloudFrontServiceException
     */
    public void deleteStreamingDistribution(String id) throws CloudFrontServiceException {
        deleteDistributionImpl(true, id);
    }

    /**
     * Create a new Origin Access Identity
     *
     * @param callerReference A user-set unique reference value that ensures the request can't be replayed
     *                        (max UTF-8 encoding size 128 bytes). This parameter may be null, in which
     *                        case your computer's local epoch time in milliseconds will be used.
     * @param comment         An optional comment to describe the identity (max 128 characters). May be null.
     * @return The origin access identity's properties.
     * @throws CloudFrontServiceException
     */
    public OriginAccessIdentity createOriginAccessIdentity(
            String callerReference, String comment)
            throws CloudFrontServiceException {
        if(log.isDebugEnabled()) {
            log.debug("Creating origin access identity");
        }

        HttpPost httpMethod = new HttpPost(ENDPOINT + VERSION +
                ORIGIN_ACCESS_IDENTITY_URI_PATH);

        if(callerReference == null) {
            callerReference = String.valueOf(System.currentTimeMillis());
        }

        try {
            XMLBuilder builder = XMLBuilder.create(
                    "CloudFrontOriginAccessIdentityConfig")
                    .a("xmlns", XML_NAMESPACE)
                    .e("CallerReference").t(callerReference).up()
                    .e("Comment").t(comment);

            httpMethod.setEntity(new StringEntity(
                    builder.asString(null),
                    ContentType.create("text/xml", Constants.DEFAULT_ENCODING)));
            HttpResponse response = performRestRequest(httpMethod, 201);

            OriginAccessIdentityHandler handler =
                    (new CloudFrontXmlResponsesSaxParser(this.jets3tProperties))
                            .parseOriginAccessIdentity(response.getEntity().getContent());

            return handler.getOriginAccessIdentity();
        }
        catch(CloudFrontServiceException e) {
            throw e;
        }
        catch(RuntimeException e) {
            throw e;
        }
        catch(Exception e) {
            throw new CloudFrontServiceException(e);
        }
    }

    /**
     * List the Origin Access Identities in a CloudFront account.
     *
     * @return List of {@link OriginAccessIdentity} objects describing the identities.
     * @throws CloudFrontServiceException
     */
    public List<OriginAccessIdentity> getOriginAccessIdentityList()
            throws CloudFrontServiceException {
        if(log.isDebugEnabled()) {
            log.debug("Getting list of origin access identities");
        }
        HttpGet httpMethod = new HttpGet(ENDPOINT + VERSION + ORIGIN_ACCESS_IDENTITY_URI_PATH);

        try {
            HttpResponse response = performRestRequest(httpMethod, 200);

            OriginAccessIdentityListHandler handler =
                    (new CloudFrontXmlResponsesSaxParser(this.jets3tProperties))
                            .parseOriginAccessIdentityListResponse(response.getEntity().getContent());
            return handler.getOriginAccessIdentityList();
        }
        catch(CloudFrontServiceException e) {
            throw e;
        }
        catch(RuntimeException e) {
            throw e;
        }
        catch(Exception e) {
            throw new CloudFrontServiceException(e);
        }
    }

    /**
     * Obtain the complete properties of an Origin Access Identity.
     *
     * @param id The identifier of the Origin Access Identity.
     * @return The origin access identity's properties.
     * @throws CloudFrontServiceException
     */
    public OriginAccessIdentity getOriginAccessIdentity(String id)
            throws CloudFrontServiceException {
        if(log.isDebugEnabled()) {
            log.debug("Getting information for origin access identity with id: " + id);
        }
        HttpGet httpMethod = new HttpGet(ENDPOINT + VERSION +
                ORIGIN_ACCESS_IDENTITY_URI_PATH + "/" + id);

        try {
            HttpResponse response = performRestRequest(httpMethod, 200);

            OriginAccessIdentityHandler handler =
                    (new CloudFrontXmlResponsesSaxParser(this.jets3tProperties))
                            .parseOriginAccessIdentity(response.getEntity().getContent());
            return handler.getOriginAccessIdentity();
        }
        catch(CloudFrontServiceException e) {
            throw e;
        }
        catch(RuntimeException e) {
            throw e;
        }
        catch(Exception e) {
            throw new CloudFrontServiceException(e);
        }
    }

    /**
     * Obtain the configuration properties of an Origin Access Identity.
     *
     * @param id The identifier of the Origin Access Identity.
     * @return The origin access identity's configuration properties.
     * @throws CloudFrontServiceException
     */
    public OriginAccessIdentityConfig getOriginAccessIdentityConfig(String id)
            throws CloudFrontServiceException {
        if(log.isDebugEnabled()) {
            log.debug("Getting config for origin access identity with id: " + id);
        }
        HttpGet httpMethod = new HttpGet(ENDPOINT + VERSION +
                ORIGIN_ACCESS_IDENTITY_URI_PATH + "/" + id + "/config");

        try {
            HttpResponse response = performRestRequest(httpMethod, 200);

            OriginAccessIdentityConfigHandler handler =
                    (new CloudFrontXmlResponsesSaxParser(this.jets3tProperties))
                            .parseOriginAccessIdentityConfig(response.getEntity().getContent());

            OriginAccessIdentityConfig config = handler.getOriginAccessIdentityConfig();
            config.setEtag(response.getFirstHeader("ETag").getValue());
            return config;
        }
        catch(CloudFrontServiceException e) {
            throw e;
        }
        catch(RuntimeException e) {
            throw e;
        }
        catch(Exception e) {
            throw new CloudFrontServiceException(e);
        }
    }

    /**
     * Update the properties of an Origin Access Identity.
     *
     * @param id      The identifier of the Origin Access Identity.
     * @param comment A new comment to apply to the identity.
     * @return The origin access identity's configuration properties.
     * @throws CloudFrontServiceException
     */
    public OriginAccessIdentityConfig updateOriginAccessIdentityConfig(
            String id, String comment) throws CloudFrontServiceException {
        if(log.isDebugEnabled()) {
            log.debug("Updating configuration of origin access identity with id: " + id);
        }

        // Retrieve the old configuration.
        OriginAccessIdentityConfig oldConfig = getOriginAccessIdentityConfig(id);

        // Sanitize parameters.
        if(comment == null) {
            comment = oldConfig.getComment();
        }

        HttpPut httpMethod = new HttpPut(ENDPOINT + VERSION +
                ORIGIN_ACCESS_IDENTITY_URI_PATH + "/" + id + "/config");

        try {
            XMLBuilder builder = XMLBuilder.create(
                    "CloudFrontOriginAccessIdentityConfig")
                    .a("xmlns", XML_NAMESPACE)
                    .e("CallerReference").t(oldConfig.getCallerReference()).up()
                    .e("Comment").t(comment);
            httpMethod.setEntity(new StringEntity(
                    builder.asString(null),
                    ContentType.create("text/xml", Constants.DEFAULT_ENCODING)));
            httpMethod.setHeader("If-Match", oldConfig.getEtag());
            HttpResponse response = performRestRequest(httpMethod, 200);

            OriginAccessIdentityConfigHandler handler =
                    (new CloudFrontXmlResponsesSaxParser(this.jets3tProperties))
                            .parseOriginAccessIdentityConfig(response.getEntity().getContent());

            OriginAccessIdentityConfig config = handler.getOriginAccessIdentityConfig();
            config.setEtag(response.getFirstHeader("ETag").getValue());
            return config;
        }
        catch(CloudFrontServiceException e) {
            throw e;
        }
        catch(RuntimeException e) {
            throw e;
        }
        catch(Exception e) {
            throw new CloudFrontServiceException(e);
        }
    }

    /**
     * Delete an Origin Access Identity.
     *
     * @param id The identifier of the Origin Access Identity.
     * @throws CloudFrontServiceException exception
     */
    public void deleteOriginAccessIdentity(String id) throws CloudFrontServiceException {
        if(log.isDebugEnabled()) {
            log.debug("Deleting origin access identity with id: " + id);
        }

        // Get the identity's current config.
        OriginAccessIdentityConfig currentConfig = getOriginAccessIdentityConfig(id);

        HttpDelete httpMethod = new HttpDelete(ENDPOINT + VERSION +
                ORIGIN_ACCESS_IDENTITY_URI_PATH + "/" + id);

        try {
            httpMethod.setHeader("If-Match", currentConfig.getEtag());
            HttpResponse response = performRestRequest(httpMethod, 204);
            releaseConnection(response);
        }
        catch(CloudFrontServiceException e) {
            throw e;
        }
        catch(RuntimeException e) {
            throw e;
        }
        catch(Exception e) {
            throw new CloudFrontServiceException(e);
        }
    }

    /**
     * Remove distribution objects from a CloudFront edge server cache to force
     * a refresh of the object data from the S3 origin.
     *
     * @param distributionId  The distribution's unique identifier.
     * @param objectKeys      S3 object key names of object(s) to invalidate.
     * @param callerReference Unique description for this distribution config
     * @return invalidation object
     * @throws CloudFrontServiceException exception
     */
    public Invalidation invalidateObjects(String distributionId, String[] objectKeys,
                                          String callerReference) throws CloudFrontServiceException {
        HttpPost httpMethod = new HttpPost(ENDPOINT + VERSION +
                "/distribution/" + distributionId + "/invalidation");
        try {
            XMLBuilder builder = XMLBuilder.create("InvalidationBatch");
            XMLBuilder paths = builder.e("Paths");
            paths.e("Quantity").t(String.valueOf(objectKeys.length));
            XMLBuilder items = paths.e("Items");
            for(String objectPath : objectKeys) {
                String encodedPath = RestUtils.encodeUrlPath(objectPath, "/");
                if(!encodedPath.startsWith("/")) {
                    encodedPath = "/" + encodedPath;
                }
                items.e("Path").t(encodedPath);
            }
            builder.e("CallerReference").t(callerReference);

            httpMethod.setEntity(new StringEntity(
                    builder.asString(null),
                    ContentType.create("text/xml", Constants.DEFAULT_ENCODING)));
            HttpResponse response = performRestRequest(httpMethod, 201);

            InvalidationHandler handler =
                    (new CloudFrontXmlResponsesSaxParser(this.jets3tProperties))
                            .parseInvalidationResponse(response.getEntity().getContent());
            return handler.getInvalidation();
        }
        catch(CloudFrontServiceException e) {
            throw e;
        }
        catch(RuntimeException e) {
            throw e;
        }
        catch(Exception e) {
            throw new CloudFrontServiceException(e);
        }
    }

    /**
     * Remove distribution objects from a CloudFront edge server cache to force
     * a refresh of the object data from the S3 origin.
     *
     * @param distributionId  The distribution's unique identifier.
     * @param objects         S3 object(s) to invalidate.
     * @param callerReference Unique description for this distribution config
     * @return invalidation object
     * @throws CloudFrontServiceException exception
     */
    public Invalidation invalidateObjects(String distributionId, S3Object[] objects,
                                          String callerReference) throws CloudFrontServiceException {
        String[] objectKeys = new String[objects.length];
        for(int i = 0; i < objects.length; i++) {
            objectKeys[i] = objects[i].getKey();
        }
        return invalidateObjects(distributionId, objectKeys, callerReference);
    }

    /**
     * @param distributionId The distribution's unique identifier.
     * @param invalidationId The identifier for the invalidation request
     * @return Details of a prior invalidation operation.
     * @throws CloudFrontServiceException exception
     */
    public Invalidation getInvalidation(String distributionId, String invalidationId)
            throws CloudFrontServiceException {
        HttpGet httpMethod = new HttpGet(ENDPOINT + VERSION +
                "/distribution/" + distributionId + "/invalidation/" + invalidationId);
        try {
            HttpResponse response = performRestRequest(httpMethod, 200);

            InvalidationHandler handler =
                    (new CloudFrontXmlResponsesSaxParser(this.jets3tProperties))
                            .parseInvalidationResponse(response.getEntity().getContent());
            return handler.getInvalidation();
        }
        catch(CloudFrontServiceException e) {
            throw e;
        }
        catch(RuntimeException e) {
            throw e;
        }
        catch(Exception e) {
            throw new CloudFrontServiceException(e);
        }
    }

    /**
     * List a single page of up to pagingSize past invalidation summaries, ordered from
     * most recent to oldest. If there are more prior invalidations than will fit on the
     * page you must perform follow-up calls to this method to obtain a complete listing.
     *
     * @param distributionId The distribution's unique identifier.
     * @param nextMarker     a marker string indicating where to begin the next page of listing results.
     *                       Start with null for an initial listing page, then set to the NextMarker value
     *                       of each subsequent page returned.
     * @param pagingSize     maximum number of invalidation summaries to include in each result page, up to 100.
     * @return invalidation listing
     * @throws CloudFrontServiceException exception
     */
    public InvalidationList listInvalidations(String distributionId, String nextMarker, int pagingSize)
            throws CloudFrontServiceException {
        try {
            String uri = ENDPOINT + VERSION +
                    "/distribution/" + distributionId + "/invalidation"
                    + "?MaxItems=" + pagingSize;
            if(nextMarker != null) {
                uri += "&Marker=" + nextMarker;
            }
            HttpGet httpMethod = new HttpGet(uri);
            HttpResponse response = performRestRequest(httpMethod, 200);

            InvalidationListHandler handler =
                    (new CloudFrontXmlResponsesSaxParser(this.jets3tProperties))
                            .parseInvalidationListResponse(response.getEntity().getContent());
            return handler.getInvalidationList();
        }
        catch(CloudFrontServiceException e) {
            throw e;
        }
        catch(RuntimeException e) {
            throw e;
        }
        catch(Exception e) {
            throw new CloudFrontServiceException(e);
        }
    }

    /**
     * List all past invalidation summaries, ordered from most recent to oldest.
     *
     * @param distributionId The distribution's unique identifier.
     * @return list of invalidation objects
     * @throws CloudFrontServiceException exception
     */
    public List<InvalidationSummary> listInvalidations(String distributionId)
            throws CloudFrontServiceException {
        try {
            List<InvalidationSummary> invalidationSummaries =
                    new ArrayList<InvalidationSummary>();

            String nextMarker = null;
            boolean incompleteListing;
            do {
                InvalidationList invalidationList = listInvalidations(
                        distributionId, nextMarker, 100);
                invalidationSummaries.addAll(invalidationList.getInvalidationSummaries());

                incompleteListing = invalidationList.isTruncated();
                nextMarker = invalidationList.getNextMarker();

                // Sanity check for valid pagination values.
                if(incompleteListing && nextMarker == null) {
                    throw new CloudFrontServiceException("Unable to retrieve paginated "
                            + "InvalidationList results without a valid NextMarker value.");
                }
            }
            while(incompleteListing);

            return invalidationSummaries;
        }
        catch(CloudFrontServiceException e) {
            throw e;
        }
        catch(RuntimeException e) {
            throw e;
        }
        catch(Exception e) {
            throw new CloudFrontServiceException(e);
        }
    }

    /**
     * Sanitizes a proposed bucket name to ensure it is fully-specified rather than
     * merely the bucket's short name. A fully specified bucket name looks like
     * "jets3t.s3.amazonaws.com".
     *
     * @param proposedBucketName the proposed S3 bucket name that will be sanitized.
     * @return the bucket name with the {@link #DEFAULT_BUCKET_SUFFIX} added, if necessary.
     */
    public static String sanitizeS3BucketName(String proposedBucketName) {
        if(!proposedBucketName.endsWith(DEFAULT_BUCKET_SUFFIX)) {
            log.warn("Bucket names used within the CloudFront service should be specified as " +
                    "full S3 subdomain paths like 'jets3t.s3.amazonaws.com'. Repairing " +
                    "faulty bucket name value \"" + proposedBucketName + "\" by adding suffix " +
                    "'" + DEFAULT_BUCKET_SUFFIX + "'.");
            return proposedBucketName + DEFAULT_BUCKET_SUFFIX;
        }
        else {
            return proposedBucketName;
        }
    }

    /**
     * Convert the given string to be safe for use in signed URLs for a private distribution.
     *
     * @param str
     * @return a URL-safe Base64 encoded version of the data.
     * @throws UnsupportedEncodingException
     */
    protected static String makeStringUrlSafe(String str) throws UnsupportedEncodingException {
        return ServiceUtils.toBase64(str.getBytes("UTF-8"))
                .replace('+', '-')
                .replace('=', '_')
                .replace('/', '~');
    }

    /**
     * Convert the given data to be safe for use in signed URLs for a private distribution by
     * using specialized Base64 encoding.
     *
     * @param bytes
     * @return a URL-safe Base64 encoded version of the data.
     * @throws UnsupportedEncodingException
     */
    protected static String makeBytesUrlSafe(byte[] bytes) throws UnsupportedEncodingException {
        return ServiceUtils.toBase64(bytes)
                .replace('+', '-')
                .replace('=', '_')
                .replace('/', '~');
    }

    /**
     * Generate a policy document that describes custom access permissions to apply
     * via a private distribution's signed URL.
     *
     * @param resourcePath         An optional HTTP/S or RTMP resource path that restricts which distribution and S3 objects
     *                             will be accessible in a signed URL. For standard distributions the resource URL will be
     *                             <tt>"http://" + distributionName + "/" + objectKey</tt> (may also include URL
     *                             parameters. For distributions with the HTTPS required protocol, the resource URL
     *                             must start with <tt>"https://"</tt>. RTMP resources do not take the form of a URL,
     *                             and instead the resource path is nothing but the stream's name.
     *                             <p>
     *                             The '*' and '?' characters can be used as a wildcards to allow multi-character or
     *                             single-character matches respectively:
     *                             <ul>
     *                             <li><tt>*</tt> : All distributions/objects will be accessible</li>
     *                             <li><tt>a1b2c3d4e5f6g7.cloudfront.net/*</tt> : All objects within the distribution
     *                             a1b2c3d4e5f6g7 will be accessible</li>
     *                             <li><tt>a1b2c3d4e5f6g7.cloudfront.net/path/to/object.txt</tt> : Only the S3 object
     *                             named <tt>path/to/object.txt</tt> in the distribution a1b2c3d4e5f6g7 will be
     *                             accessible.</li>
     *                             </ul>
     *                             If this parameter is null the policy will permit access to all distributions and S3
     *                             objects associated with the certificate keypair used to generate the signed URL.
     * @param epochDateLessThan    The time and date when the signed URL will expire. REQUIRED.
     * @param limitToIpAddressCIDR An optional range of client IP addresses that will be allowed to access the distribution,
     *                             specified as a CIDR range. If null, the CIDR will be <tt>0.0.0.0/0</tt> and any
     *                             client will be permitted.
     * @param epochDateGreaterThan An optional time and date when the signed URL will become active. If null, the signed
     *                             URL will be active as soon as it is created.
     * @return A policy document describing the access permission to apply when generating a signed URL.
     * @throws CloudFrontServiceException exception
     */
    public static String buildPolicyForSignedUrl(
            String resourcePath, Date epochDateLessThan,
            String limitToIpAddressCIDR, Date epochDateGreaterThan)
            throws CloudFrontServiceException {
        if(epochDateLessThan == null) {
            throw new CloudFrontServiceException(
                    "epochDateLessThan must be provided to sign CloudFront URLs");
        }
        if(resourcePath == null) {
            resourcePath = "*";
        }
        String ipAddress = (limitToIpAddressCIDR == null
                ? "0.0.0.0/0"  // No IP restriction
                : limitToIpAddressCIDR);
        return "{\"Statement\": [{" +
                "\"Resource\":\"" + resourcePath + "\"" +
                ",\"Condition\":{" +
                "\"DateLessThan\":{\"AWS:EpochTime\":"
                + epochDateLessThan.getTime() / 1000 + "}" +
                ",\"IpAddress\":{\"AWS:SourceIp\":\"" + ipAddress + "\"}" +
                (epochDateGreaterThan == null ? ""
                        : ",\"DateGreaterThan\":{\"AWS:EpochTime\":"
                        + epochDateGreaterThan.getTime() / 1000 + "}") +
                "}}]}";
    }

    /**
     * Generate a signed URL that allows access to distribution and S3 objects by
     * applying access restrictions specified in a custom policy document.
     *
     * @param resourceUrlOrPath The URL or path that uniquely identifies a resource within a distribution.
     *                          For standard distributions the resource URL will be
     *                          <tt>"http://" + distributionName + "/" + objectKey</tt> (may also include URL
     *                          parameters. For distributions with the HTTPS required protocol, the resource URL
     *                          must start with <tt>"https://"</tt>. RTMP resources do not take the form of a URL,
     *                          and instead the resource path is nothing but the stream's name.
     * @param keyPairId         Identifier of a public/private certificate keypair already configured in your
     *                          Amazon Web Services account.
     * @param derPrivateKey     The RSA private key data that corresponding to the certificate keypair identified by
     *                          keyPairId, in DER format. To convert a standard PEM private key file into this format
     *                          use the utility method {@link EncryptionUtil#convertRsaPemToDer(java.io.InputStream)}
     * @param policy            A policy document that describes the access permissions that will be applied by the
     *                          signed URL. To generate a custom policy use
     *                          {@link #buildPolicyForSignedUrl(String, Date, String, Date)}.
     * @return A signed URL that will permit access to distribution and S3 objects as specified
     *         in the policy document.
     * @throws CloudFrontServiceException exception
     */
    public static String signUrl(String resourceUrlOrPath,
                                 String keyPairId, byte[] derPrivateKey, String policy)
            throws CloudFrontServiceException {
        try {
            byte[] signatureBytes = EncryptionUtil.signWithRsaSha1(derPrivateKey,
                    policy.getBytes("UTF-8"));

            String urlSafePolicy = makeStringUrlSafe(policy);
            String urlSafeSignature = makeBytesUrlSafe(signatureBytes);

            return resourceUrlOrPath
                    + (resourceUrlOrPath.indexOf('?') >= 0 ? "&" : "?")
                    + "Policy=" + urlSafePolicy
                    + "&Signature=" + urlSafeSignature
                    + "&Key-Pair-Id=" + keyPairId;
        }
        catch(RuntimeException e) {
            throw e;
        }
        catch(Exception e) {
            throw new CloudFrontServiceException(e);
        }
    }

    /**
     * Generate a signed URL that allows access to a specific distribution and
     * S3 object by applying a access restrictions from a "canned" (simplified)
     * policy document.
     *
     * @param resourceUrlOrPath The URL or path that uniquely identifies a resource within a distribution.
     *                          For standard distributions the resource URL will be
     *                          <tt>"http://" + distributionName + "/" + objectKey</tt> (may also include URL
     *                          parameters. For distributions with the HTTPS required protocol, the resource URL
     *                          must start with <tt>"https://"</tt>. RTMP resources do not take the form of a URL,
     *                          and instead the resource path is nothing but the stream's name.
     * @param keyPairId         Identifier of a public/private certificate keypair already configured in your
     *                          Amazon Web Services account.
     * @param derPrivateKey     The RSA private key data that corresponding to the certificate keypair identified by
     *                          keyPairId, in DER format. To convert a standard PEM private key file into this format
     *                          use the utility method {@link EncryptionUtil#convertRsaPemToDer(java.io.InputStream)}
     * @param epochDateLessThan The time and date when the signed URL will expire. REQUIRED.
     * @return A signed URL that will permit access to a specific distribution and S3 object.
     * @throws CloudFrontServiceException exception
     */
    public static String signUrlCanned(String resourceUrlOrPath,
                                       String keyPairId, byte[] derPrivateKey, Date epochDateLessThan)
            throws CloudFrontServiceException {
        try {
            String cannedPolicy =
                    "{\"Statement\":[{\"Resource\":\"" + resourceUrlOrPath
                            + "\",\"Condition\":{\"DateLessThan\":{\"AWS:EpochTime\":"
                            + epochDateLessThan.getTime() / 1000 + "}}}]}";

            byte[] signatureBytes = EncryptionUtil.signWithRsaSha1(derPrivateKey,
                    cannedPolicy.getBytes("UTF-8"));

            String urlSafeSignature = makeBytesUrlSafe(signatureBytes);

            return resourceUrlOrPath
                    + (resourceUrlOrPath.indexOf('?') >= 0 ? "&" : "?")
                    + "Expires=" + epochDateLessThan.getTime() / 1000
                    + "&Signature=" + urlSafeSignature
                    + "&Key-Pair-Id=" + keyPairId;
        }
        catch(RuntimeException e) {
            throw e;
        }
        catch(Exception e) {
            throw new CloudFrontServiceException(e);
        }
    }
}
TOP

Related Classes of org.jets3t.service.CloudFrontService

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.