// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.
package com.cloud.bridge.service.controller.s3;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.text.SimpleDateFormat;
import java.util.Calendar;

import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.bind.DatatypeConverter;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.stream.XMLStreamException;

import org.apache.log4j.Logger;
import org.json.simple.parser.ParseException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.amazon.s3.GetBucketAccessControlPolicyResponse;
import com.amazon.s3.ListAllMyBucketsResponse;
import com.amazon.s3.ListBucketResponse;
import com.cloud.bridge.io.MTOMAwareResultStreamWriter;
import com.cloud.bridge.model.BucketPolicyVO;
import com.cloud.bridge.model.SAcl;
import com.cloud.bridge.model.SAclVO;
import com.cloud.bridge.model.SBucketVO;
import com.cloud.bridge.persist.dao.BucketPolicyDao;
import com.cloud.bridge.persist.dao.MultipartLoadDao;
import com.cloud.bridge.persist.dao.SBucketDao;
import com.cloud.bridge.service.S3Constants;
import com.cloud.bridge.service.S3RestServlet;
import com.cloud.bridge.service.UserContext;
import com.cloud.bridge.service.core.s3.S3AccessControlList;
import com.cloud.bridge.service.core.s3.S3AccessControlPolicy;
import com.cloud.bridge.service.core.s3.S3BucketPolicy;
import com.cloud.bridge.service.core.s3.S3BucketPolicy.PolicyAccess;
import com.cloud.bridge.service.core.s3.S3CanonicalUser;
import com.cloud.bridge.service.core.s3.S3CreateBucketConfiguration;
import com.cloud.bridge.service.core.s3.S3CreateBucketRequest;
import com.cloud.bridge.service.core.s3.S3CreateBucketResponse;
import com.cloud.bridge.service.core.s3.S3DeleteBucketRequest;
import com.cloud.bridge.service.core.s3.S3DeleteObjectRequest;
import com.cloud.bridge.service.core.s3.S3Engine;
import com.cloud.bridge.service.core.s3.S3GetBucketAccessControlPolicyRequest;
import com.cloud.bridge.service.core.s3.S3Grant;
import com.cloud.bridge.service.core.s3.S3ListAllMyBucketsEntry;
import com.cloud.bridge.service.core.s3.S3ListAllMyBucketsRequest;
import com.cloud.bridge.service.core.s3.S3ListAllMyBucketsResponse;
import com.cloud.bridge.service.core.s3.S3ListBucketObjectEntry;
import com.cloud.bridge.service.core.s3.S3ListBucketRequest;
import com.cloud.bridge.service.core.s3.S3ListBucketResponse;
import com.cloud.bridge.service.core.s3.S3MultipartUpload;
import com.cloud.bridge.service.core.s3.S3PolicyAction.PolicyActions;
import com.cloud.bridge.service.core.s3.S3PolicyCondition.ConditionKeys;
import com.cloud.bridge.service.core.s3.S3PolicyContext;
import com.cloud.bridge.service.core.s3.S3Response;
import com.cloud.bridge.service.core.s3.S3SetBucketAccessControlPolicyRequest;
import com.cloud.bridge.service.exception.InvalidRequestContentException;
import com.cloud.bridge.service.exception.NetworkIOException;
import com.cloud.bridge.service.exception.NoSuchObjectException;
import com.cloud.bridge.service.exception.ObjectAlreadyExistsException;
import com.cloud.bridge.service.exception.PermissionDeniedException;
import com.cloud.bridge.util.Converter;
import com.cloud.bridge.util.OrderedPair;
import com.cloud.bridge.util.PolicyParser;
import com.cloud.bridge.util.StringHelper;
import com.cloud.bridge.util.XSerializer;
import com.cloud.bridge.util.XSerializerXmlAdapter;
import com.cloud.bridge.util.XmlHelper;
import com.cloud.utils.db.Transaction;


public class S3BucketAction implements ServletAction {
    protected final static Logger logger = Logger.getLogger(S3BucketAction.class);
    @Inject BucketPolicyDao bPolicyDao;
    @Inject SBucketDao bucketDao;

    private DocumentBuilderFactory dbf = null;
    public S3BucketAction() {
        dbf = DocumentBuilderFactory.newInstance();
        dbf.setNamespaceAware( true );

    }

