org.jets3t.service.impl.rest.XmlResponsesSaxParser.java Source code

Java tutorial

Introduction

Here is the source code for org.jets3t.service.impl.rest.XmlResponsesSaxParser.java

Source

/*
 * JetS3t : Java S3 Toolkit
 * Project hosted at http://bitbucket.org/jmurty/jets3t/
 *
 * Copyright 2006-2011 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.impl.rest;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jets3t.service.Constants;
import org.jets3t.service.Jets3tProperties;
import org.jets3t.service.ServiceException;
import org.jets3t.service.acl.EmailAddressGrantee;
import org.jets3t.service.acl.S3CanonicalGrantee;
import org.jets3t.service.acl.S3GrantAndPermission;
import org.jets3t.service.acl.S3GranteeInterface;
import org.jets3t.service.acl.S3GroupGrantee;
import org.jets3t.service.acl.S3Permission;
//import org.jets3t.service.acl.gs.GSAccessControlList;
import org.jets3t.service.model.BaseVersionOrDeleteMarker;
//import org.jets3t.service.model.GSBucket;
//import org.jets3t.service.model.GSBucketLoggingStatus;
//import org.jets3t.service.model.GSObject;
//import org.jets3t.service.model.GSOwner;
import org.jets3t.service.model.S3LifecycleConfiguration;
import org.jets3t.service.model.S3LifecycleConfiguration.Expiration;
import org.jets3t.service.model.S3LifecycleConfiguration.Rule;
import org.jets3t.service.model.S3LifecycleConfiguration.TimeEvent;
import org.jets3t.service.model.S3LifecycleConfiguration.Transition;
import org.jets3t.service.model.MultipartCompleted;
import org.jets3t.service.model.MultipartPart;
import org.jets3t.service.model.MultipleDeleteResult;
import org.jets3t.service.model.NotificationConfig;
import org.jets3t.service.model.RedirectRule;
import org.jets3t.service.model.RoutingRule;
import org.jets3t.service.model.RoutingRuleCondition;
import org.jets3t.service.model.S3BucketLoggingStatus;
import org.jets3t.service.model.S3BucketVersioningStatus;
import org.jets3t.service.model.S3DeleteMarker;
import org.jets3t.service.model.S3MultipartUpload;
import org.jets3t.service.model.S3Owner;
import org.jets3t.service.model.S3Version;
import org.jets3t.service.model.SS3Bucket;
import org.jets3t.service.model.SS3Object;
import org.jets3t.service.model.StorageBucket;
import org.jets3t.service.model.StorageBucketLoggingStatus;
import org.jets3t.service.model.StorageObject;
import org.jets3t.service.model.StorageOwner;
import org.jets3t.service.model.S3WebsiteConfiguration;
import org.jets3t.service.utils.ServiceUtils;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;

/**
 * XML Sax parser to read XML documents returned by S3 via the REST interface, converting these
 * documents into JetS3t objects.
 *
 * @author James Murty
 */
public class XmlResponsesSaxParser {
    private static final Log log = LogFactory.getLog(XmlResponsesSaxParser.class);

    private XMLReader xr = null;

    private Jets3tProperties properties = null;

    private boolean isGoogleStorageMode = false;

    /**
     * Constructs the XML SAX parser.
     *
     * @param properties
     * the JetS3t properties that will be applied when parsing XML documents.
     *
     * @throws ServiceException
     */
    public XmlResponsesSaxParser(Jets3tProperties properties, boolean returnGoogleStorageObjects)
            throws ServiceException {
        this.properties = properties;
        this.isGoogleStorageMode = returnGoogleStorageObjects;
        this.xr = ServiceUtils.loadXMLReader();
    }

    protected StorageBucket newBucket() {
        //        if (isGoogleStorageMode)
        //        {
        //            return new GSBucket();
        //        }
        //        else
        {
            return new SS3Bucket();
        }
    }

    protected StorageObject newObject() {
        //        if (isGoogleStorageMode)
        //        {
        //            return new GSObject();
        //        }
        //        else
        {
            return new SS3Object();
        }
    }

    protected StorageOwner newOwner() {
        //        if (isGoogleStorageMode)
        //        {
        //            return new GSOwner();
        //        }
        //        else
        {
            return new S3Owner();
        }
    }

    /**
     * Parses an XML document from an input stream using a document handler.
     * @param handler
     *        the handler for the XML document
     * @param inputStream
     *        an input stream containing the XML document to parse
     * @throws ServiceException
     *        any parsing, IO or other exceptions are wrapped in an ServiceException.
     */
    protected void parseXmlInputStream(DefaultHandler handler, InputStream inputStream) throws ServiceException {
        try {
            if (log.isDebugEnabled()) {
                log.debug("Parsing XML response document with handler: " + handler.getClass());
            }
            BufferedReader breader = new BufferedReader(
                    new InputStreamReader(inputStream, Constants.DEFAULT_ENCODING));
            xr.setContentHandler(handler);
            xr.setErrorHandler(handler);
            xr.parse(new InputSource(breader));
            inputStream.close();
        } catch (Throwable t) {
            try {
                inputStream.close();
            } catch (IOException e) {
                if (log.isErrorEnabled()) {
                    log.error("Unable to close response InputStream up after XML parse failure", e);
                }
            }
            throw new ServiceException("Failed to parse XML document with handler " + handler.getClass(), t);
        }
    }