    @Override
    public void execute(HttpServletRequest request, HttpServletResponse response) 
            throws IOException, XMLStreamException 
            {
        String method = request.getMethod(); 
        String queryString = request.getQueryString();

        if ( method.equalsIgnoreCase("PUT")) 
        {
            if ( queryString != null && queryString.length() > 0 ) 
            {
                if ( queryString.startsWith("acl")) {
                    executePutBucketAcl(request, response);
                    return;
                } 
                else if (queryString.startsWith("versioning")) {
                    executePutBucketVersioning(request, response);
                    return;
                } 
                else if (queryString.startsWith("policy")) {
                    executePutBucketPolicy(request, response);
                    return;
                }
                else if (queryString.startsWith("logging")) {
                    executePutBucketLogging(request, response);
                    return;
                }
                else if (queryString.startsWith("website")) {
                    executePutBucketWebsite(request, response);
                    return;
                }
            }
            executePutBucket(request, response);
        } 
        else if(method.equalsIgnoreCase("GET") || method.equalsIgnoreCase("HEAD")) 
        {
            if (queryString != null && queryString.length() > 0) 
            {
                if ( queryString.startsWith("acl")) {
                    executeGetBucketAcl(request, response);
                    return;
                } 
                else if (queryString.startsWith("versioning")) {
                    executeGetBucketVersioning(request, response);
                    return;
                } 
                else if (queryString.contains("versions")) {
                    executeGetBucketObjectVersions(request, response);
                    return;
                } 
                else if (queryString.startsWith("location")) {
                    executeGetBucketLocation(request, response);
                    return;
                }
                else if (queryString.startsWith("uploads")) {
                    executeListMultipartUploads(request, response);
                    return;
                }
                else if (queryString.startsWith("policy")) {
                    executeGetBucketPolicy(request, response);
                    return;
                }
                else if (queryString.startsWith("logging")) {
                    executeGetBucketLogging(request, response);
                    return;
                } 
                else if (queryString.startsWith("website")) {
                    executeGetBucketWebsite(request, response);
                    return;
                } 
            }

            String bucketAtr = (String)request.getAttribute(S3Constants.BUCKET_ATTR_KEY);
            if ( bucketAtr.equals( "/" ))
                executeGetAllBuckets(request, response);
            else executeGetBucket(request, response);
        } 
        else if (method.equalsIgnoreCase("DELETE")) 
        {
            if (queryString != null && queryString.length() > 0) 
            {
                if ( queryString.startsWith("policy")) {
                    executeDeleteBucketPolicy(request, response);
                    return;
                }
                else if (queryString.startsWith("website")) {
                    executeDeleteBucketWebsite(request, response);
                    return;
                }

            }
            executeDeleteBucket(request, response);
        }
        else if ( (method.equalsIgnoreCase("POST")) && (queryString.equalsIgnoreCase("delete")) )
        {
            executeMultiObjectDelete(request, response);
        }
        else throw new IllegalArgumentException("Unsupported method in REST request");
            }



    private void executeMultiObjectDelete(HttpServletRequest request, HttpServletResponse response) throws IOException{

        int contentLength = request.getContentLength();
        StringBuffer  xmlDeleteResponse = null;
        boolean quite = true;

        if(contentLength > 0) 
        {
            InputStream is = null;
            String versionID =null;
            try {
                is = request.getInputStream();
                String xml = StringHelper.stringFromStream(is);
                String elements[] = {"Key","VersionId"};
                Document doc = XmlHelper.parse(xml);
                Node node = XmlHelper.getRootNode(doc);

                if(node == null) {
                    System.out.println("Invalid XML document, no root element");
                    return;
                }

                xmlDeleteResponse = new StringBuffer("<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
                        "<DeleteResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">");

                String bucket = (String)request.getAttribute(S3Constants.BUCKET_ATTR_KEY);

                S3DeleteObjectRequest engineRequest = new S3DeleteObjectRequest();
                engineRequest.setBucketName( bucket );
                is.close();

                doc.getDocumentElement().normalize();
                NodeList qList = doc.getElementsByTagName("Quiet");

                if (qList.getLength() == 1 ) {
                    Node qNode= qList.item(0);
                    if ( qNode.getFirstChild().getNodeValue().equalsIgnoreCase("true") == false )
                        quite = false;

                    logger.debug("Quite value  :" + qNode.getFirstChild().getNodeValue());
                }

                NodeList objList = doc.getElementsByTagName("Object");

                for (int i = 0; i < objList.getLength(); i++) {

                    Node key = objList.item(i);
                    NodeList key_data = key.getChildNodes();

                    if (key.getNodeType() == Node.ELEMENT_NODE) {
                        Element eElement = (Element) key;
                        String key_name = getTagValue(elements[0], eElement);
                        engineRequest.setBucketName(bucket);
                        engineRequest.setKey(key_name);

                        if (key_data.getLength() == 2) {
                            versionID = getTagValue(elements[1], eElement);
                            engineRequest.setVersion(versionID);
                        }

                        S3Response engineResponse = ServiceProvider.getInstance().getS3Engine().handleRequest( engineRequest );
                        int resultCode = engineResponse.getResultCode();
                        String resutlDesc = engineResponse.getResultDescription();
                        if(resultCode == 204) {
                            if (quite) { // show response depending on quite/verbose
                                xmlDeleteResponse.append("<Deleted><Key>"+key_name+"</Key>");
                                if (resutlDesc != null)
                                    xmlDeleteResponse.append(resutlDesc);
                                xmlDeleteResponse.append("</Deleted>");
                            }
                        }
                        else {
                            logger.debug("Error in delete ::" + key_name + " eng response:: " + engineResponse.getResultDescription());
                            xmlDeleteResponse.append("<Error><Key>"+key_name+"</Key>" );
                            if (resutlDesc != null)
                                xmlDeleteResponse.append(resutlDesc);
                            xmlDeleteResponse.append("</Error>");
                        }


                    }
                }

                String version = engineRequest.getVersion();
                if (null != version) response.addHeader( "x-amz-version-id", version );


            } catch (IOException e) {
                logger.error("Unable to read request data due to " + e.getMessage(), e);
                throw new NetworkIOException(e);

            } finally {
                if(is != null) is.close();
            }

            xmlDeleteResponse.append("</DeleteResult>");

        }
        response.setStatus(200);
        response.setContentType("text/xml; charset=UTF-8");
        S3RestServlet.endResponse(response, xmlDeleteResponse.toString());

    }

    private String getTagValue(String sTag, Element eElement) {

        NodeList nlList = eElement.getElementsByTagName(sTag).item(0).getChildNodes();
        Node nValue = nlList.item(0);
        return nValue.getNodeValue();
    }


    /** 
     * In order to support a policy on the "s3:CreateBucket" action we must be able to set and get
     * policies before a bucket is actually created.
     * 
     * @param request
     * @param response
     * @throws IOException
     */
    private void executePutBucketPolicy(HttpServletRequest request, HttpServletResponse response) throws IOException
    {
        String bucketName = (String)request.getAttribute(S3Constants.BUCKET_ATTR_KEY);
        String policy = streamToString( request.getInputStream());

        // [A] Is there an owner of an existing policy or bucket?
        SBucketVO bucket = bucketDao.getByName( bucketName );
        String owner = null;

        if ( null != bucket ) 
        {
            owner = bucket.getOwnerCanonicalId();
        }
        else 
        {    try {
            owner = bPolicyDao.getByName(bucketName).getOwnerCanonicalID();
        }
        catch( Exception e ) {}
        }


        // [B] "The bucket owner by default has permissions to attach bucket policies to their buckets using PUT Bucket policy." 
        //  -> the bucket owner may want to restrict the IP address from where this can be executed
        String client = UserContext.current().getCanonicalUserId();
        S3PolicyContext context = new S3PolicyContext(
                PolicyActions.PutBucketPolicy, bucketName);

        switch (S3Engine.verifyPolicy(context)) {
        case ALLOW:
            break;

        case DEFAULT_DENY:
            if (null != owner && !client.equals(owner)) {
                response.setStatus(405);
                return;
            }
            break;
        case DENY:
            response.setStatus(403);
            return;
        }			
        Transaction txn = Transaction.open(Transaction.AWSAPI_DB);
        // [B] Place the policy into the database over writting an existing policy
        try {
            // -> first make sure that the policy is valid by parsing it
            PolicyParser parser = new PolicyParser();
            S3BucketPolicy sbp = parser.parse( policy, bucketName );
            bPolicyDao.deletePolicy(bucketName);

            if (null != policy && !policy.isEmpty()) {
                BucketPolicyVO bpolicy = new BucketPolicyVO(bucketName, client, policy);
                bpolicy = bPolicyDao.persist(bpolicy);
                //policyDao.addPolicy( bucketName, client, policy );
            }

            if (null != sbp) ServiceProvider.getInstance().setBucketPolicy( bucketName, sbp );
            response.setStatus(200);
            txn.commit();
            txn.close();
        }
        catch( PermissionDeniedException e ) {
            logger.error("Put Bucket Policy failed due to " + e.getMessage(), e);	
            throw e; 		
        }
        catch( ParseException e ) {
            logger.error("Put Bucket Policy failed due to " + e.getMessage(), e);	
            throw new PermissionDeniedException( e.toString());		   		
        }
        catch( Exception e ) {
            logger.error("Put Bucket Policy failed due to " + e.getMessage(), e);	
            response.setStatus(500);
        }
    }

    private void executeGetBucketPolicy(HttpServletRequest request, HttpServletResponse response) 
    {
        String bucketName = (String)request.getAttribute(S3Constants.BUCKET_ATTR_KEY);

        // [A] Is there an owner of an existing policy or bucket?
        SBucketVO bucket = bucketDao.getByName(bucketName);
        String owner = null;

        if (null != bucket) {
            owner = bucket.getOwnerCanonicalId();
        } else {
            try {
                owner = bPolicyDao.getByName(bucketName).getOwnerCanonicalID();
            } catch (Exception e) {
            }
        }

        // [B]
        // "The bucket owner by default has permissions to retrieve bucket policies using GET Bucket policy."
        // -> the bucket owner may want to restrict the IP address from where
        // this can be executed
        String client = UserContext.current().getCanonicalUserId();
        S3PolicyContext context = new S3PolicyContext(
                PolicyActions.GetBucketPolicy, bucketName);
        switch (S3Engine.verifyPolicy(context)) {
        case ALLOW:
            break;

        case DEFAULT_DENY:
            if (null != owner && !client.equals(owner)) {
                response.setStatus(405);
                return;
            }
            break;

        case DENY:
            response.setStatus(403);
            return;
        }

        // [B] Pull the policy from the database if one exists
        try {
            String policy = bPolicyDao.getByName(bucketName).getPolicy();
            if (null == policy) {
                response.setStatus(404);
            } else {
                response.setStatus(200);
                response.setContentType("application/json");
                S3RestServlet.endResponse(response, policy);
            }
        } catch (Exception e) {
            logger.error("Get Bucket Policy failed due to " + e.getMessage(), e);
            response.setStatus(500);
        }
    }

    private void executeDeleteBucketPolicy(HttpServletRequest request,
            HttpServletResponse response) {
        String bucketName = (String) request
                .getAttribute(S3Constants.BUCKET_ATTR_KEY);

        SBucketVO bucket = bucketDao.getByName(bucketName);
        if (bucket != null) {
            String client = UserContext.current().getCanonicalUserId();
            if (!client.equals(bucket.getOwnerCanonicalId())) {
                response.setStatus(405);
                return;
            }
        }

        try {

            String policy = bPolicyDao.getByName(bucketName).getPolicy();
            if (null == policy) {
                response.setStatus(204);
            } else {
                ServiceProvider.getInstance().deleteBucketPolicy(bucketName);
                bPolicyDao.deletePolicy(bucketName);
                response.setStatus(200);
            }
        } catch (Exception e) {
            logger.error(
                    "Delete Bucket Policy failed due to " + e.getMessage(), e);
            response.setStatus(500);
        }
    }

    public void executeGetAllBuckets(HttpServletRequest request,
            HttpServletResponse response) throws IOException,
            XMLStreamException {
        Calendar cal = Calendar.getInstance();
        cal.set(1970, 1, 1);
        S3ListAllMyBucketsRequest engineRequest = new S3ListAllMyBucketsRequest();
        engineRequest.setAccessKey(UserContext.current().getAccessKey());
        engineRequest.setRequestTimestamp(cal);
        engineRequest.setSignature("");

        S3ListAllMyBucketsResponse engineResponse = ServiceProvider
                .getInstance().getS3Engine().handleRequest(engineRequest);

        // To allow the all buckets list to be serialized via Axiom classes
        ListAllMyBucketsResponse allBuckets = S3SerializableServiceImplementation
                .toListAllMyBucketsResponse(engineResponse);

        OutputStream outputStream = response.getOutputStream();
        response.setStatus(200);
        response.setContentType("application/xml");
        // The content-type literally should be "application/xml; charset=UTF-8"
        // but any compliant JVM supplies utf-8 by default

        // MTOMAwareResultStreamWriter resultWriter = new
        // MTOMAwareResultStreamWriter ("ListAllMyBucketsResult", outputStream
        // );
        // resultWriter.startWrite();
        // resultWriter.writeout(allBuckets);
        // resultWriter.stopWrite();
        StringBuffer xml = new StringBuffer();
        xml.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
        xml.append("<ListAllMyBucketsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">");
        xml.append("<Owner><ID>");
        xml.append(engineResponse.getOwner().getID()).append("</ID>");
        xml.append("<DisplayName>")
        .append(engineResponse.getOwner().getDisplayName())
        .append("</DisplayName>");
        xml.append("</Owner>").append("<Buckets>");
        SimpleDateFormat sdf = new SimpleDateFormat(
                "yyyy-MM-dd'T'HH:mm:ss.SSSZ");
        for (S3ListAllMyBucketsEntry entry : engineResponse.getBuckets()) {
            xml.append("<Bucket>").append("<Name>").append(entry.getName())
            .append("</Name>");
            xml.append("<CreationDate>")
            .append(sdf.format(entry.getCreationDate().getTime()))
            .append("</CreationDate>");
            xml.append("</Bucket>");
        }
        xml.append("</Buckets>").append("</ListAllMyBucketsResult>");
        response.setStatus(200);
        response.setContentType("text/xml; charset=UTF-8");
        S3RestServlet.endResponse(response, xml.toString());

    }

    public void executeGetBucket(HttpServletRequest request, HttpServletResponse response) 
            throws IOException, XMLStreamException 
            {
        S3ListBucketRequest engineRequest = new S3ListBucketRequest();
        engineRequest.setBucketName((String) request
                .getAttribute(S3Constants.BUCKET_ATTR_KEY));
        engineRequest.setDelimiter(request.getParameter("delimiter"));
        engineRequest.setMarker(request.getParameter("marker"));
        engineRequest.setPrefix(request.getParameter("prefix"));

        int maxKeys = Converter.toInt(request.getParameter("max-keys"), 1000);
        engineRequest.setMaxKeys(maxKeys);
        try {
            S3ListBucketResponse engineResponse = ServiceProvider.getInstance()
                    .getS3Engine().listBucketContents(engineRequest, false);

            // To allow the all list buckets result to be serialized via Axiom
            // classes
            ListBucketResponse oneBucket = S3SerializableServiceImplementation
                    .toListBucketResponse(engineResponse);

            OutputStream outputStream = response.getOutputStream();
            response.setStatus(200);
            response.setContentType("application/xml");
            // The content-type literally should be
            // "application/xml; charset=UTF-8"
            // but any compliant JVM supplies utf-8 by default;

            MTOMAwareResultStreamWriter resultWriter = new MTOMAwareResultStreamWriter(
                    "ListBucketResult", outputStream);
            resultWriter.startWrite();
            resultWriter.writeout(oneBucket);
            resultWriter.stopWrite();
        } catch (NoSuchObjectException nsoe) {
            response.setStatus(404);
            response.setContentType("application/xml");

            StringBuffer xmlError = new StringBuffer();
            xmlError.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>")
            .append("<Error><Code>NoSuchBucket</Code><Message>The specified bucket does not exist</Message>")
            .append("<BucketName>")
            .append((String) request
                    .getAttribute(S3Constants.BUCKET_ATTR_KEY))
                    .append("</BucketName>")
                    .append("<RequestId>1DEADBEEF9</RequestId>") // TODO
                    .append("<HostId>abCdeFgHiJ1k2LmN3op4q56r7st89</HostId>") // TODO
                    .append("</Error>");
            S3RestServlet.endResponse(response, xmlError.toString());

        }

            }

    public void executeGetBucketAcl(HttpServletRequest request, HttpServletResponse response) 
            throws IOException, XMLStreamException 
            {
        S3GetBucketAccessControlPolicyRequest engineRequest = new S3GetBucketAccessControlPolicyRequest();
        Calendar cal = Calendar.getInstance();
        cal.set( 1970, 1, 1 ); 
        engineRequest.setAccessKey(UserContext.current().getAccessKey());
        engineRequest.setRequestTimestamp( cal );
        engineRequest.setSignature( "" );   // TODO - Consider providing signature in a future release which allows additional user description
        engineRequest.setBucketName((String)request.getAttribute(S3Constants.BUCKET_ATTR_KEY));

        S3AccessControlPolicy engineResponse = ServiceProvider.getInstance().getS3Engine().handleRequest(engineRequest);

        // To allow the bucket acl policy result to be serialized via Axiom classes
        GetBucketAccessControlPolicyResponse onePolicy = S3SerializableServiceImplementation.toGetBucketAccessControlPolicyResponse( engineResponse );

        OutputStream outputStream = response.getOutputStream();
        response.setStatus(200);	
        response.setContentType("application/xml");   
        // The content-type literally should be "application/xml; charset=UTF-8" 
        // but any compliant JVM supplies utf-8 by default;

        MTOMAwareResultStreamWriter resultWriter = new MTOMAwareResultStreamWriter ("GetBucketAccessControlPolicyResult", outputStream );
        resultWriter.startWrite();
        resultWriter.writeout(onePolicy);
        resultWriter.stopWrite();


            }

    public void executeGetBucketVersioning(HttpServletRequest request, HttpServletResponse response) throws IOException
    {
        // [A] Does the bucket exist?
        String bucketName = (String)request.getAttribute(S3Constants.BUCKET_ATTR_KEY);
        String versioningStatus = null;

        if (null == bucketName) {
            logger.error( "executeGetBucketVersioning - no bucket name given" );
            response.setStatus( 400 ); 
            return; 
        }

        SBucketVO sbucket = bucketDao.getByName( bucketName );
        if (sbucket == null) {
            response.setStatus( 404 );
            return;
        }

        // [B] The owner may want to restrict the IP address at which this can be performed
        String client = UserContext.current().getCanonicalUserId();
        if (!client.equals( sbucket.getOwnerCanonicalId()))
            throw new PermissionDeniedException( "Access Denied - only the owner can read bucket versioning" );

        S3PolicyContext context = new S3PolicyContext( PolicyActions.GetBucketVersioning, bucketName );
        if (PolicyAccess.DENY == S3Engine.verifyPolicy( context )) {
            response.setStatus(403);
            return;
        }


        // [C]
        switch( sbucket.getVersioningStatus()) {
        default:
        case 0: versioningStatus = "";           break;
        case 1: versioningStatus = "Enabled";    break;
        case 2: versioningStatus = "Suspended";  break;
        }

        StringBuffer xml = new StringBuffer();
        xml.append( "<?xml version=\"1.0\" encoding=\"utf-8\"?>" );
        xml.append( "<VersioningConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">" );
        if (0 < versioningStatus.length()) xml.append( "<Status>" ).append( versioningStatus ).append( "</Status>" );
        xml.append( "</VersioningConfiguration>" );

        response.setStatus(200);
        response.setContentType("text/xml; charset=UTF-8");
        S3RestServlet.endResponse(response, xml.toString());
    }

    public void executeGetBucketObjectVersions(HttpServletRequest request, HttpServletResponse response) throws IOException
    {   
        S3ListBucketRequest engineRequest = new S3ListBucketRequest();
        String keyMarker       = request.getParameter("key-marker");
        String versionIdMarker = request.getParameter("version-id-marker");

        engineRequest.setBucketName((String)request.getAttribute(S3Constants.BUCKET_ATTR_KEY));
        engineRequest.setDelimiter(request.getParameter("delimiter"));
        engineRequest.setMarker( keyMarker );  
        engineRequest.setPrefix(request.getParameter("prefix"));
        engineRequest.setVersionIdMarker( versionIdMarker );

        int maxKeys = Converter.toInt(request.getParameter("max-keys"), 1000);
        engineRequest.setMaxKeys(maxKeys);
        S3ListBucketResponse engineResponse = ServiceProvider.getInstance().getS3Engine().listBucketContents( engineRequest, true );

        // -> the SOAP version produces different XML
        StringBuffer xml = new StringBuffer();
        xml.append( "<?xml version=\"1.0\" encoding=\"utf-8\"?>" );
        xml.append( "<ListVersionsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">" );
        xml.append( "<Name>" ).append( engineResponse.getBucketName()).append( "</Name>" );

        if ( null == keyMarker )
            xml.append( "<KeyMarker/>" );
        else xml.append( "<KeyMarker>" ).append( keyMarker ).append( "</KeyMarker" ); 

        if ( null == versionIdMarker )
            xml.append( "<VersionIdMarker/>" );
        else xml.append( "<VersionIdMarker>" ).append( keyMarker ).append( "</VersionIdMarker" ); 

        xml.append( "<MaxKeys>" ).append( engineResponse.getMaxKeys()).append( "</MaxKeys>" );
        xml.append( "<IsTruncated>" ).append( engineResponse.isTruncated()).append( "</IsTruncated>" );

        S3ListBucketObjectEntry[] versions = engineResponse.getContents();
        for( int i=0; null != versions && i < versions.length; i++ )
        {
            S3CanonicalUser owner    = versions[i].getOwner();
            boolean isDeletionMarker = versions[i].getIsDeletionMarker();
            String displayName       = owner.getDisplayName();
            String id                = owner.getID();

            if ( isDeletionMarker ) 
            { 	  
                xml.append( "<DeleteMarker>" );  	 
                xml.append( "<Key>" ).append( versions[i].getKey()).append( "</Key>" );
                xml.append( "<VersionId>" ).append( versions[i].getVersion()).append( "</VersionId>" );
                xml.append( "<IsLatest>" ).append( versions[i].getIsLatest()).append( "</IsLatest>" );
                xml.append( "<LastModified>" ).append( DatatypeConverter.printDateTime( versions[i].getLastModified())).append( "</LastModified>" );
            }
            else
            { 	  xml.append( "<Version>" );  	 
            xml.append( "<Key>" ).append( versions[i].getKey()).append( "</Key>" );
            xml.append( "<VersionId>" ).append( versions[i].getVersion()).append( "</VersionId>" );
            xml.append( "<IsLatest>" ).append( versions[i].getIsLatest()).append( "</IsLatest>" );
            xml.append( "<LastModified>" ).append( DatatypeConverter.printDateTime( versions[i].getLastModified())).append( "</LastModified>" );
            xml.append( "<ETag>" ).append( versions[i].getETag()).append( "</ETag>" );
            xml.append( "<Size>" ).append( versions[i].getSize()).append( "</Size>" );
            xml.append( "<StorageClass>" ).append( versions[i].getStorageClass()).append( "</StorageClass>" );
            }

            xml.append( "<Owner>" );
            xml.append( "<ID>" ).append( id ).append( "</ID>" );
            if ( null == displayName )
                xml.append( "<DisplayName/>" );
            else xml.append( "<DisplayName>" ).append( owner.getDisplayName()).append( "</DisplayName>" );
            xml.append( "</Owner>" );

            if ( isDeletionMarker )
                xml.append( "</DeleteMarker>" ); 
            else xml.append( "</Version>" );
        }
        xml.append( "</ListVersionsResult>" );

        response.setStatus(200);
        response.setContentType("text/xml; charset=UTF-8");
        S3RestServlet.endResponse(response, xml.toString());
    }

    public void executeGetBucketLogging(HttpServletRequest request, HttpServletResponse response) throws IOException {
        // TODO -- Review this in future.  Currently this is a beta feature of S3
        response.setStatus(405);
    }

    public void executeGetBucketLocation(HttpServletRequest request, HttpServletResponse response) throws IOException {
        // TODO - This is a fakery! We don't actually store location in backend
        StringBuffer xml = new StringBuffer();
        xml.append( "<?xml version=\"1.0\" encoding=\"utf-8\"?>" );
        xml.append( "<LocationConstraint xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">" );
        // This is the real fakery
        xml.append( "us-west-2" );
        xml.append( "</LocationConstraint>" );
        response.setStatus(200);
        response.setContentType("text/xml; charset=UTF-8");
        S3RestServlet.endResponse(response, xml.toString());
    }

    public void executeGetBucketWebsite(HttpServletRequest request, HttpServletResponse response) throws IOException {
        response.setStatus(405);
    }

    public void executeDeleteBucketWebsite(HttpServletRequest request, HttpServletResponse response) throws IOException {
        response.setStatus(405);
    }

    public void executePutBucket(HttpServletRequest request, HttpServletResponse response) throws IOException 
    {
        int contentLength = request.getContentLength();
        Object objectInContent = null;

        if(contentLength > 0) 
        {
            InputStream is = null;
            try {
                is = request.getInputStream();
                String xml = StringHelper.stringFromStream(is);
                Class.forName("com.cloud.bridge.service.core.s3.S3CreateBucketConfiguration");
                XSerializer serializer = new XSerializer(new XSerializerXmlAdapter()); 
                objectInContent = serializer.serializeFrom(xml);
                if(objectInContent != null && !(objectInContent instanceof S3CreateBucketConfiguration)) {
                    throw new InvalidRequestContentException("Invalid request content in create-bucket: " + xml);
                }
                is.close();

            } catch (IOException e) {
                logger.error("Unable to read request data due to " + e.getMessage(), e);
                throw new NetworkIOException(e);

            }  catch (ClassNotFoundException e) {
                logger.error("In a normal world this should never never happen:" + e.getMessage(), e);
                throw new RuntimeException("A required class was not found in the classpath:" + e.getMessage());
            }
            finally {
                if(is != null) is.close();
            }
        }

        S3CreateBucketRequest engineRequest = new S3CreateBucketRequest();
        engineRequest.setBucketName((String)request.getAttribute(S3Constants.BUCKET_ATTR_KEY));
        engineRequest.setConfig((S3CreateBucketConfiguration)objectInContent);
        try {
            S3CreateBucketResponse engineResponse = ServiceProvider.getInstance().getS3Engine().handleRequest(engineRequest);
            response.addHeader("Location", "/" + engineResponse.getBucketName());
            response.setContentLength(0);
            response.setStatus(200);
            response.flushBuffer();
        } catch (ObjectAlreadyExistsException oaee) {
            response.setStatus(409);
            String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?> <Error><Code>OperationAborted</Code><Message>A conflicting conditional operation is currently in progress against this resource. Please try again..</Message>";
            response.setContentType("text/xml; charset=UTF-8");
            S3RestServlet.endResponse(response, xml.toString());
        }
    }

    public void executePutBucketAcl(HttpServletRequest request, HttpServletResponse response) throws IOException 
    { 
        // [A] Determine that there is an applicable bucket which might have an ACL set

        String bucketName = (String) request
                .getAttribute(S3Constants.BUCKET_ATTR_KEY);
        SBucketVO bucket = bucketDao.getByName(bucketName);
        String owner = null;
        if (null != bucket)
            owner = bucket.getOwnerCanonicalId();
        if (null == owner) {
            logger.error("ACL update failed since " + bucketName
                    + " does not exist");
            throw new IOException("ACL update failed");
        }

        // [B] Obtain the grant request which applies to the acl request string.
        // This latter is supplied as the value of the x-amz-acl header.

        S3SetBucketAccessControlPolicyRequest engineRequest = new S3SetBucketAccessControlPolicyRequest();
        S3Grant grantRequest = new S3Grant();
        S3AccessControlList aclRequest = new S3AccessControlList();

        String aclRequestString = request.getHeader("x-amz-acl");
        OrderedPair<Integer, Integer> accessControlsForBucketOwner = SAclVO.getCannedAccessControls(aclRequestString, "SBucket");
        grantRequest.setPermission(accessControlsForBucketOwner.getFirst());
        grantRequest.setGrantee(accessControlsForBucketOwner.getSecond());
        grantRequest.setCanonicalUserID(owner);
        aclRequest.addGrant(grantRequest);
        engineRequest.setAcl(aclRequest);
        engineRequest.setBucketName(bucketName);

        // [C] Allow an S3Engine to handle the
        // S3SetBucketAccessControlPolicyRequest
        S3Response engineResponse = ServiceProvider.getInstance().getS3Engine()
                .handleRequest(engineRequest);
        response.setStatus(engineResponse.getResultCode());

    }

    public void executePutBucketVersioning(HttpServletRequest request, HttpServletResponse response) throws IOException 
    {
        String bucketName = (String) request.getAttribute(S3Constants.BUCKET_ATTR_KEY);
        String versioningStatus = null;
        Node item = null;

        if (null == bucketName) {
            logger.error("executePutBucketVersioning - no bucket name given");
            response.setStatus(400);
            return;
        }

        // -> is the XML as defined?
        try {
            DocumentBuilder db = dbf.newDocumentBuilder();
            Document restXML = db.parse(request.getInputStream());
            NodeList match = S3RestServlet.getElement(restXML,
                    "http://s3.amazonaws.com/doc/2006-03-01/", "Status");
            if (0 < match.getLength()) {
                item = match.item(0);
                versioningStatus = new String(item.getFirstChild()
                        .getNodeValue());
            } else {
                logger.error("executePutBucketVersioning - cannot find Status tag in XML body");
                response.setStatus(400);
                return;
            }
        } catch (Exception e) {
            logger.error(
                    "executePutBucketVersioning - failed to parse XML due to "
                            + e.getMessage(), e);
            response.setStatus(400);
            return;
        }

        try {
            // Irrespective of what the ACLs say only the owner can turn on
            // versioning on a bucket.
            // The bucket owner may want to restrict the IP address from which
            // this can occur.

            SBucketVO sbucket = bucketDao.getByName(bucketName);

            String client = UserContext.current().getCanonicalUserId();
            if (!client.equals(sbucket.getOwnerCanonicalId()))
                throw new PermissionDeniedException(
                        "Access Denied - only the owner can turn on versioing on a bucket");

            S3PolicyContext context = new S3PolicyContext(
                    PolicyActions.PutBucketVersioning, bucketName);
            if (PolicyAccess.DENY == S3Engine.verifyPolicy(context)) {
                response.setStatus(403);
                return;
            }

            if (versioningStatus.equalsIgnoreCase("Enabled"))
                sbucket.setVersioningStatus(1);
            else if (versioningStatus.equalsIgnoreCase("Suspended"))
                sbucket.setVersioningStatus(2);
            else {
                logger.error("executePutBucketVersioning - unknown state: ["
                        + versioningStatus + "]");
                response.setStatus(400);
                return;
            }
            bucketDao.update(sbucket.getId(), sbucket);

        } catch (PermissionDeniedException e) {
            logger.error(
                    "executePutBucketVersioning - failed due to "
                            + e.getMessage(), e);
            throw e;

        } catch (Exception e) {
            logger.error(
                    "executePutBucketVersioning - failed due to "
                            + e.getMessage(), e);
            response.setStatus(500);
            return;
        }
        response.setStatus(200);
    }

    public void executePutBucketLogging(HttpServletRequest request, HttpServletResponse response) throws IOException {
        // TODO -- Review this in future.  Currently this is a S3 beta feature
        response.setStatus(501);
    }

    public void executePutBucketWebsite(HttpServletRequest request, HttpServletResponse response) throws IOException {
        // TODO -- LoPri - Undertake checks on Put Bucket Website
        // Tested using configuration <Directory /Users/john1/S3-Mount>\nAllowOverride FileInfo AuthConfig Limit...</Directory> in httpd.conf
        // 	Need some way of using  AllowOverride to allow use of .htaccess and then pushing .httaccess file to bucket subdirectory of mount point
        // Currently has noop effect in the sense that a running apachectl process sees the directory contents without further action
        response.setStatus(200);
    }

    public void executeDeleteBucket(HttpServletRequest request, HttpServletResponse response) throws IOException 
    {
        S3DeleteBucketRequest engineRequest = new S3DeleteBucketRequest();
        engineRequest.setBucketName((String)request.getAttribute(S3Constants.BUCKET_ATTR_KEY));
        S3Response engineResponse = ServiceProvider.getInstance().getS3Engine().handleRequest(engineRequest);  
        response.setStatus(engineResponse.getResultCode());
        response.flushBuffer();
    }

    /**
     * Multipart upload is a complex operation with all the options defined by Amazon.   Part of the functionality is 
     * provided by the query done against the database.  The CommonPrefixes functionality is done the same way 
     * as done in the listBucketContents function (i.e., by iterating though the list to decide which output 
     * element each key is placed).
     * 
     * @param request
     * @param response
     * @throws IOException
     */
    public void executeListMultipartUploads(HttpServletRequest request, HttpServletResponse response) throws IOException 
    {
        // [A] Obtain parameters and do basic bucket verification
        String bucketName = (String) request
                .getAttribute(S3Constants.BUCKET_ATTR_KEY);
        String delimiter = request.getParameter("delimiter");
        String keyMarker = request.getParameter("key-marker");
        String prefix = request.getParameter("prefix");
        int maxUploads = 1000;
        int nextUploadId = 0;
        String nextKey = null;
        boolean isTruncated = false;
        S3MultipartUpload[] uploads = null;
        S3MultipartUpload onePart = null;
        String temp = request.getParameter("max-uploads");
        if (null != temp) {
            maxUploads = Integer.parseInt(temp);
            if (maxUploads > 1000 || maxUploads < 0)
                maxUploads = 1000;
        }

        // -> upload-id-marker is ignored unless key-marker is also specified
        String uploadIdMarker = request.getParameter("upload-id-marker");
        if (null == keyMarker)
            uploadIdMarker = null;

        // -> does the bucket exist, we may need it to verify access permissions
        SBucketVO bucket = bucketDao.getByName(bucketName);
        if (bucket == null) {
            logger.error("listMultipartUpload failed since " + bucketName
                    + " does not exist");
            response.setStatus(404);
            return;
        }

        S3PolicyContext context = new S3PolicyContext(
                PolicyActions.ListBucketMultipartUploads, bucketName);
        context.setEvalParam(ConditionKeys.Prefix, prefix);
        context.setEvalParam(ConditionKeys.Delimiter, delimiter);
        S3Engine.verifyAccess(context, "SBucket", bucket.getId(),
                SAcl.PERMISSION_READ);

        // [B] Query the multipart table to get the list of current uploads
        try {
            MultipartLoadDao uploadDao = new MultipartLoadDao();
            OrderedPair<S3MultipartUpload[], Boolean> result = uploadDao
                    .getInitiatedUploads(bucketName, maxUploads, prefix,
                            keyMarker, uploadIdMarker);
            uploads = result.getFirst();
            isTruncated = result.getSecond().booleanValue();
        } catch (Exception e) {
            logger.error(
                    "List Multipart Uploads failed due to " + e.getMessage(), e);
            response.setStatus(500);
        }

        StringBuffer xml = new StringBuffer();
        xml.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
        xml.append("<ListMultipartUploadsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">");
        xml.append("<Bucket>").append(bucketName).append("</Bucket>");
        xml.append("<KeyMarker>").append((null == keyMarker ? "" : keyMarker))
        .append("</KeyMarker>");
        xml.append("<UploadIdMarker>")
        .append((null == uploadIdMarker ? "" : uploadIdMarker))
        .append("</UploadIdMarker>");

        // [C] Construct the contents of the <Upload> element
        StringBuffer partsList = new StringBuffer();
        for (int i = 0; i < uploads.length; i++) {
            onePart = uploads[i];
            if (null == onePart)
                break;

            if (delimiter != null && !delimiter.isEmpty()) {
                // -> is this available only in the CommonPrefixes element?
                if (StringHelper.substringInBetween(onePart.getKey(), prefix,
                        delimiter) != null)
                    continue;
            }

            nextKey = onePart.getKey();
            nextUploadId = onePart.getId();
            partsList.append("<Upload>");
            partsList.append("<Key>").append(nextKey).append("</Key>");
            partsList.append("<UploadId>").append(nextUploadId)
            .append("</UploadId>");
            partsList.append("<Initiator>");
            partsList.append("<ID>").append(onePart.getAccessKey())
            .append("</ID>");
            partsList.append("<DisplayName></DisplayName>");
            partsList.append("</Initiator>");
            partsList.append("<Owner>");
            partsList.append("<ID>").append(onePart.getAccessKey())
            .append("</ID>");
            partsList.append("<DisplayName></DisplayName>");
            partsList.append("</Owner>");
            partsList.append("<StorageClass>STANDARD</StorageClass>");
            partsList
            .append("<Initiated>")
            .append(DatatypeConverter.printDateTime(onePart
                    .getLastModified())).append("</Initiated>");
            partsList.append("</Upload>");
        }

        // [D] Construct the contents of the <CommonPrefixes> elements (if any)
        for (int i = 0; i < uploads.length; i++) {
            onePart = uploads[i];
            if (null == onePart)
                break;

            if (delimiter != null && !delimiter.isEmpty()) {
                String subName = StringHelper.substringInBetween(
                        onePart.getKey(), prefix, delimiter);
                if (subName != null) {
                    partsList.append("<CommonPrefixes>");
                    partsList.append("<Prefix>");
                    if (prefix != null && prefix.length() > 0)
                        partsList.append(prefix + delimiter + subName);
                    else
                        partsList.append(subName);
                    partsList.append("</Prefix>");
                    partsList.append("</CommonPrefixes>");
                }
            }
        }

        // [D] Finish off the response
        xml.append("<NextKeyMarker>").append((null == nextKey ? "" : nextKey))
        .append("</NextKeyMarker>");
        xml.append("<NextUploadIdMarker>")
        .append((0 == nextUploadId ? "" : nextUploadId))
        .append("</NextUploadIdMarker>");
        xml.append("<MaxUploads>").append(maxUploads).append("</MaxUploads>");
        xml.append("<IsTruncated>").append(isTruncated)
        .append("</IsTruncated>");

        xml.append(partsList.toString());
        xml.append("</ListMultipartUploadsResult>");

        response.setStatus(200);
        response.setContentType("text/xml; charset=UTF-8");
        S3RestServlet.endResponse(response, xml.toString());
    }

    private String streamToString( InputStream is ) throws IOException 
    {
        int n = 0;

        if ( null != is ) 
        {
            Writer writer = new StringWriter();
            char[] buffer = new char[1024];
            try {
                Reader reader = new BufferedReader( new InputStreamReader(is, "UTF-8"));
                while ((n = reader.read(buffer)) != -1) writer.write(buffer, 0, n);
            } 
            finally {
                is.close();
            }
            return writer.toString();	        
        } 
        else return null;       
    }
}