    protected InputStream sanitizeXmlDocument(DefaultHandler handler, InputStream inputStream)
            throws ServiceException {
        if (!properties.getBoolProperty("xmlparser.sanitize-listings", true)) {
            // No sanitizing will be performed, return the original input stream unchanged.
            return inputStream;
        } else {
            if (log.isDebugEnabled()) {
                log.debug("Sanitizing XML document destined for handler " + handler.getClass());
            }

            InputStream sanitizedInputStream = null;

            try {
                /* Read object listing XML document from input stream provided into a
                 * string buffer, so we can replace troublesome characters before
                 * sending the document to the XML parser.
                 */
                StringBuilder listingDocBuffer = new StringBuilder();
                BufferedReader br = new BufferedReader(
                        new InputStreamReader(inputStream, Constants.DEFAULT_ENCODING));

                char[] buf = new char[8192];
                int read = -1;
                while ((read = br.read(buf)) != -1) {
                    listingDocBuffer.append(buf, 0, read);
                }
                br.close();

                // Replace any carriage return (\r) characters with explicit XML
                // character entities, to prevent the SAX parser from
                // misinterpreting 0x0D characters as 0x0A.
                String listingDoc = listingDocBuffer.toString().replaceAll("\r", "
");

                sanitizedInputStream = new ByteArrayInputStream(listingDoc.getBytes(Constants.DEFAULT_ENCODING));
            } catch (Throwable t) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    if (log.isErrorEnabled()) {
                        log.error("Unable to close response InputStream after failure sanitizing XML document", e);
                    }
                }
                throw new ServiceException(
                        "Failed to sanitize XML document destined for handler " + handler.getClass(), t);
            }
            return sanitizedInputStream;
        }
    }

    /**
     * Parses a ListBucket response XML document from an input stream.
     * @param inputStream
     * XML data input stream.
     * @return
     * the XML handler object populated with data parsed from the XML stream.
     * @throws ServiceException
     */
    public ListBucketHandler parseListBucketResponse(InputStream inputStream) throws ServiceException {
        ListBucketHandler handler = new ListBucketHandler();
        parseXmlInputStream(handler, sanitizeXmlDocument(handler, inputStream));
        return handler;
    }

    /**
     * Parses a ListAllMyBuckets response XML document from an input stream.
     * @param inputStream
     * XML data input stream.
     * @return
     * the XML handler object populated with data parsed from the XML stream.
     * @throws ServiceException
     */
    public ListAllMyBucketsHandler parseListMyBucketsResponse(InputStream inputStream) throws ServiceException {
        ListAllMyBucketsHandler handler = new ListAllMyBucketsHandler();
        parseXmlInputStream(handler, sanitizeXmlDocument(handler, inputStream));
        return handler;
    }

    /**
     * Parses an AccessControlListHandler response XML document from an input stream.
     *
     * @param inputStream
     * XML data input stream.
     * @return
     * the XML handler object populated with data parsed from the XML stream.
     *
     * @throws ServiceException
     */
    public AccessControlListHandler parseAccessControlListResponse(InputStream inputStream)
            throws ServiceException {
        AccessControlListHandler handler;
        handler = new AccessControlListHandler();

        return parseAccessControlListResponse(inputStream, handler);
    }

    public StorageInfoHandler parseStorageInfoResponse(InputStream inputStream) throws ServiceException {
        StorageInfoHandler handler = new StorageInfoHandler();

        return parseStorageInfoResponse(inputStream, handler);
    }

    public QuotaHandler parseQuotaResponse(InputStream inputStream) throws ServiceException {
        QuotaHandler handler = new QuotaHandler();

        return parseQuotaResponse(inputStream, handler);
    }

    public StoragePolicyHandler parseStoragePolicyResponse(InputStream inputStream) throws ServiceException {
        StoragePolicyHandler handler = new StoragePolicyHandler();

        return parseStoragePolicyResponse(inputStream, handler);
    }

    /**
     * Parses an AccessControlListHandler response XML document from an input stream.
     *
     * @param inputStream
     * XML data input stream.
     * @param handler
     * the instance of AccessControlListHandler to be used.
     * @return
     * the XML handler object populated with data parsed from the XML stream.
     *
     * @throws ServiceException
     */
    public AccessControlListHandler parseAccessControlListResponse(InputStream inputStream,
            AccessControlListHandler handler) throws ServiceException {
        parseXmlInputStream(handler, inputStream);
        return handler;
    }

    public StorageInfoHandler parseStorageInfoResponse(InputStream inputStream, StorageInfoHandler handler)
            throws ServiceException {
        parseXmlInputStream(handler, inputStream);
        return handler;
    }

    public QuotaHandler parseQuotaResponse(InputStream inputStream, QuotaHandler handler) throws ServiceException {
        parseXmlInputStream(handler, inputStream);
        return handler;
    }

    public StoragePolicyHandler parseStoragePolicyResponse(InputStream inputStream, StoragePolicyHandler handler)
            throws ServiceException {
        parseXmlInputStream(handler, inputStream);
        return handler;
    }

    /**
     * Parses a LoggingStatus response XML document for a bucket from an input stream.
     *
     * @param inputStream
     * XML data input stream.
     * @return
     * the XML handler object populated with data parsed from the XML stream.
     *
     * @throws ServiceException
     */
    public BucketLoggingStatusHandler parseLoggingStatusResponse(InputStream inputStream) throws ServiceException {
        BucketLoggingStatusHandler handler;
        //        if (this.isGoogleStorageMode)
        //        {
        //            handler = new GSBucketLoggingStatusHandler();
        //        }
        //        else
        {
            handler = new S3BucketLoggingStatusHandler();
        }
        parseXmlInputStream(handler, inputStream);
        return handler;
    }

    /**
     * Parses a LoggingStatus response XML document for a bucket from an input stream.
     *
     * @param inputStream
     * XML data input stream.
     * @return
     * the XML handler object populated with data parsed from the XML stream.
     *
     * @throws ServiceException
     */
    public BucketLoggingStatusHandler parseLoggingStatusResponse(InputStream inputStream,
            BucketLoggingStatusHandler handler) throws ServiceException {
        parseXmlInputStream(handler, inputStream);
        return handler;
    }

    public String parseBucketLocationResponse(InputStream inputStream) throws ServiceException {
        BucketLocationHandler handler = new BucketLocationHandler();
        parseXmlInputStream(handler, inputStream);
        return handler.getLocation();
    }

    public CopyObjectResultHandler parseCopyObjectResponse(InputStream inputStream) throws ServiceException {
        CopyObjectResultHandler handler = new CopyObjectResultHandler();
        parseXmlInputStream(handler, inputStream);
        return handler;
    }

    /**
     * @param inputStream
     *
     * @return
     * true if the bucket is configured as Requester Pays, false if it is
     * configured as Owner pays.
     *
     * @throws ServiceException
     */
    public boolean parseRequestPaymentConfigurationResponse(InputStream inputStream) throws ServiceException {
        RequestPaymentConfigurationHandler handler = new RequestPaymentConfigurationHandler();
        parseXmlInputStream(handler, inputStream);
        return handler.isRequesterPays();
    }

    /**
     * @param inputStream
     *
     * @return
     * true if the bucket has versioning enabled, false otherwise.
     *
     * @throws ServiceException
     */
    public S3BucketVersioningStatus parseVersioningConfigurationResponse(InputStream inputStream)
            throws ServiceException {
        VersioningConfigurationHandler handler = new VersioningConfigurationHandler();
        parseXmlInputStream(handler, inputStream);
        return handler.getVersioningStatus();
    }

    public ListVersionsResultsHandler parseListVersionsResponse(InputStream inputStream) throws ServiceException {
        ListVersionsResultsHandler handler = new ListVersionsResultsHandler();
        parseXmlInputStream(handler, sanitizeXmlDocument(handler, inputStream));
        return handler;
    }

    public S3MultipartUpload parseInitiateMultipartUploadResult(InputStream inputStream) throws ServiceException {
        MultipartUploadResultHandler handler = new MultipartUploadResultHandler(xr);
        parseXmlInputStream(handler, sanitizeXmlDocument(handler, inputStream));
        return handler.getMultipartUpload();
    }

    public MultipartPart parseMultipartUploadPartCopyResult(InputStream inputStream) throws ServiceException {
        MultipartPartResultHandler handler = new MultipartPartResultHandler(xr);
        parseXmlInputStream(handler, sanitizeXmlDocument(handler, inputStream));
        return handler.getMultipartPart();
    }

    public ListMultipartUploadsResultHandler parseListMultipartUploadsResult(InputStream inputStream)
            throws ServiceException {
        ListMultipartUploadsResultHandler handler = new ListMultipartUploadsResultHandler(xr);
        parseXmlInputStream(handler, sanitizeXmlDocument(handler, inputStream));
        return handler;
    }

    public ListMultipartPartsResultHandler parseListMultipartPartsResult(InputStream inputStream)
            throws ServiceException {
        ListMultipartPartsResultHandler handler = new ListMultipartPartsResultHandler(xr);
        parseXmlInputStream(handler, sanitizeXmlDocument(handler, inputStream));
        return handler;
    }

    public CompleteMultipartUploadResultHandler parseCompleteMultipartUploadResult(InputStream inputStream)
            throws ServiceException {
        CompleteMultipartUploadResultHandler handler = new CompleteMultipartUploadResultHandler(xr);
        parseXmlInputStream(handler, sanitizeXmlDocument(handler, inputStream));
        return handler;
    }

    public S3WebsiteConfiguration parseWebsiteConfigurationResponse(InputStream inputStream)
            throws ServiceException {
        WebsiteConfigurationHandler handler = new WebsiteConfigurationHandler();
        parseXmlInputStream(handler, inputStream);
        return handler.getWebsiteConfig();
    }

    public NotificationConfig parseNotificationConfigurationResponse(InputStream inputStream)
            throws ServiceException {
        NotificationConfigurationHandler handler = new NotificationConfigurationHandler();
        parseXmlInputStream(handler, inputStream);
        return handler.getNotificationConfig();
    }

    public MultipleDeleteResult parseMultipleDeleteResponse(InputStream inputStream) throws ServiceException {
        MultipleDeleteResultHandler handler = new MultipleDeleteResultHandler();
        parseXmlInputStream(handler, inputStream);
        return handler.getMultipleDeleteResult();
    }

    public S3LifecycleConfiguration parseLifecycleConfigurationResponse(InputStream inputStream)
            throws ServiceException {
        LifecycleConfigurationHandler handler = new LifecycleConfigurationHandler(xr);
        parseXmlInputStream(handler, inputStream);
        return handler.getLifecycleConfig();
    }

    //////////////
    // Handlers //
    //////////////

    /**
     * Handler for ListBucket response XML documents.
     * The document is parsed into {@link SS3Object}s available via the {@link #getObjects()} method.
     */
    public class ListBucketHandler extends DefaultXmlHandler {
        private StorageObject currentObject = null;

        private StorageOwner currentOwner = null;

        private boolean insideCommonPrefixes = false;

        private final List<StorageObject> objects = new ArrayList<StorageObject>();

        private final List<String> commonPrefixes = new ArrayList<String>();

        // Listing properties.
        private String bucketName = null;

        private String requestPrefix = null;

        private String requestMarker = null;

        private long requestMaxKeys = 0;

        private boolean listingTruncated = false;

        private String lastKey = null;

        private String nextMarker = null;

        /**
         * If the listing is truncated this method will return the marker that should be used
         * in subsequent bucket list calls to complete the listing.
         *
         * @return
         * null if the listing is not truncated, otherwise the next marker if it's available or
         * the last object key seen if the next marker isn't available.
         */
        public String getMarkerForNextListing() {
            if (listingTruncated) {
                if (nextMarker != null) {
                    return nextMarker;
                } else if (lastKey != null) {
                    return lastKey;
                } else {
                    if (log.isWarnEnabled()) {
                        log.warn("Unable to find Next Marker or Last Key for truncated listing");
                    }
                    return null;
                }
            } else {
                return null;
            }
        }

        /**
         * @return
         * true if the listing document was truncated, and therefore only contained a subset of the
         * available S3 objects.
         */
        public boolean isListingTruncated() {
            return listingTruncated;
        }

        /**
         * @return
         * the S3 objects contained in the listing.
         */
        public StorageObject[] getObjects() {
            return objects.toArray(new StorageObject[objects.size()]);
        }

        public String[] getCommonPrefixes() {
            return commonPrefixes.toArray(new String[commonPrefixes.size()]);
        }

        public String getRequestPrefix() {
            return requestPrefix;
        }

        public String getRequestMarker() {
            return requestMarker;
        }

        public String getNextMarker() {
            return nextMarker;
        }

        public long getRequestMaxKeys() {
            return requestMaxKeys;
        }

        @Override
        public void startElement(String name) {
            if (name.equals("Contents")) {
                currentObject = newObject();
                if (currentObject instanceof SS3Object) {
                    ((SS3Object) currentObject).setBucketName(bucketName);
                }
            } else if (name.equals("Owner")) {
                currentOwner = newOwner();
                currentObject.setOwner(currentOwner);
            } else if (name.equals("CommonPrefixes")) {
                insideCommonPrefixes = true;
            }
        }

        @Override
        public void endElement(String name, String elementText) {
            // Listing details
            if (name.equals("Name")) {
                bucketName = elementText;
                if (log.isDebugEnabled()) {
                    log.debug("Examining listing for bucket: " + bucketName);
                }
            } else if (!insideCommonPrefixes && name.equals("Prefix")) {
                requestPrefix = elementText;
            } else if (name.equals("Marker")) {
                requestMarker = elementText;
            } else if (name.equals("NextMarker")) {
                nextMarker = elementText;
            } else if (name.equals("MaxKeys")) {
                requestMaxKeys = Long.parseLong(elementText);
            } else if (name.equals("IsTruncated")) {
                String isTruncatedStr = elementText.toLowerCase(Locale.getDefault());
                if (isTruncatedStr.startsWith("false")) {
                    listingTruncated = false;
                } else if (isTruncatedStr.startsWith("true")) {
                    listingTruncated = true;
                } else {
                    throw new RuntimeException("Invalid value for IsTruncated field: " + isTruncatedStr);
                }
            }
            // Object details.
            else if (name.equals("Contents")) {
                objects.add(currentObject);
                if (log.isDebugEnabled()) {
                    log.debug("Created new object from listing: " + currentObject);
                }
            } else if (name.equals("Key")) {
                currentObject.setKey(elementText);
                lastKey = elementText;
            } else if (name.equals("LastModified")) {
                try {
                    currentObject.setLastModifiedDate(ServiceUtils.parseIso8601Date(elementText));
                } catch (ParseException e) {
                    throw new RuntimeException(
                            "Non-ISO8601 date for LastModified in bucket's object listing output: " + elementText,
                            e);
                }
            } else if (name.equals("ETag")) {
                currentObject.setETag(elementText);
            } else if (name.equals("Size")) {
                currentObject.setContentLength(Long.parseLong(elementText));
            } else if (name.equals("StorageClass")) {
                currentObject.setStorageClass(elementText);
            }
            // Owner details.
            else if (name.equals("ID")) {
                // Work-around to support Eucalyptus responses, which do not
                // contain Owner elements.
                if (currentOwner == null) {
                    currentOwner = newOwner();
                    currentObject.setOwner(currentOwner);
                }

                currentOwner.setId(elementText);
            } else if (name.equals("DisplayName")) {
                currentOwner.setDisplayName(elementText);
            }
            // Common prefixes.
            else if (insideCommonPrefixes && name.equals("Prefix")) {
                commonPrefixes.add(elementText);
            } else if (name.equals("CommonPrefixes")) {
                insideCommonPrefixes = false;
            }
        }
    }

    /**
     * Handler for ListAllMyBuckets response XML documents. The document is parsed into
     * {@link StorageBucket}s available via the {@link #getBuckets()} method.
     *
     * @author James Murty
     *
     */
    public class ListAllMyBucketsHandler extends DefaultXmlHandler {
        private StorageOwner bucketsOwner = null;

        private StorageBucket currentBucket = null;

        private final List<StorageBucket> buckets = new ArrayList<StorageBucket>();

        /**
         * @return
         * the buckets listed in the document.
         */
        public StorageBucket[] getBuckets() {
            return buckets.toArray(new StorageBucket[buckets.size()]);
        }

        /**
         * @return
         * the owner of the buckets.
         */
        public StorageOwner getOwner() {
            return bucketsOwner;
        }

        @Override
        public void startElement(String name) {
            if (name.equals("Bucket")) {
                currentBucket = newBucket();
            } else if (name.equals("Owner")) {
                bucketsOwner = newOwner();
            }
        }

        @Override
        public void endElement(String name, String elementText) {
            // Listing details.
            if (name.equals("ID")) {
                bucketsOwner.setId(elementText);
            } else if (name.equals("DisplayName")) {
                bucketsOwner.setDisplayName(elementText);
            }
            // Bucket item details.
            else if (name.equals("Bucket")) {
                if (log.isDebugEnabled()) {
                    log.debug("Created new bucket from listing: " + currentBucket);
                }
                currentBucket.setOwner(bucketsOwner);
                buckets.add(currentBucket);
            } else if (name.equals("Name")) {
                currentBucket.setName(elementText);
            } else if (name.equals("CreationDate")) {
                elementText += ".000Z";
                try {
                    currentBucket.setCreationDate(ServiceUtils.parseIso8601Date(elementText));
                } catch (ParseException e) {
                    throw new RuntimeException(
                            "Non-ISO8601 date for CreationDate in list buckets output: " + elementText, e);
                }
            }
        }
    }

    public class BucketLoggingStatusHandler extends DefaultXmlHandler {
        protected StorageBucketLoggingStatus bucketLoggingStatus;

        /**
         * @return
         * an object representing the bucket's LoggingStatus document.
         */
        public StorageBucketLoggingStatus getBucketLoggingStatus() {
            return bucketLoggingStatus;
        }
    }

    /**
     * Handler for LoggingStatus response XML documents for a bucket.
     * The document is parsed into an {@link S3BucketLoggingStatus} object available via the
     * {@link #getBucketLoggingStatus()} method.
     *
     * @author James Murty
     *
     */
    public class S3BucketLoggingStatusHandler extends BucketLoggingStatusHandler {
        private String targetBucket = null;

        private String targetPrefix = null;

        private S3GranteeInterface currentGrantee = null;

        private S3Permission currentPermission = null;

        @Override
        public void startElement(String name) {
            if (name.equals("BucketLoggingStatus")) {
                bucketLoggingStatus = new S3BucketLoggingStatus();
            }
        }

        @Override
        public void endElement(String name, String elementText) {
            if (name.equals("TargetBucket")) {
                targetBucket = elementText;
            } else if (name.equals("TargetPrefix")) {
                targetPrefix = elementText;
            } else if (name.equals("LoggingEnabled")) {
                bucketLoggingStatus.setTargetBucketName(targetBucket);
                bucketLoggingStatus.setLogfilePrefix(targetPrefix);
            }
            // Handle TargetGrants ACLs
            else if (name.equals("ID")) {
                currentGrantee = new S3CanonicalGrantee();
                currentGrantee.setIdentifier(elementText);
            } else if (name.equals("EmailAddress")) {
                currentGrantee = new EmailAddressGrantee();
                currentGrantee.setIdentifier(elementText);
            } else if (name.equals("URI")) {
                currentGrantee = new S3GroupGrantee();
                currentGrantee.setIdentifier(elementText);
            } else if (name.equals("DisplayName")) {
                ((S3CanonicalGrantee) currentGrantee).setDisplayName(elementText);
            } else if (name.equals("Permission")) {
                currentPermission = S3Permission.parsePermission(elementText);
            } else if (name.equals("Grant")) {
                S3GrantAndPermission grantAndPermission = new S3GrantAndPermission(currentGrantee,
                        currentPermission);
                ((S3BucketLoggingStatus) bucketLoggingStatus).addTargetGrant(grantAndPermission);
            }
        }
    }

    /**
     * Handler for Logging response XML documents for a bucket.
     * The document is parsed into an {@link GSBucketLoggingStatus} object available via the
     * {@link #getBucketLoggingStatus()} method.
     *
     * @author David Kocher
     *
     */
    //    public class GSBucketLoggingStatusHandler extends BucketLoggingStatusHandler
    //    {
    //        @Override
    //        public void startElement(String name)
    //        {
    //            if (name.equals("Logging"))
    //            {
    //                bucketLoggingStatus = new GSBucketLoggingStatus();
    //            }
    //        }
    //        
    //        @Override
    //        public void endElement(String name, String elementText)
    //        {
    //            if (name.equals("LogBucket"))
    //            {
    //                bucketLoggingStatus.setTargetBucketName(elementText);
    //            }
    //            else if (name.equals("LogObjectPrefix"))
    //            {
    //                bucketLoggingStatus.setLogfilePrefix(elementText);
    //            }
    //            else if (name.equals("PredefinedAcl"))
    //            {
    //                if (elementText.equals(GSAccessControlList.REST_CANNED_PRIVATE.getValueForRESTHeaderACL()))
    //                {
    //                    ((GSBucketLoggingStatus) bucketLoggingStatus)
    //                        .setPredefinedAcl(GSAccessControlList.REST_CANNED_PRIVATE);
    //                }
    //                else if (elementText.equals(GSAccessControlList.REST_CANNED_PUBLIC_READ.getValueForRESTHeaderACL()))
    //                {
    //                    ((GSBucketLoggingStatus) bucketLoggingStatus)
    //                        .setPredefinedAcl(GSAccessControlList.REST_CANNED_PUBLIC_READ);
    //                }
    //                else if (elementText.equals(GSAccessControlList.REST_CANNED_PUBLIC_READ_WRITE
    //                    .getValueForRESTHeaderACL()))
    //                {
    //                    ((GSBucketLoggingStatus) bucketLoggingStatus)
    //                        .setPredefinedAcl(GSAccessControlList.REST_CANNED_PUBLIC_READ_WRITE);
    //                }
    //                else if (elementText.equals(GSAccessControlList.REST_CANNED_AUTHENTICATED_READ
    //                    .getValueForRESTHeaderACL()))
    //                {
    //                    ((GSBucketLoggingStatus) bucketLoggingStatus)
    //                        .setPredefinedAcl(GSAccessControlList.REST_CANNED_AUTHENTICATED_READ);
    //                }
    //                else if (elementText.equals(GSAccessControlList.REST_CANNED_BUCKET_OWNER_READ
    //                    .getValueForRESTHeaderACL()))
    //                {
    //                    ((GSBucketLoggingStatus) bucketLoggingStatus)
    //                        .setPredefinedAcl(GSAccessControlList.REST_CANNED_BUCKET_OWNER_READ);
    //                }
    //                else if (elementText.equals(GSAccessControlList.REST_CANNED_BUCKET_OWNER_FULL_CONTROL
    //                    .getValueForRESTHeaderACL()))
    //                {
    //                    ((GSBucketLoggingStatus) bucketLoggingStatus)
    //                        .setPredefinedAcl(GSAccessControlList.REST_CANNED_BUCKET_OWNER_FULL_CONTROL);
    //                }
    //            }
    //        }
    //    }

    /**
     * Handler for CreateBucketConfiguration response XML documents for a bucket.
     * The document is parsed into a String representing the bucket's location,
     * available via the {@link #getLocation()} method.
     *
     * @author James Murty
     *
     */
    public class BucketLocationHandler extends DefaultXmlHandler {
        private String location = null;

        /**
         * @return
         * the bucket's location.
         */
        public String getLocation() {
            return location;
        }

        @Override
        public void endElement(String name, String elementText) {
            if (name.equals("LocationConstraint")) {
                if (elementText.length() == 0) {
                    location = null;
                } else {
                    location = elementText;
                }
            }
        }
    }

    public class CopyObjectResultHandler extends DefaultXmlHandler {
        // Data items for successful copy
        private String etag = null;

        private Date lastModified = null;

        // Data items for failed copy
        private String errorCode = null;

        private String errorMessage = null;

        private String errorRequestId = null;

        private String errorHostId = null;

        private boolean receivedErrorResponse = false;

        public Date getLastModified() {
            return lastModified;
        }

        public String getETag() {
            return etag;
        }

        public String getErrorCode() {
            return errorCode;
        }

        public String getErrorHostId() {
            return errorHostId;
        }

        public String getErrorMessage() {
            return errorMessage;
        }

        public String getErrorRequestId() {
            return errorRequestId;
        }

        public boolean isErrorResponse() {
            return receivedErrorResponse;
        }

        @Override
        public void startElement(String name) {
            if (name.equals("CopyObjectResult")) {
                receivedErrorResponse = false;
            } else if (name.equals("Error")) {
                receivedErrorResponse = true;
            }
        }

        @Override
        public void endElement(String name, String elementText) {
            if (name.equals("LastModified")) {
                try {
                    lastModified = ServiceUtils.parseIso8601Date(elementText);
                } catch (ParseException e) {
                    throw new RuntimeException(
                            "Non-ISO8601 date for LastModified in copy object output: " + elementText, e);
                }
            } else if (name.equals("ETag")) {
                etag = elementText;
            } else if (name.equals("Code")) {
                errorCode = elementText;
            } else if (name.equals("Message")) {
                errorMessage = elementText;
            } else if (name.equals("RequestId")) {
                errorRequestId = elementText;
            } else if (name.equals("HostId")) {
                errorHostId = elementText;
            }
        }
    }

    /**
     * Handler for RequestPaymentConfiguration response XML documents for a bucket.
     * The document is parsed into a boolean value: true if the bucket is configured
     * as Requester Pays, false if it is configured as Owner pays. This boolean value
     * is available via the {@link #isRequesterPays()} method.
     *
     * @author James Murty
     */
    public class RequestPaymentConfigurationHandler extends DefaultXmlHandler {
        private String payer = null;

        /**
         * @return
         * true if the bucket is configured as Requester Pays, false if it is
         * configured as Owner pays.
         */
        public boolean isRequesterPays() {
            return "Requester".equals(payer);
        }

        @Override
        public void endElement(String name, String elementText) {
            if (name.equals("Payer")) {
                payer = elementText;
            }
        }
    }

    public class VersioningConfigurationHandler extends DefaultXmlHandler {
        private S3BucketVersioningStatus versioningStatus = null;

        private String status = null;

        private String mfaStatus = null;

        public S3BucketVersioningStatus getVersioningStatus() {
            return this.versioningStatus;
        }

        @Override
        public void endElement(String name, String elementText) {
            if (name.equals("Status")) {
                this.status = elementText;
            } else if (name.equals("MfaDelete")) {
                this.mfaStatus = elementText;
            } else if (name.equals("VersioningConfiguration")) {
                if (!"Enabled".equals(status) && !"Suspended".equals(status)) {
                    this.versioningStatus = new S3BucketVersioningStatus("Off", "Enabled".equals(mfaStatus));
                } else {
                    this.versioningStatus = new S3BucketVersioningStatus(this.status, "Enabled".equals(mfaStatus));
                }
            }
        }
    }

    public class ListVersionsResultsHandler extends DefaultXmlHandler {
        private final List<BaseVersionOrDeleteMarker> items = new ArrayList<BaseVersionOrDeleteMarker>();

        private final List<String> commonPrefixes = new ArrayList<String>();

        private String key = null;

        private String versionId = null;

        private boolean isLatest = false;

        private Date lastModified = null;

        private StorageOwner owner = null;

        private String etag = null;

        private long size = 0;

        private String storageClass = null;

        private boolean insideCommonPrefixes = false;

        // Listing properties.
        private String bucketName = null;

        private String requestPrefix = null;

        private String keyMarker = null;

        private String versionIdMarker = null;

        private long requestMaxKeys = 0;

        private boolean listingTruncated = false;

        private String nextMarker = null;

        private String nextVersionIdMarker = null;

        public String getBucketName() {
            return this.bucketName;
        }

        /**
         * @return
         * true if the listing document was truncated, and therefore only contained a subset of the
         * available S3 objects.
         */
        public boolean isListingTruncated() {
            return listingTruncated;
        }

        /**
         * @return
         * the S3 objects contained in the listing.
         */
        public BaseVersionOrDeleteMarker[] getItems() {
            return items.toArray(new BaseVersionOrDeleteMarker[items.size()]);
        }

        public String[] getCommonPrefixes() {
            return commonPrefixes.toArray(new String[commonPrefixes.size()]);
        }

        public String getRequestPrefix() {
            return requestPrefix;
        }

        public String getKeyMarker() {
            return keyMarker;
        }

        public String getVersionIdMarker() {
            return versionIdMarker;
        }

        public String getNextKeyMarker() {
            return nextMarker;
        }

        public String getNextVersionIdMarker() {
            return nextVersionIdMarker;
        }

        public long getRequestMaxKeys() {
            return requestMaxKeys;
        }

        @Override
        public void startElement(String name) {
            if (name.equals("Owner")) {
                owner = null;
            } else if (name.equals("CommonPrefixes")) {
                insideCommonPrefixes = true;
            }
        }

        @Override
        public void endElement(String name, String elementText) {
            // Listing details
            if (name.equals("Name")) {
                bucketName = elementText;
                if (log.isDebugEnabled()) {
                    log.debug("Examining listing for bucket: " + bucketName);
                }
            } else if (!insideCommonPrefixes && name.equals("Prefix")) {
                requestPrefix = elementText;
            } else if (name.equals("KeyMarker")) {
                keyMarker = elementText;
            } else if (name.equals("NextKeyMarker")) {
                nextMarker = elementText;
            } else if (name.equals("VersionIdMarker")) {
                versionIdMarker = elementText;
            } else if (name.equals("NextVersionIdMarker")) {
                nextVersionIdMarker = elementText;
            } else if (name.equals("MaxKeys")) {
                requestMaxKeys = Long.parseLong(elementText);
            } else if (name.equals("IsTruncated")) {
                String isTruncatedStr = elementText.toLowerCase(Locale.getDefault());
                if (isTruncatedStr.startsWith("false")) {
                    listingTruncated = false;
                } else if (isTruncatedStr.startsWith("true")) {
                    listingTruncated = true;
                } else {
                    throw new RuntimeException("Invalid value for IsTruncated field: " + isTruncatedStr);
                }
            }
            // Version/DeleteMarker finished.
            else if (name.equals("Version")) {
                BaseVersionOrDeleteMarker item = new S3Version(key, versionId, isLatest, lastModified,
                        (S3Owner) owner, etag, size, storageClass);
                items.add(item);
            } else if (name.equals("DeleteMarker")) {
                BaseVersionOrDeleteMarker item = new S3DeleteMarker(key, versionId, isLatest, lastModified,
                        (S3Owner) owner);
                items.add(item);

                // Version/DeleteMarker details
            } else if (name.equals("Key")) {
                key = elementText;
            } else if (name.equals("VersionId")) {
                versionId = elementText;
            } else if (name.equals("IsLatest")) {
                isLatest = "true".equals(elementText);
            } else if (name.equals("LastModified")) {
                try {
                    lastModified = ServiceUtils.parseIso8601Date(elementText);
                } catch (ParseException e) {
                    throw new RuntimeException(
                            "Non-ISO8601 date for LastModified in bucket's versions listing output: " + elementText,
                            e);
                }
            } else if (name.equals("ETag")) {
                etag = elementText;
            } else if (name.equals("Size")) {
                size = Long.parseLong(elementText);
            } else if (name.equals("StorageClass")) {
                storageClass = elementText;
            }
            // Owner details.
            else if (name.equals("ID")) {
                owner = newOwner();
                owner.setId(elementText);
            } else if (name.equals("DisplayName")) {
                owner.setDisplayName(elementText);
            }
            // Common prefixes.
            else if (insideCommonPrefixes && name.equals("Prefix")) {
                commonPrefixes.add(elementText);
            } else if (name.equals("CommonPrefixes")) {
                insideCommonPrefixes = false;
            }
        }
    }

    public class OwnerHandler extends SimpleHandler {
        private String id;

        private String displayName;

        public OwnerHandler(XMLReader xr) {
            super(xr);
        }

        public StorageOwner getOwner() {
            StorageOwner owner = newOwner();
            owner.setId(id);
            owner.setDisplayName(displayName);
            return owner;
        }

        public void endID(String text) {
            this.id = text;
        }

        public void endDisplayName(String text) {
            this.displayName = text;
        }

        public void endOwner(String text) {
            returnControlToParentHandler();
        }

        // </Initiator> represents end of an owner item in ListMultipartUploadsResult/Upload
        public void endInitiator(String text) {
            returnControlToParentHandler();
        }
    }

    public class MultipartUploadResultHandler extends SimpleHandler {
        private String uploadId;

        private String bucketName;

        private String objectKey;

        private String storageClass;

        private S3Owner owner;

        private S3Owner initiator;

        private Date initiatedDate;

        private boolean inInitiator = false;

        public MultipartUploadResultHandler(XMLReader xr) {
            super(xr);
        }

        public S3MultipartUpload getMultipartUpload() {
            if (initiatedDate != null) {
                // Return the contents from a ListMultipartUploadsResult response
                return new S3MultipartUpload(uploadId, objectKey, storageClass, initiator, owner, initiatedDate);
            } else {
                // Return the contents from an InitiateMultipartUploadsResult response
                return new S3MultipartUpload(uploadId, bucketName, objectKey);
            }
        }

        public void endUploadId(String text) {
            this.uploadId = text;
        }

        public void endBucket(String text) {
            this.bucketName = text;
        }

        public void endKey(String text) {
            this.objectKey = text;
        }

        public void endStorageClass(String text) {
            this.storageClass = text;
        }

        public void endInitiated(String text) throws ParseException {
            this.initiatedDate = ServiceUtils.parseIso8601Date(text);
        }

        public void startOwner() {
            inInitiator = false;
            transferControlToHandler(new OwnerHandler(xr));
        }

        public void startInitiator() {
            inInitiator = true;
            transferControlToHandler(new OwnerHandler(xr));
        }

        @Override
        public void controlReturned(SimpleHandler childHandler) {
            if (inInitiator) {
                this.owner = (S3Owner) ((OwnerHandler) childHandler).getOwner();
            } else {
                this.initiator = (S3Owner) ((OwnerHandler) childHandler).getOwner();
            }
        }

        // </Upload> represents end of a MultipartUpload item in ListMultipartUploadsResult
        public void endUpload(String text) {
            returnControlToParentHandler();
        }
    }

    public class ListMultipartUploadsResultHandler extends SimpleHandler {
        private final List<S3MultipartUpload> uploads = new ArrayList<S3MultipartUpload>();

        private final List<String> commonPrefixes = new ArrayList<String>();

        private boolean insideCommonPrefixes;

        private String bucketName = null;

        private String keyMarker = null;

        private String uploadIdMarker = null;

        private String nextKeyMarker = null;

        private String nextUploadIdMarker = null;

        private int maxUploads = 1000;

        private boolean isTruncated = false;

        public ListMultipartUploadsResultHandler(XMLReader xr) {
            super(xr);
        }

        public List<S3MultipartUpload> getMultipartUploadList() {
            // Update multipart upload objects with overall bucket name
            for (S3MultipartUpload upload : uploads) {
                upload.setBucketName(bucketName);
            }
            return uploads;
        }

        public boolean isTruncated() {
            return isTruncated;
        }

        public String getKeyMarker() {
            return keyMarker;
        }

        public String getUploadIdMarker() {
            return uploadIdMarker;
        }

        public String getNextKeyMarker() {
            return nextKeyMarker;
        }

        public String getNextUploadIdMarker() {
            return nextUploadIdMarker;
        }

        public int getMaxUploads() {
            return maxUploads;
        }

        public String[] getCommonPrefixes() {
            return commonPrefixes.toArray(new String[commonPrefixes.size()]);
        }

        public void startUpload() {
            transferControlToHandler(new MultipartUploadResultHandler(xr));
        }

        public void startCommonPrefixes() {
            insideCommonPrefixes = true;
        }

        @Override
        public void controlReturned(SimpleHandler childHandler) {
            uploads.add(((MultipartUploadResultHandler) childHandler).getMultipartUpload());
        }

        public void endBucket(String text) {
            this.bucketName = text;
        }

        public void endKeyMarker(String text) {
            this.keyMarker = text;
        }

        public void endUploadIdMarker(String text) {
            this.uploadIdMarker = text;
        }

        public void endNextKeyMarker(String text) {
            this.nextKeyMarker = text;
        }

        public void endNextUploadIdMarker(String text) {
            this.nextUploadIdMarker = text;
        }

        public void endMaxUploads(String text) {
            this.maxUploads = Integer.parseInt(text);
        }

        public void endIsTruncated(String text) {
            this.isTruncated = "true".equalsIgnoreCase(text);
        }

        public void endPrefix(String text) {
            if (insideCommonPrefixes) {
                commonPrefixes.add(text);
            }
        }

        public void endCommonPrefixes() {
            insideCommonPrefixes = false;
        }

    }

    public class MultipartPartResultHandler extends SimpleHandler {
        private Integer partNumber = -1; // CopyPartResult doesn't include part number, use clearly invalid default

        private Date lastModified;

        private String etag;

        private Long size = -1l; // CopyPartResult doesn't include size, use clearly invalid default

        public MultipartPartResultHandler(XMLReader xr) {
            super(xr);
        }

        public MultipartPart getMultipartPart() {
            return new MultipartPart(partNumber, lastModified, etag, size);
        }

        public void endPartNumber(String text) {
            this.partNumber = Integer.parseInt(text);
        }

        public void endLastModified(String text) throws ParseException {
            this.lastModified = ServiceUtils.parseIso8601Date(text);
        }

        public void endETag(String text) {
            this.etag = text;
        }

        public void endSize(String text) {
            this.size = Long.parseLong(text);
        }

        // </Part> represents end of a Part item in ListPartsResultHandler/Part
        public void endPart(String text) {
            returnControlToParentHandler();
        }
    }

    public class ListMultipartPartsResultHandler extends SimpleHandler {
        private final List<MultipartPart> parts = new ArrayList<MultipartPart>();

        private String bucketName = null;

        private String objectKey = null;

        private String uploadId = null;

        private S3Owner initiator = null;

        private S3Owner owner = null;

        private String storageClass = null;

        private String partNumberMarker = null;

        private String nextPartNumberMarker = null;

        private int maxParts = 1000;

        private boolean isTruncated = false;

        private boolean inInitiator = false;

        public ListMultipartPartsResultHandler(XMLReader xr) {
            super(xr);
        }

        public List<MultipartPart> getMultipartPartList() {
            return parts;
        }

        public boolean isTruncated() {
            return isTruncated;
        }

        public String getBucketName() {
            return bucketName;
        }

        public String getObjectKey() {
            return objectKey;
        }

        public String getUploadId() {
            return uploadId;
        }

        public S3Owner getInitiator() {
            return initiator;
        }

        public S3Owner getOwner() {
            return owner;
        }

        public String getStorageClass() {
            return storageClass;
        }

        public String getPartNumberMarker() {
            return partNumberMarker;
        }

        public String getNextPartNumberMarker() {
            return nextPartNumberMarker;
        }

        public int getMaxParts() {
            return maxParts;
        }

        public void startPart() {
            transferControlToHandler(new MultipartPartResultHandler(xr));
        }

        @Override
        public void controlReturned(SimpleHandler childHandler) {
            if (childHandler instanceof MultipartPartResultHandler) {
                parts.add(((MultipartPartResultHandler) childHandler).getMultipartPart());
            } else {
                if (inInitiator) {
                    initiator = (S3Owner) ((OwnerHandler) childHandler).getOwner();
                } else {
                    owner = (S3Owner) ((OwnerHandler) childHandler).getOwner();
                }
            }
        }

        public void startInitiator() {
            inInitiator = true;
            transferControlToHandler(new OwnerHandler(xr));
        }

        public void startOwner() {
            inInitiator = false;
            transferControlToHandler(new OwnerHandler(xr));
        }

        public void endBucket(String text) {
            this.bucketName = text;
        }

        public void endKey(String text) {
            this.objectKey = text;
        }

        public void endStorageClass(String text) {
            this.storageClass = text;
        }

        public void endUploadId(String text) {
            this.uploadId = text;
        }

        public void endPartNumberMarker(String text) {
            this.partNumberMarker = text;
        }

        public void endNextPartNumberMarker(String text) {
            this.nextPartNumberMarker = text;
        }

        public void endMaxParts(String text) {
            this.maxParts = Integer.parseInt(text);
        }

        public void endIsTruncated(String text) {
            this.isTruncated = "true".equalsIgnoreCase(text);
        }
    }

    public class CompleteMultipartUploadResultHandler extends SimpleHandler {
        private String location;

        private String bucketName;

        private String objectKey;

        private String etag;

        private ServiceException serviceException = null;

        public CompleteMultipartUploadResultHandler(XMLReader xr) {
            super(xr);
        }

        public MultipartCompleted getMultipartCompleted() {
            return new MultipartCompleted(location, bucketName, objectKey, etag);
        }

        public ServiceException getServiceException() {
            return serviceException;
        }

        public void endLocation(String text) {
            this.location = text;
        }

        public void endBucket(String text) {
            this.bucketName = text;
        }

        public void endKey(String text) {
            this.objectKey = text;
        }

        public void endETag(String text) {
            this.etag = text;
        }

        public void startError() {
            transferControlToHandler(new CompleteMultipartUploadErrorHandler(xr));
        }

        @Override
        public void controlReturned(SimpleHandler childHandler) {
            this.serviceException = ((CompleteMultipartUploadErrorHandler) childHandler).getServiceException();
        }
    }

    public class CompleteMultipartUploadErrorHandler extends SimpleHandler {
        private String code = null;

        private String message = null;

        private String etag = null;

        private Long minSizeAllowed = null;

        private Long proposedSize = null;

        private String hostId = null;

        private Integer partNumber = null;

        private String requestId = null;

        public CompleteMultipartUploadErrorHandler(XMLReader xr) {
            super(xr);
        }

        public ServiceException getServiceException() {
            String fullMessage = message + ": PartNumber=" + partNumber + ", MinSizeAllowed=" + minSizeAllowed
                    + ", ProposedSize=" + proposedSize + ", ETag=" + etag;
            ServiceException e = new ServiceException(fullMessage);
            e.setErrorCode(code);
            e.setErrorMessage(message);
            e.setErrorHostId(hostId);
            e.setErrorRequestId(requestId);
            return e;
        }

        public void endCode(String text) {
            this.code = text;
        }

        public void endMessage(String text) {
            this.message = text;
        }

        public void endETag(String text) {
            this.etag = text;
        }

        public void endMinSizeAllowed(String text) {
            this.minSizeAllowed = Long.parseLong(text);
        }

        public void endProposedSize(String text) {
            this.proposedSize = Long.parseLong(text);
        }

        public void endHostId(String text) {
            this.hostId = text;
        }

        public void endPartNumber(String text) {
            this.partNumber = Integer.parseInt(text);
        }

        public void endRequestId(String text) {
            this.requestId = text;
        }

        public void endError(String text) {
            returnControlToParentHandler();
        }
    }

    public class WebsiteConfigurationHandler extends DefaultXmlHandler {
        private S3WebsiteConfiguration config = new S3WebsiteConfiguration();

        private RedirectRule currentRedirectRule = null;

        private RoutingRule currentRoutingRule = null;

        private RoutingRuleCondition currentCondition = null;

        private String indexDocumentSuffix = null;

        private String errorDocumentKey = null;

        public S3WebsiteConfiguration getWebsiteConfig() {
            return config;
        }

        @Override
        public void startElement(String name) {

            if (name.equals("RedirectAllRequestsTo")) {
                currentRedirectRule = new RedirectRule();
                this.config.setRedirectAllRequestsTo(currentRedirectRule);
            }
            if ("RoutingRules".equals(name)) {
                List<RoutingRule> routingRules = new LinkedList<RoutingRule>();
                this.config.setRoutingRules(routingRules);
            }
            if (name.equals("RoutingRule")) {
                currentRoutingRule = new RoutingRule();
                this.config.getRoutingRules().add(currentRoutingRule);
            }
            if (name.equals("Condition")) {
                currentCondition = new RoutingRuleCondition();
                currentRoutingRule.setCondition(currentCondition);
            } else if (name.equals("Redirect")) {
                currentRedirectRule = new RedirectRule();
                currentRoutingRule.setRedirect(currentRedirectRule);
            }
        }

        @Override
        public void endElement(String name, String elementText) {
            if (name.equals("Suffix")) {
                config.setIndexDocumentSuffix(elementText);
            }
            if (name.equals("Key")) {
                config.setErrorDocumentKey(elementText);
            }
            if (name.equals("KeyPrefixEquals")) {
                currentCondition.setKeyPrefixEquals(elementText);
            } else if (name.equals("HttpErrorCodeReturnedEquals")) {
                currentCondition.setHttpErrorCodeReturnedEquals(elementText);
            }

            if (name.equals("Protocol")) {
                currentRedirectRule.setProtocol(elementText);

            } else if (name.equals("HostName")) {
                currentRedirectRule.setHostName(elementText);

            } else if (name.equals("ReplaceKeyPrefixWith")) {
                currentRedirectRule.setReplaceKeyPrefixWith(elementText);

            } else if (name.equals("ReplaceKeyWith")) {
                currentRedirectRule.setReplaceKeyWith(elementText);

            } else if (name.equals("HttpRedirectCode")) {
                currentRedirectRule.setHttpRedirectCode(elementText);
            }
        }
    }

    public class NotificationConfigurationHandler extends DefaultXmlHandler {
        private NotificationConfig config = new NotificationConfig();

        private String lastTopic = null;

        private String lastEvent = null;

        public NotificationConfig getNotificationConfig() {
            return config;
        }

        @Override
        public void endElement(String name, String elementText) {
            if (name.equals("Topic")) {
                this.lastTopic = elementText;
            } else if (name.equals("Event")) {
                this.lastEvent = elementText;
                config.addTopicConfig(config.new TopicConfig(this.lastTopic, this.lastEvent));
            } else if (name.equals("NotificationConfiguration")) {
            }
        }
    }

    public class MultipleDeleteResultHandler extends DefaultXmlHandler {
        private MultipleDeleteResult result = new MultipleDeleteResult();

        private List<MultipleDeleteResult.DeletedObjectResult> deletedObjectResults = new ArrayList<MultipleDeleteResult.DeletedObjectResult>();

        private List<MultipleDeleteResult.ErrorResult> errorResults = new ArrayList<MultipleDeleteResult.ErrorResult>();

        private boolean inDeleted, inError;

        private String key, version, deleteMarkerVersion, errorCode, message;

        private Boolean withDeleteMarker;

        public MultipleDeleteResult getMultipleDeleteResult() {
            return result;
        }

        @Override
        public void startElement(String name) {
            if ("Deleted".equals(name)) {
                inDeleted = true;
            } else if ("Error".equals(name)) {
                inError = true;
            }
        }

        @Override
        public void endElement(String name, String elementText) {
            if ("Key".equals(name)) {
                key = elementText;
            } else if ("VersionId".equals(name)) {
                version = elementText;
            } else if ("DeleteMarker".equals(name)) {
                withDeleteMarker = Boolean.valueOf(elementText);
            } else if ("DeleteMarkerVersionId".equals(name)) {
                deleteMarkerVersion = elementText;
            } else if ("Code".equals(name)) {
                errorCode = elementText;
            } else if ("Message".equals(name)) {
                message = elementText;
            }

            else if ("Deleted".equals(name)) {
                deletedObjectResults
                        .add(result.new DeletedObjectResult(key, version, withDeleteMarker, deleteMarkerVersion));
                inDeleted = false;
                key = version = deleteMarkerVersion = errorCode = message = null;
                withDeleteMarker = null;
            } else if ("Error".equals(name)) {
                errorResults.add(result.new ErrorResult(key, version, errorCode, message));
                inError = false;
                key = version = deleteMarkerVersion = errorCode = message = null;
                withDeleteMarker = null;
            }

            else if (name.equals("DeleteResult")) {
                result.setDeletedObjectResults(deletedObjectResults);
                result.setErrorResults(errorResults);
            }
        }
    }

    public class LifecycleConfigurationHandler extends SimpleHandler {
        private S3LifecycleConfiguration config = new S3LifecycleConfiguration();

        private Rule latestRule = null;

        private TimeEvent latestTimeEvent = null;

        public LifecycleConfigurationHandler(XMLReader xr) {
            super(xr);
        }

        public S3LifecycleConfiguration getLifecycleConfig() {
            return config;
        }

        // Transition/Expiration section

        public void startTransition() {
            latestTimeEvent = config.new Transition();
            latestRule.setTransition(((Transition) latestTimeEvent));
        }

        public void startExpiration() {
            latestTimeEvent = config.new Expiration();
            latestRule.setExpiration(((Expiration) latestTimeEvent));
        }

        public void endDate(String text) throws ParseException {
            this.latestTimeEvent.setDate(ServiceUtils.parseIso8601Date(text));
        }

        public void endDays(String text) {
            this.latestTimeEvent.setDays(Integer.parseInt(text));
        }

        public void endStorageClass(String text) {
            ((Transition) this.latestTimeEvent).setStorageClass(text);
        }

        // Rule section

        public void startRule() {
            latestRule = config.new Rule();
        }

        public void endID(String text) {
            latestRule.setId(text);
        }

        public void endPrefix(String text) {
            latestRule.setPrefix(text);
        }

        public void endStatus(String text) {
            latestRule.setEnabled(text.equals("Enabled"));
        }

        public void endRule(String text) {
            config.addRule(latestRule);
        }
    }

}