package org.jgroups.protocols;

import org.jgroups.Address;
import org.jgroups.annotations.Experimental;
import org.jgroups.annotations.Property;
import org.jgroups.annotations.Unsupported;
import org.jgroups.util.Util;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLReaderFactory;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;

import static java.lang.String.valueOf;


/**
 * Discovery protocol using Amazon's S3 storage. The S3 access code reuses the example shipped by Amazon.
 * This protocol is unsupported and experimental !
 * @author Bela Ban
 */
public class S3_PING extends FILE_PING {

    @Property(description="The access key to AWS (S3)")
    protected String access_key=null;

    @Property(description="The secret access key to AWS (S3)")
    protected String secret_access_key=null;

    @Property(description="When non-null, we set location to prefix-UUID")
    protected String prefix=null;

    @Property(description="When non-null, we use this pre-signed URL for PUTs")
    protected String pre_signed_put_url=null;

    @Property(description="When non-null, we use this pre-signed URL for DELETEs")
    protected String pre_signed_delete_url=null;

    protected AWSAuthConnection conn=null;


  
    public void init() throws Exception {
        super.init();
        //if(access_key == null || secret_access_key == null)
          //  throw new IllegalArgumentException("access_key and secret_access_key must be non-null");

        validateProperties();

        conn=new AWSAuthConnection(access_key, secret_access_key);

        if(prefix != null && prefix.length() > 0) {
            ListAllMyBucketsResponse bucket_list=conn.listAllMyBuckets(null);
            List buckets=bucket_list.entries;
            if(buckets != null) {
                boolean found=false;
                for(Object tmp: buckets) {
                    if(tmp instanceof Bucket) {
                        Bucket bucket=(Bucket)tmp;
                        if(bucket.name.startsWith(prefix)) {
                            location=bucket.name;
                            found=true;
                        }
                    }
                }
                if(!found) {
                    location=prefix + "-" + java.util.UUID.randomUUID().toString();
                }
            }
        }

        if(usingPreSignedUrls()) {
            PreSignedUrlParser parsedPut = new PreSignedUrlParser(pre_signed_put_url);
            location = parsedPut.getBucket();
        }

        if(!conn.checkBucketExists(location)) {
            conn.createBucket(location, AWSAuthConnection.LOCATION_DEFAULT, null).connection.getResponseMessage();
        }

        Runtime.getRuntime().addShutdownHook(new Thread() {
            public void run() {
                remove(group_addr, local_addr);
            }
        });
    }

    protected void createRootDir() {
        ; // do *not* create root file system (don't remove !)
    }

    protected List<PingData> readAll(String clustername) {
        if(clustername == null)
            return null;

        List<PingData> retval=new ArrayList<PingData>();
        try {
            if (usingPreSignedUrls()) {
                PreSignedUrlParser parsedPut = new PreSignedUrlParser(pre_signed_put_url);
                clustername = parsedPut.getPrefix();
            }
            ListBucketResponse rsp=conn.listBucket(location, sanitize(clustername), null, null, null);
            if(rsp.entries != null) {
                for(Iterator<ListEntry> it=rsp.entries.iterator(); it.hasNext();) {
                    ListEntry key=it.next();
                    GetResponse val=conn.get(location, key.key, null);
                    if(val.object != null) {
                        byte[] buf=val.object.data;
                        if(buf != null && buf.length > 0) {
                            try {
                                PingData data=(PingData)Util.objectFromByteBuffer(buf);
                                retval.add(data);
                            }
                            catch(Throwable e) {
                                log.error("failed marshalling buffer to address", e);
                            }
                        }
                    }
                }
            }

            return retval;
        }
        catch(IOException ex) {
            log.error("failed reading addresses", ex);
            return retval;
        }
    }


    protected void writeToFile(PingData data, String clustername) {
        if(clustername == null || data == null)
            return;
        String filename=local_addr instanceof org.jgroups.util.UUID? ((org.jgroups.util.UUID)local_addr).toStringLong() : local_addr.toString();
        String key=sanitize(clustername) + "/" + sanitize(filename);
        try {
            byte[] buf=Util.objectToByteBuffer(data);
            S3Object val=new S3Object(buf, null);

            if (usingPreSignedUrls()) {
                Map headers = new TreeMap();
                headers.put("x-amz-acl", Arrays.asList("public-read"));
                conn.put(pre_signed_put_url, val, headers).connection.getResponseMessage();
            } else {
                Map headers=new TreeMap();
                headers.put("Content-Type", Arrays.asList("text/plain"));
                conn.put(location, key, val, headers).connection.getResponseMessage();
            }
        }
        catch(Exception e) {
            log.error("failed marshalling " + data + " to buffer", e);
        }
    }


    protected void remove(String clustername, Address addr) {
        if(clustername == null || addr == null)
            return;
        String filename=addr instanceof org.jgroups.util.UUID? ((org.jgroups.util.UUID)addr).toStringLong() : addr.toString();
        String key=sanitize(clustername) + "/" + sanitize(filename);
        try {
            Map headers=new TreeMap();
            headers.put("Content-Type", Arrays.asList("text/plain"));
            if (usingPreSignedUrls()) {
                conn.delete(pre_signed_delete_url).connection.getResponseMessage();
            } else {
                conn.delete(location, key, headers).connection.getResponseMessage();
            }
            if(log.isTraceEnabled())
                log.trace("removing " + location + "/" + key);
        }
        catch(Exception e) {
            log.error("failure removing data", e);
        }
    }


    protected void validateProperties() {
        if (pre_signed_put_url != null && pre_signed_delete_url != null) {
            PreSignedUrlParser parsedPut = new PreSignedUrlParser(pre_signed_put_url);
            PreSignedUrlParser parsedDelete = new PreSignedUrlParser(pre_signed_delete_url);
            if (!parsedPut.getBucket().equals(parsedDelete.getBucket()) ||
                    !parsedPut.getPrefix().equals(parsedDelete.getPrefix())) {
                throw new IllegalArgumentException("pre_signed_put_url and pre_signed_delete_url must have the same path");
            }
        } else if (pre_signed_put_url != null || pre_signed_delete_url != null) {
            throw new IllegalArgumentException("pre_signed_put_url and pre_signed_delete_url must both be set or both unset");
        }
    }
    
    protected boolean usingPreSignedUrls() {
        return pre_signed_put_url != null;
    }


    /** Sanitizes bucket and folder names according to AWS guidelines */
    protected static String sanitize(final String name) {
        String retval=name;
        retval=retval.replace('/', '-');
        retval=retval.replace('\\', '-');
        return retval;
    }


    /**
     * Use this helper method to generate pre-signed S3 urls for use with S3_PING.
     * You'll need to generate urls for both the put and delete http methods.
     * Example:
     * Your AWS Access Key is "abcd".
     * Your AWS Secret Access Key is "efgh".
     * You want this node to write its information to "/S3_PING/DemoCluster/node1".
     * So, your bucket is "S3_PING" and your key is "DemoCluster/node1".
     * You want this to expire one year from now, or
     *   (System.currentTimeMillis / 1000) + (60 * 60 * 24 * 365)
     *   Let's assume that this equals 1316286684
     * 
     * Here's how to generate the value for the pre_signed_put_url property:
     * String putUrl = S3_PING.generatePreSignedUrl("abcd", "efgh", "put",
     *                                              "S3_Ping", "DemoCluster/node1",
     *                                              1316286684);
     *                                              
     * Here's how to generate the value for the pre_signed_delete_url property:
     * String deleteUrl = S3_PING.generatePreSignedUrl("abcd", "efgh", "delete",
     *                                                 "S3_Ping", "DemoCluster/node1",
     *                                                 1316286684);
     * 
     * @param awsAccessKey Your AWS Access Key
     * @param awsSecretAccessKey Your AWS Secret Access Key
     * @param method The HTTP method - use "put" or "delete" for use with S3_PING
     * @param bucket The S3 bucket you want to write to
     * @param key The key within the bucket to write to
     * @param expirationDate The date this pre-signed url should expire, in seconds since epoch
     * @return The pre-signed url to be used in pre_signed_put_url or pre_signed_delete_url properties
     */
    public static String generatePreSignedUrl(String awsAccessKey, String awsSecretAccessKey, String method,
                                       String bucket, String key, long expirationDate) {
        Map headers = new HashMap();
        if (method.equalsIgnoreCase("PUT")) {
            headers.put("x-amz-acl", Arrays.asList("public-read"));
        }
        return Utils.generateQueryStringAuthentication(awsAccessKey, awsSecretAccessKey, method,
                                                       bucket, key, new HashMap(), headers,
                                                       expirationDate);
    }



    /**
     * Utility class to parse S3 pre-signed URLs
     */
    static class PreSignedUrlParser {
        String bucket = "";
        String prefix = "";

        public PreSignedUrlParser(String preSignedUrl) {
            try {
                URL url = new URL(preSignedUrl);
                String path = url.getPath();
                String[] pathParts = path.split("/");
                
                if (pathParts.length < 3) {
                    throw new IllegalArgumentException("pre-signed url " + preSignedUrl + " must point to a file within a bucket");
                }
                if (pathParts.length > 4) {
                    throw new IllegalArgumentException("pre-signed url " + preSignedUrl + " may only have only subdirectory under a bucket");
                }
                this.bucket = pathParts[1];
                if (pathParts.length > 3) {
                    this.prefix = pathParts[2];
                }
            } catch (MalformedURLException ex) {
                throw new IllegalArgumentException("pre-signed url " + preSignedUrl + " is not a valid url");
            }
        }

        public String getBucket() {
            return bucket;
        }
        
        public String getPrefix() {
            return prefix;
        }
    }

    


    /**
     * The following classes have been copied from Amazon's sample code
     */
    static class AWSAuthConnection {
        public static final String LOCATION_DEFAULT=null;
        public static final String LOCATION_EU="EU";

        private String awsAccessKeyId;
        private String awsSecretAccessKey;
        private boolean isSecure;
        private String server;
        private int port;
        private CallingFormat callingFormat;

        public AWSAuthConnection(String awsAccessKeyId, String awsSecretAccessKey) {
            this(awsAccessKeyId, awsSecretAccessKey, true);
        }

        public AWSAuthConnection(String awsAccessKeyId, String awsSecretAccessKey, boolean isSecure) {
            this(awsAccessKeyId, awsSecretAccessKey, isSecure, Utils.DEFAULT_HOST);
        }

        public AWSAuthConnection(String awsAccessKeyId, String awsSecretAccessKey, boolean isSecure,
                                 String server) {
            this(awsAccessKeyId, awsSecretAccessKey, isSecure, server,
                 isSecure? Utils.SECURE_PORT : Utils.INSECURE_PORT);
        }

        public AWSAuthConnection(String awsAccessKeyId, String awsSecretAccessKey, boolean isSecure,
                                 String server, int port) {
            this(awsAccessKeyId, awsSecretAccessKey, isSecure, server, port, CallingFormat.getSubdomainCallingFormat());

        }

        public AWSAuthConnection(String awsAccessKeyId, String awsSecretAccessKey, boolean isSecure,
                                 String server, CallingFormat format) {
            this(awsAccessKeyId, awsSecretAccessKey, isSecure, server,
                 isSecure? Utils.SECURE_PORT : Utils.INSECURE_PORT,
                 format);
        }

        /**
         * Create a new interface to interact with S3 with the given credential and connection
         * parameters
         * @param awsAccessKeyId     Your user key into AWS
         * @param awsSecretAccessKey The secret string used to generate signatures for authentication.
         * @param isSecure           use SSL encryption
         * @param server             Which host to connect to.  Usually, this will be s3.amazonaws.com
         * @param port               Which port to use.
         * @param format             Type of request Regular/Vanity or Pure Vanity domain
         */
        public AWSAuthConnection(String awsAccessKeyId, String awsSecretAccessKey, boolean isSecure,
                                 String server, int port, CallingFormat format) {
            this.awsAccessKeyId=awsAccessKeyId;
            this.awsSecretAccessKey=awsSecretAccessKey;
            this.isSecure=isSecure;
            this.server=server;
            this.port=port;
            this.callingFormat=format;
        }

        /**
         * Creates a new bucket.
         * @param bucket   The name of the bucket to create.
         * @param headers  A Map of String to List of Strings representing the http headers to pass (can be null).
         */
        public Response createBucket(String bucket, Map headers) throws IOException {
            return createBucket(bucket, null, headers);
        }

        /**
         * Creates a new bucket.
         * @param bucket   The name of the bucket to create.
         * @param location Desired location ("EU") (or null for default).
         * @param headers  A Map of String to List of Strings representing the http
         *                 headers to pass (can be null).
         * @throws IllegalArgumentException on invalid location
         */
        public Response createBucket(String bucket, String location, Map headers) throws IOException {
            String body;
            if(location == null) {
                body=null;
            }
            else if(LOCATION_EU.equals(location)) {
                if(!callingFormat.supportsLocatedBuckets())
                    throw new IllegalArgumentException("Creating location-constrained bucket with unsupported calling-format");
                body="<CreateBucketConstraint><LocationConstraint>" + location + "</LocationConstraint></CreateBucketConstraint>";
            }
            else
                throw new IllegalArgumentException("Invalid Location: " + location);

            // validate bucket name
            if(!Utils.validateBucketName(bucket, callingFormat))
                throw new IllegalArgumentException("Invalid Bucket Name: " + bucket);

            HttpURLConnection request=makeRequest("PUT", bucket, "", null, headers);
            if(body != null) {
                request.setDoOutput(true);
                request.getOutputStream().write(body.getBytes("UTF-8"));
            }
            return new Response(request);
        }

        /**
         * Check if the specified bucket exists (via a HEAD request)
         * @param bucket The name of the bucket to check
         * @return true if HEAD access returned success
         */
        public boolean checkBucketExists(String bucket) throws IOException {
            HttpURLConnection response=makeRequest("HEAD", bucket, "", null, null);
            int httpCode=response.getResponseCode();

            if(httpCode >= 200 && httpCode < 300)
                return true;
            if(httpCode == HttpURLConnection.HTTP_NOT_FOUND) // bucket doesn't exist
                return false;
            throw new IOException("bucket '" + bucket + "' could not be accessed (rsp=" +
                    httpCode + " (" + response.getResponseMessage() + "). Maybe the bucket is owned by somebody else or " +
                    "the authentication failed");

        }

        /**
         * Lists the contents of a bucket.
         * @param bucket  The name of the bucket to create.
         * @param prefix  All returned keys will start with this string (can be null).
         * @param marker  All returned keys will be lexographically greater than
         *                this string (can be null).
         * @param maxKeys The maximum number of keys to return (can be null).
         * @param headers A Map of String to List of Strings representing the http
         *                headers to pass (can be null).
         */
        public ListBucketResponse listBucket(String bucket, String prefix, String marker,
                                             Integer maxKeys, Map headers) throws IOException {
            return listBucket(bucket, prefix, marker, maxKeys, null, headers);
        }

        /**
         * Lists the contents of a bucket.
         * @param bucket    The name of the bucket to list.
         * @param prefix    All returned keys will start with this string (can be null).
         * @param marker    All returned keys will be lexographically greater than
         *                  this string (can be null).
         * @param maxKeys   The maximum number of keys to return (can be null).
         * @param delimiter Keys that contain a string between the prefix and the first
         *                  occurrence of the delimiter will be rolled up into a single element.
         * @param headers   A Map of String to List of Strings representing the http
         *                  headers to pass (can be null).
         */
        public ListBucketResponse listBucket(String bucket, String prefix, String marker,
                                             Integer maxKeys, String delimiter, Map headers) throws IOException {

            Map pathArgs=Utils.paramsForListOptions(prefix, marker, maxKeys, delimiter);
            return new ListBucketResponse(makeRequest("GET", bucket, "", pathArgs, headers));
        }

        /**
         * Deletes a bucket.
         * @param bucket  The name of the bucket to delete.
         * @param headers A Map of String to List of Strings representing the http headers to pass (can be null).
         */
        public Response deleteBucket(String bucket, Map headers) throws IOException {
            return new Response(makeRequest("DELETE", bucket, "", null, headers));
        }

        /**
         * Writes an object to S3.
         * @param bucket  The name of the bucket to which the object will be added.
         * @param key     The name of the key to use.
         * @param object  An S3Object containing the data to write.
         * @param headers A Map of String to List of Strings representing the http
         *                headers to pass (can be null).
         */
        public Response put(String bucket, String key, S3Object object, Map headers) throws IOException {
            HttpURLConnection request=
                    makeRequest("PUT", bucket, Utils.urlencode(key), null, headers, object);

            request.setDoOutput(true);
            request.getOutputStream().write(object.data == null? new byte[]{} : object.data);

            return new Response(request);
        }

        public Response put(String preSignedUrl, S3Object object, Map headers) throws IOException {
            HttpURLConnection request = makePreSignedRequest("PUT", preSignedUrl, headers);
            request.setDoOutput(true);
            request.getOutputStream().write(object.data == null? new byte[]{} : object.data);

            return new Response(request);
        }

        /**
         * Creates a copy of an existing S3 Object.  In this signature, we will copy the
         * existing metadata.  The default access control policy is private; if you want
         * to override it, please use x-amz-acl in the headers.
         * @param sourceBucket      The name of the bucket where the source object lives.
         * @param sourceKey         The name of the key to copy.
         * @param destinationBucket The name of the bucket to which the object will be added.
         * @param destinationKey    The name of the key to use.
         * @param headers           A Map of String to List of Strings representing the http
         *                          headers to pass (can be null).  You may wish to set the x-amz-acl header appropriately.
         */
        public Response copy(String sourceBucket, String sourceKey, String destinationBucket, String destinationKey, Map headers)
                throws IOException {
            S3Object object=new S3Object(new byte[]{}, new HashMap());
            headers=headers == null? new HashMap() : new HashMap(headers);
            headers.put("x-amz-copy-source", Arrays.asList(sourceBucket + "/" + sourceKey));
            headers.put("x-amz-metadata-directive", Arrays.asList("COPY"));
            return verifyCopy(put(destinationBucket, destinationKey, object, headers));
        }

        /**
         * Creates a copy of an existing S3 Object.  In this signature, we will replace the
         * existing metadata.  The default access control policy is private; if you want
         * to override it, please use x-amz-acl in the headers.
         * @param sourceBucket      The name of the bucket where the source object lives.
         * @param sourceKey         The name of the key to copy.
         * @param destinationBucket The name of the bucket to which the object will be added.
         * @param destinationKey    The name of the key to use.
         * @param metadata          A Map of String to List of Strings representing the S3 metadata
         *                          for the new object.
         * @param headers           A Map of String to List of Strings representing the http
         *                          headers to pass (can be null).  You may wish to set the x-amz-acl header appropriately.
         */
        public Response copy(String sourceBucket, String sourceKey, String destinationBucket, String destinationKey, Map metadata, Map headers)
                throws IOException {
            S3Object object=new S3Object(new byte[]{}, metadata);
            headers=headers == null? new HashMap() : new HashMap(headers);
            headers.put("x-amz-copy-source", Arrays.asList(sourceBucket + "/" + sourceKey));
            headers.put("x-amz-metadata-directive", Arrays.asList("REPLACE"));
            return verifyCopy(put(destinationBucket, destinationKey, object, headers));
        }

        /**
         * Copy sometimes returns a successful response and starts to send whitespace
         * characters to us.  This method processes those whitespace characters and
         * will throw an exception if the response is either unknown or an error.
         * @param response Response object from the PUT request.
         * @return The response with the input stream drained.
         * @throws IOException If anything goes wrong.
         */
        private static Response verifyCopy(Response response) throws IOException {
            if(response.connection.getResponseCode() < 400) {
                byte[] body=GetResponse.slurpInputStream(response.connection.getInputStream());
                String message=new String(body);
                if(message.contains("<Error")) {
                    throw new IOException(message.substring(message.indexOf("<Error")));
                }
                else if(message.contains("</CopyObjectResult>")) {
                    // It worked!
                }
                else {
                    throw new IOException("Unexpected response: " + message);
                }
            }
            return response;
        }

        /**
         * Reads an object from S3.
         * @param bucket  The name of the bucket where the object lives.
         * @param key     The name of the key to use.
         * @param headers A Map of String to List of Strings representing the http
         *                headers to pass (can be null).
         */
        public GetResponse get(String bucket, String key, Map headers) throws IOException {
            return new GetResponse(makeRequest("GET", bucket, Utils.urlencode(key), null, headers));
        }

        /**
         * Deletes an object from S3.
         * @param bucket  The name of the bucket where the object lives.
         * @param key     The name of the key to use.
         * @param headers A Map of String to List of Strings representing the http
         *                headers to pass (can be null).
         */
        public Response delete(String bucket, String key, Map headers) throws IOException {
            return new Response(makeRequest("DELETE", bucket, Utils.urlencode(key), null, headers));
        }

        public Response delete(String preSignedUrl) throws IOException {
            return new Response(makePreSignedRequest("DELETE", preSignedUrl, null));
        }

        /**
         * Get the requestPayment xml document for a given bucket
         * @param bucket  The name of the bucket
         * @param headers A Map of String to List of Strings representing the http
         *                headers to pass (can be null).
         */
        public GetResponse getBucketRequestPayment(String bucket, Map headers) throws IOException {
            Map pathArgs=new HashMap();
            pathArgs.put("requestPayment", null);
            return new GetResponse(makeRequest("GET", bucket, "", pathArgs, headers));
        }

        /**
         * Write a new requestPayment xml document for a given bucket
         * @param bucket        The name of the bucket
         * @param requestPaymentXMLDoc
         * @param headers       A Map of String to List of Strings representing the http
         *                      headers to pass (can be null).
         */
        public Response putBucketRequestPayment(String bucket, String requestPaymentXMLDoc, Map headers)
                throws IOException {
            Map pathArgs=new HashMap();
            pathArgs.put("requestPayment", null);
            S3Object object=new S3Object(requestPaymentXMLDoc.getBytes(), null);
            HttpURLConnection request=makeRequest("PUT", bucket, "", pathArgs, headers, object);

            request.setDoOutput(true);
            request.getOutputStream().write(object.data == null? new byte[]{} : object.data);

            return new Response(request);
        }

        /**
         * Get the logging xml document for a given bucket
         * @param bucket  The name of the bucket
         * @param headers A Map of String to List of Strings representing the http headers to pass (can be null).
         */
        public GetResponse getBucketLogging(String bucket, Map headers) throws IOException {
            Map pathArgs=new HashMap();
            pathArgs.put("logging", null);
            return new GetResponse(makeRequest("GET", bucket, "", pathArgs, headers));
        }

        /**
         * Write a new logging xml document for a given bucket
         * @param loggingXMLDoc The xml representation of the logging configuration as a String
         * @param bucket        The name of the bucket
         * @param headers       A Map of String to List of Strings representing the http
         *                      headers to pass (can be null).
         */
        public Response putBucketLogging(String bucket, String loggingXMLDoc, Map headers) throws IOException {
            Map pathArgs=new HashMap();
            pathArgs.put("logging", null);
            S3Object object=new S3Object(loggingXMLDoc.getBytes(), null);
            HttpURLConnection request=makeRequest("PUT", bucket, "", pathArgs, headers, object);

            request.setDoOutput(true);
            request.getOutputStream().write(object.data == null? new byte[]{} : object.data);

            return new Response(request);
        }

        /**
         * Get the ACL for a given bucket
         * @param bucket  The name of the bucket where the object lives.
         * @param headers A Map of String to List of Strings representing the http
         *                headers to pass (can be null).
         */
        public GetResponse getBucketACL(String bucket, Map headers) throws IOException {
            return getACL(bucket, "", headers);
        }

        /**
         * Get the ACL for a given object (or bucket, if key is null).
         * @param bucket  The name of the bucket where the object lives.
         * @param key     The name of the key to use.
         * @param headers A Map of String to List of Strings representing the http
         *                headers to pass (can be null).
         */
        public GetResponse getACL(String bucket, String key, Map headers) throws IOException {
            if(key == null) key="";

            Map pathArgs=new HashMap();
            pathArgs.put("acl", null);

            return new GetResponse(
                    makeRequest("GET", bucket, Utils.urlencode(key), pathArgs, headers)
            );
        }

        /**
         * Write a new ACL for a given bucket
         * @param aclXMLDoc The xml representation of the ACL as a String
         * @param bucket    The name of the bucket where the object lives.
         * @param headers   A Map of String to List of Strings representing the http headers to pass (can be null).
         */
        public Response putBucketACL(String bucket, String aclXMLDoc, Map headers) throws IOException {
            return putACL(bucket, "", aclXMLDoc, headers);
        }

        /**
         * Write a new ACL for a given object
         * @param aclXMLDoc The xml representation of the ACL as a String
         * @param bucket    The name of the bucket where the object lives.
         * @param key       The name of the key to use.
         * @param headers   A Map of String to List of Strings representing the http
         *                  headers to pass (can be null).
         */
        public Response putACL(String bucket, String key, String aclXMLDoc, Map headers)
                throws IOException {
            S3Object object=new S3Object(aclXMLDoc.getBytes(), null);

            Map pathArgs=new HashMap();
            pathArgs.put("acl", null);

            HttpURLConnection request=
                    makeRequest("PUT", bucket, Utils.urlencode(key), pathArgs, headers, object);

            request.setDoOutput(true);
            request.getOutputStream().write(object.data == null? new byte[]{} : object.data);

            return new Response(request);
        }

        public LocationResponse getBucketLocation(String bucket)
                throws IOException {
            Map pathArgs=new HashMap();
            pathArgs.put("location", null);
            return new LocationResponse(makeRequest("GET", bucket, "", pathArgs, null));
        }


        /**
         * List all the buckets created by this account.
         * @param headers A Map of String to List of Strings representing the http
         *                headers to pass (can be null).
         */
        public ListAllMyBucketsResponse listAllMyBuckets(Map headers)
                throws IOException {
            return new ListAllMyBucketsResponse(makeRequest("GET", "", "", null, headers));
        }


        /**
         * Make a new HttpURLConnection without passing an S3Object parameter.
         * Use this method for key operations that do require arguments
         * @param method     The method to invoke
         * @param bucketName the bucket this request is for
         * @param key        the key this request is for
         * @param pathArgs   the
         * @param headers
         * @return
         * @throws MalformedURLException
         * @throws IOException
         */
        private HttpURLConnection makeRequest(String method, String bucketName, String key, Map pathArgs, Map headers)
                throws IOException {
            return makeRequest(method, bucketName, key, pathArgs, headers, null);
        }


        /**
         * Make a new HttpURLConnection.
         * @param method     The HTTP method to use (GET, PUT, DELETE)
         * @param bucket     The bucket name this request affects
         * @param key        The key this request is for
         * @param pathArgs   parameters if any to be sent along this request
         * @param headers    A Map of String to List of Strings representing the http
         *                   headers to pass (can be null).
         * @param object     The S3Object that is to be written (can be null).
         */
        private HttpURLConnection makeRequest(String method, String bucket, String key, Map pathArgs, Map headers,
                                              S3Object object)
                throws IOException {
            CallingFormat format=Utils.getCallingFormatForBucket(this.callingFormat, bucket);
            if(isSecure && format != CallingFormat.getPathCallingFormat() && bucket.contains(".")) {
                System.err.println("You are making an SSL connection, however, the bucket contains periods and the wildcard certificate will not match by default.  Please consider using HTTP.");
            }

            // build the domain based on the calling format
            URL url=format.getURL(isSecure, server, this.port, bucket, key, pathArgs);

            HttpURLConnection connection=(HttpURLConnection)url.openConnection();
            connection.setRequestMethod(method);

            // subdomain-style urls may encounter http redirects.
            // Ensure that redirects are supported.
            if(!connection.getInstanceFollowRedirects()
                    && format.supportsLocatedBuckets())
                throw new RuntimeException("HTTP redirect support required.");

            addHeaders(connection, headers);
            if(object != null) addMetadataHeaders(connection, object.metadata);
            addAuthHeader(connection, method, bucket, key, pathArgs);

            return connection;
        }

        private HttpURLConnection makePreSignedRequest(String method, String preSignedUrl, Map headers) throws IOException {
            URL url = new URL(preSignedUrl);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.setRequestMethod(method);

            addHeaders(connection, headers);

            return connection;
        }

        /**
         * Add the given headers to the HttpURLConnection.
         * @param connection The HttpURLConnection to which the headers will be added.
         * @param headers    A Map of String to List of Strings representing the http
         *                   headers to pass (can be null).
         */
        private static void addHeaders(HttpURLConnection connection, Map headers) {
            addHeaders(connection, headers, "");
        }

        /**
         * Add the given metadata fields to the HttpURLConnection.
         * @param connection The HttpURLConnection to which the headers will be added.
         * @param metadata   A Map of String to List of Strings representing the s3
         *                   metadata for this resource.
         */
        private static void addMetadataHeaders(HttpURLConnection connection, Map metadata) {
            addHeaders(connection, metadata, Utils.METADATA_PREFIX);
        }

        /**
         * Add the given headers to the HttpURLConnection with a prefix before the keys.
         * @param connection The HttpURLConnection to which the headers will be added.
         * @param headers    A Map of String to List of Strings representing the http
         *                   headers to pass (can be null).
         * @param prefix     The string to prepend to each key before adding it to the connection.
         */
        private static void addHeaders(HttpURLConnection connection, Map headers, String prefix) {
            if(headers != null) {
                for(Iterator i=headers.keySet().iterator(); i.hasNext();) {
                    String key=(String)i.next();
                    for(Iterator j=((List)headers.get(key)).iterator(); j.hasNext();) {
                        String value=(String)j.next();
                        connection.addRequestProperty(prefix + key, value);
                    }
                }
            }
        }

        /**
         * Add the appropriate Authorization header to the HttpURLConnection.
         * @param connection The HttpURLConnection to which the header will be added.
         * @param method     The HTTP method to use (GET, PUT, DELETE)
         * @param bucket     the bucket name this request is for
         * @param key        the key this request is for
         * @param pathArgs   path arguments which are part of this request
         */
        private void addAuthHeader(HttpURLConnection connection, String method, String bucket, String key, Map pathArgs) {
            if(connection.getRequestProperty("Date") == null) {
                connection.setRequestProperty("Date", httpDate());
            }
            if(connection.getRequestProperty("Content-Type") == null) {
                connection.setRequestProperty("Content-Type", "");
            }

            if(this.awsAccessKeyId != null && this.awsSecretAccessKey != null) {
                String canonicalString=
                        Utils.makeCanonicalString(method, bucket, key, pathArgs, connection.getRequestProperties());
                String encodedCanonical=Utils.encode(this.awsSecretAccessKey, canonicalString, false);
                connection.setRequestProperty("Authorization",
                                              "AWS " + this.awsAccessKeyId + ":" + encodedCanonical);
            }
        }


        /**
         * Generate an rfc822 date for use in the Date HTTP header.
         */
        public static String httpDate() {
            final String DateFormat="EEE, dd MMM yyyy HH:mm:ss ";
            SimpleDateFormat format=new SimpleDateFormat(DateFormat, Locale.US);
            format.setTimeZone(TimeZone.getTimeZone("GMT"));
            return format.format(new Date()) + "GMT";
        }
    }

    static class ListEntry {
        /**
         * The name of the object
         */
        public String key;

        /**
         * The date at which the object was last modified.
         */
        public Date lastModified;

        /**
         * The object's ETag, which can be used for conditional GETs.
         */
        public String eTag;

        /**
         * The size of the object in bytes.
         */
        public long size;

        /**
         * The object's storage class
         */
        public String storageClass;

        /**
         * The object's owner
         */
        public Owner owner;

        public String toString() {
            return key;
        }
    }

    static class Owner {
        public String id;
        public String displayName;
    }


    static class Response {
        public HttpURLConnection connection;

        public Response(HttpURLConnection connection) throws IOException {
            this.connection=connection;
        }
    }


    static class GetResponse extends Response {
        public S3Object object;

        /**
         * Pulls a representation of an S3Object out of the HttpURLConnection response.
         */
        public GetResponse(HttpURLConnection connection) throws IOException {
            super(connection);
            if(connection.getResponseCode() < 400) {
                Map metadata=extractMetadata(connection);
                byte[] body=slurpInputStream(connection.getInputStream());
                this.object=new S3Object(body, metadata);
            }
        }

        /**
         * Examines the response's header fields and returns a Map from String to List of Strings
         * representing the object's metadata.
         */
        private static Map extractMetadata(HttpURLConnection connection) {
            TreeMap metadata=new TreeMap();
            Map headers=connection.getHeaderFields();
            for(Iterator i=headers.keySet().iterator(); i.hasNext();) {
                String key=(String)i.next();
                if(key == null) continue;
                if(key.startsWith(Utils.METADATA_PREFIX)) {
                    metadata.put(key.substring(Utils.METADATA_PREFIX.length()), headers.get(key));
                }
            }

            return metadata;
        }

        /**
         * Read the input stream and dump it all into a big byte array
         */
        static byte[] slurpInputStream(InputStream stream) throws IOException {
            final int chunkSize=2048;
            byte[] buf=new byte[chunkSize];
            ByteArrayOutputStream byteStream=new ByteArrayOutputStream(chunkSize);
            int count;

            while((count=stream.read(buf)) != -1) byteStream.write(buf, 0, count);

            return byteStream.toByteArray();
        }
    }

    static class LocationResponse extends Response {
        String location;

        /**
         * Parse the response to a ?location query.
         */
        public LocationResponse(HttpURLConnection connection) throws IOException {
            super(connection);
            if(connection.getResponseCode() < 400) {
                try {
                    XMLReader xr=Utils.createXMLReader();
                    ;
                    LocationResponseHandler handler=new LocationResponseHandler();
                    xr.setContentHandler(handler);
                    xr.setErrorHandler(handler);

                    xr.parse(new InputSource(connection.getInputStream()));
                    this.location=handler.loc;
                }
                catch(SAXException e) {
                    throw new RuntimeException("Unexpected error parsing ListAllMyBuckets xml", e);
                }
            }
            else {
                this.location="<error>";
            }
        }

        /**
         * Report the location-constraint for a bucket.
         * A value of null indicates an error;
         * the empty string indicates no constraint;
         * and any other value is an actual location constraint value.
         */
        public String getLocation() {
            return location;
        }

        /**
         * Helper class to parse LocationConstraint response XML
         */
        static class LocationResponseHandler extends DefaultHandler {
            String loc=null;
            private StringBuffer currText=null;

            public void startDocument() {
            }

            public void startElement(String uri, String name, String qName, Attributes attrs) {
                if(name.equals("LocationConstraint")) {
                    this.currText=new StringBuffer();
                }
            }

            public void endElement(String uri, String name, String qName) {
                if(name.equals("LocationConstraint")) {
                    loc=this.currText.toString();
                    this.currText=null;
                }
            }

            public void characters(char ch[], int start, int length) {
                if(currText != null)
                    this.currText.append(ch, start, length);
            }
        }
    }


    static class Bucket {
        /**
         * The name of the bucket.
         */
        public String name;

        /**
         * The bucket's creation date.
         */
        public Date creationDate;

        public Bucket() {
            this.name=null;
            this.creationDate=null;
        }

        public Bucket(String name, Date creationDate) {
            this.name=name;
            this.creationDate=creationDate;
        }

        public String toString() {
            return this.name;
        }
    }

    static class ListBucketResponse extends Response {

        /**
         * The name of the bucket being listed.  Null if request fails.
         */
        public String name=null;

        /**
         * The prefix echoed back from the request.  Null if request fails.
         */
        public String prefix=null;

        /**
         * The marker echoed back from the request.  Null if request fails.
         */
        public String marker=null;

        /**
         * The delimiter echoed back from the request.  Null if not specified in
         * the request, or if it fails.
         */
        public String delimiter=null;

        /**
         * The maxKeys echoed back from the request if specified.  0 if request fails.
         */
        public int maxKeys=0;

        /**
         * Indicates if there are more results to the list.  True if the current
         * list results have been truncated.  false if request fails.
         */
        public boolean isTruncated=false;

        /**
         * Indicates what to use as a marker for subsequent list requests in the event
         * that the results are truncated.  Present only when a delimiter is specified.
         * Null if request fails.
         */
        public String nextMarker=null;

        /**
         * A List of ListEntry objects representing the objects in the given bucket.
         * Null if the request fails.
         */
        public List entries=null;

        /**
         * A List of CommonPrefixEntry objects representing the common prefixes of the
         * keys that matched up to the delimiter.  Null if the request fails.
         */
        public List commonPrefixEntries=null;

        public ListBucketResponse(HttpURLConnection connection) throws IOException {
            super(connection);
            if(connection.getResponseCode() < 400) {
                try {
                    XMLReader xr=Utils.createXMLReader();
                    ListBucketHandler handler=new ListBucketHandler();
                    xr.setContentHandler(handler);
                    xr.setErrorHandler(handler);

                    xr.parse(new InputSource(connection.getInputStream()));

                    this.name=handler.getName();
                    this.prefix=handler.getPrefix();
                    this.marker=handler.getMarker();
                    this.delimiter=handler.getDelimiter();
                    this.maxKeys=handler.getMaxKeys();
                    this.isTruncated=handler.getIsTruncated();
                    this.nextMarker=handler.getNextMarker();
                    this.entries=handler.getKeyEntries();
                    this.commonPrefixEntries=handler.getCommonPrefixEntries();

                }
                catch(SAXException e) {
                    throw new RuntimeException("Unexpected error parsing ListBucket xml", e);
                }
            }
        }

        static class ListBucketHandler extends DefaultHandler {

            private String name=null;
            private String prefix=null;
            private String marker=null;
            private String delimiter=null;
            private int maxKeys=0;
            private boolean isTruncated=false;
            private String nextMarker=null;
            private boolean isEchoedPrefix=false;
            private List keyEntries=null;
            private ListEntry keyEntry=null;
            private List commonPrefixEntries=null;
            private CommonPrefixEntry commonPrefixEntry=null;
            private StringBuffer currText=null;
            private SimpleDateFormat iso8601Parser=null;

            public ListBucketHandler() {
                super();
                keyEntries=new ArrayList();
                commonPrefixEntries=new ArrayList();
                this.iso8601Parser=new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
                this.iso8601Parser.setTimeZone(new SimpleTimeZone(0, "GMT"));
                this.currText=new StringBuffer();
            }

            public void startDocument() {
                this.isEchoedPrefix=true;
            }

            public void endDocument() {
                // ignore
            }

            public void startElement(String uri, String name, String qName, Attributes attrs) {
                if(name.equals("Contents")) {
                    this.keyEntry=new ListEntry();
                }
                else if(name.equals("Owner")) {
                    this.keyEntry.owner=new Owner();
                }
                else if(name.equals("CommonPrefixes")) {
                    this.commonPrefixEntry=new CommonPrefixEntry();
                }
            }

            public void endElement(String uri, String name, String qName) {
                if(name.equals("Name")) {
                    this.name=this.currText.toString();
                }
                // this prefix is the one we echo back from the request
                else if(name.equals("Prefix") && this.isEchoedPrefix) {
                    this.prefix=this.currText.toString();
                    this.isEchoedPrefix=false;
                }
                else if(name.equals("Marker")) {
                    this.marker=this.currText.toString();
                }
                else if(name.equals("MaxKeys")) {
                    this.maxKeys=Integer.parseInt(this.currText.toString());
                }
                else if(name.equals("Delimiter")) {
                    this.delimiter=this.currText.toString();
                }
                else if(name.equals("IsTruncated")) {
                    this.isTruncated=Boolean.valueOf(this.currText.toString());
                }
                else if(name.equals("NextMarker")) {
                    this.nextMarker=this.currText.toString();
                }
                else if(name.equals("Contents")) {
                    this.keyEntries.add(this.keyEntry);
                }
                else if(name.equals("Key")) {
                    this.keyEntry.key=this.currText.toString();
                }
                else if(name.equals("LastModified")) {
                    try {
                        this.keyEntry.lastModified=this.iso8601Parser.parse(this.currText.toString());
                    }
                    catch(ParseException e) {
                        throw new RuntimeException("Unexpected date format in list bucket output", e);
                    }
                }
                else if(name.equals("ETag")) {
                    this.keyEntry.eTag=this.currText.toString();
                }
                else if(name.equals("Size")) {
                    this.keyEntry.size=Long.parseLong(this.currText.toString());
                }
                else if(name.equals("StorageClass")) {
                    this.keyEntry.storageClass=this.currText.toString();
                }
                else if(name.equals("ID")) {
                    this.keyEntry.owner.id=this.currText.toString();
                }
                else if(name.equals("DisplayName")) {
                    this.keyEntry.owner.displayName=this.currText.toString();
                }
                else if(name.equals("CommonPrefixes")) {
                    this.commonPrefixEntries.add(this.commonPrefixEntry);
                }
                // this is the common prefix for keys that match up to the delimiter
                else if(name.equals("Prefix")) {
                    this.commonPrefixEntry.prefix=this.currText.toString();
                }
                if(this.currText.length() != 0)
                    this.currText=new StringBuffer();
            }

            public void characters(char ch[], int start, int length) {
                this.currText.append(ch, start, length);
            }

            public String getName() {
                return this.name;
            }

            public String getPrefix() {
                return this.prefix;
            }

            public String getMarker() {
                return this.marker;
            }

            public String getDelimiter() {
                return this.delimiter;
            }

            public int getMaxKeys() {
                return this.maxKeys;
            }

            public boolean getIsTruncated() {
                return this.isTruncated;
            }

            public String getNextMarker() {
                return this.nextMarker;
            }

            public List getKeyEntries() {
                return this.keyEntries;
            }

            public List getCommonPrefixEntries() {
                return this.commonPrefixEntries;
            }
        }
    }


    static class CommonPrefixEntry {
        /**
         * The prefix common to the delimited keys it represents
         */
        public String prefix;
    }


    static class ListAllMyBucketsResponse extends Response {
        /**
         * A list of Bucket objects, one for each of this account's buckets.  Will be null if
         * the request fails.
         */
        public List entries;

        public ListAllMyBucketsResponse(HttpURLConnection connection) throws IOException {
            super(connection);
            if(connection.getResponseCode() < 400) {
                try {
                    XMLReader xr=Utils.createXMLReader();
                    ;
                    ListAllMyBucketsHandler handler=new ListAllMyBucketsHandler();
                    xr.setContentHandler(handler);
                    xr.setErrorHandler(handler);

                    xr.parse(new InputSource(connection.getInputStream()));
                    this.entries=handler.getEntries();
                }
                catch(SAXException e) {
                    throw new RuntimeException("Unexpected error parsing ListAllMyBuckets xml", e);
                }
            }
        }

        static class ListAllMyBucketsHandler extends DefaultHandler {

            private List entries=null;
            private Bucket currBucket=null;
            private StringBuffer currText=null;
            private SimpleDateFormat iso8601Parser=null;

            public ListAllMyBucketsHandler() {
                super();
                entries=new ArrayList();
                this.iso8601Parser=new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
                this.iso8601Parser.setTimeZone(new SimpleTimeZone(0, "GMT"));
                this.currText=new StringBuffer();
            }

            public void startDocument() {
                // ignore
            }

            public void endDocument() {
                // ignore
            }

            public void startElement(String uri, String name, String qName, Attributes attrs) {
                if(name.equals("Bucket")) {
                    this.currBucket=new Bucket();
                }
            }

            public void endElement(String uri, String name, String qName) {
                if(name.equals("Bucket")) {
                    this.entries.add(this.currBucket);
                }
                else if(name.equals("Name")) {
                    this.currBucket.name=this.currText.toString();
                }
                else if(name.equals("CreationDate")) {
                    try {
                        this.currBucket.creationDate=this.iso8601Parser.parse(this.currText.toString());
                    }
                    catch(ParseException e) {
                        throw new RuntimeException("Unexpected date format in list bucket output", e);
                    }
                }
                this.currText=new StringBuffer();
            }

            public void characters(char ch[], int start, int length) {
                this.currText.append(ch, start, length);
            }

            public List getEntries() {
                return this.entries;
            }
        }
    }


    static class S3Object {

        public byte[] data;

        /**
         * A Map from String to List of Strings representing the object's metadata
         */
        public Map metadata;

        public S3Object(byte[] data, Map metadata) {
            this.data=data;
            this.metadata=metadata;
        }
    }


    abstract static class CallingFormat {

        protected static CallingFormat pathCallingFormat=new PathCallingFormat();
        protected static CallingFormat subdomainCallingFormat=new SubdomainCallingFormat();
        protected static CallingFormat vanityCallingFormat=new VanityCallingFormat();

        public abstract boolean supportsLocatedBuckets();

        public abstract String getEndpoint(String server, int port, String bucket);

        public abstract String getPathBase(String bucket, String key);

        public abstract URL getURL(boolean isSecure, String server, int port, String bucket, String key, Map pathArgs)
                throws MalformedURLException;

        public static CallingFormat getPathCallingFormat() {
            return pathCallingFormat;
        }

        public static CallingFormat getSubdomainCallingFormat() {
            return subdomainCallingFormat;
        }

        public static CallingFormat getVanityCallingFormat() {
            return vanityCallingFormat;
        }

        private static class PathCallingFormat extends CallingFormat {
            public boolean supportsLocatedBuckets() {
                return false;
            }

            public String getPathBase(String bucket, String key) {
                return isBucketSpecified(bucket)? "/" + bucket + "/" + key : "/";
            }

            public String getEndpoint(String server, int port, String bucket) {
                return server + ":" + port;
            }

            public URL getURL(boolean isSecure, String server, int port, String bucket, String key, Map pathArgs)
                    throws MalformedURLException {
                String pathBase=isBucketSpecified(bucket)? "/" + bucket + "/" + key : "/";
                String pathArguments=Utils.convertPathArgsHashToString(pathArgs);
                return new URL(isSecure? "https" : "http", server, port, pathBase + pathArguments);
            }

            private static boolean isBucketSpecified(String bucket) {
                return bucket != null && bucket.length() != 0;
            }
        }

        private static class SubdomainCallingFormat extends CallingFormat {
            public boolean supportsLocatedBuckets() {
                return true;
            }

            public String getServer(String server, String bucket) {
                return bucket + "." + server;
            }

            public String getEndpoint(String server, int port, String bucket) {
                return getServer(server, bucket) + ":" + port;
            }

            public String getPathBase(String bucket, String key) {
                return "/" + key;
            }

            public URL getURL(boolean isSecure, String server, int port, String bucket, String key, Map pathArgs)
                    throws MalformedURLException {
                if(bucket == null || bucket.length() == 0) {
                    //The bucket is null, this is listAllBuckets request
                    String pathArguments=Utils.convertPathArgsHashToString(pathArgs);
                    return new URL(isSecure? "https" : "http", server, port, "/" + pathArguments);
                }
                else {
                    String serverToUse=getServer(server, bucket);
                    String pathBase=getPathBase(bucket, key);
                    String pathArguments=Utils.convertPathArgsHashToString(pathArgs);
                    return new URL(isSecure? "https" : "http", serverToUse, port, pathBase + pathArguments);
                }
            }
        }

        private static class VanityCallingFormat extends SubdomainCallingFormat {
            public String getServer(String server, String bucket) {
                return bucket;
            }
        }
    }

    static class Utils {
        static final String METADATA_PREFIX="x-amz-meta-";
        static final String AMAZON_HEADER_PREFIX="x-amz-";
        static final String ALTERNATIVE_DATE_HEADER="x-amz-date";
        public static final String DEFAULT_HOST="s3.amazonaws.com";

        public static final int SECURE_PORT=443;
        public static final int INSECURE_PORT=80;


        /**
         * HMAC/SHA1 Algorithm per RFC 2104.
         */
        private static final String HMAC_SHA1_ALGORITHM="HmacSHA1";

        static String makeCanonicalString(String method, String bucket, String key, Map pathArgs, Map headers) {
            return makeCanonicalString(method, bucket, key, pathArgs, headers, null);
        }

        /**
         * Calculate the canonical string.  When expires is non-null, it will be
         * used instead of the Date header.
         */
        static String makeCanonicalString(String method, String bucketName, String key, Map pathArgs,
                                          Map headers, String expires) {
            StringBuilder buf=new StringBuilder();
            buf.append(method + "\n");

            // Add all interesting headers to a list, then sort them.  "Interesting"
            // is defined as Content-MD5, Content-Type, Date, and x-amz-
            SortedMap interestingHeaders=new TreeMap();
            if(headers != null) {
                for(Iterator i=headers.keySet().iterator(); i.hasNext();) {
                    String hashKey=(String)i.next();
                    if(hashKey == null) continue;
                    String lk=hashKey.toLowerCase();

                    // Ignore any headers that are not particularly interesting.
                    if(lk.equals("content-type") || lk.equals("content-md5") || lk.equals("date") ||
                            lk.startsWith(AMAZON_HEADER_PREFIX)) {
                        List s=(List)headers.get(hashKey);
                        interestingHeaders.put(lk, concatenateList(s));
                    }
                }
            }

            if(interestingHeaders.containsKey(ALTERNATIVE_DATE_HEADER)) {
                interestingHeaders.put("date", "");
            }

            // if the expires is non-null, use that for the date field.  this
            // trumps the x-amz-date behavior.
            if(expires != null) {
                interestingHeaders.put("date", expires);
            }

            // these headers require that we still put a new line in after them,
            // even if they don't exist.
            if(!interestingHeaders.containsKey("content-type")) {
                interestingHeaders.put("content-type", "");
            }
            if(!interestingHeaders.containsKey("content-md5")) {
                interestingHeaders.put("content-md5", "");
            }

            // Finally, add all the interesting headers (i.e.: all that startwith x-amz- ;-))
            for(Iterator i=interestingHeaders.keySet().iterator(); i.hasNext();) {
                String headerKey=(String)i.next();
                if(headerKey.startsWith(AMAZON_HEADER_PREFIX)) {
                    buf.append(headerKey).append(':').append(interestingHeaders.get(headerKey));
                }
                else {
                    buf.append(interestingHeaders.get(headerKey));
                }
                buf.append("\n");
            }

            // build the path using the bucket and key
            if(bucketName != null && bucketName.length() != 0) {
                buf.append("/" + bucketName);
            }

            // append the key (it might be an empty string)
            // append a slash regardless
            buf.append("/");
            if(key != null) {
                buf.append(key);
            }

            // if there is an acl, logging or torrent parameter
            // add them to the string
            if(pathArgs != null) {
                if(pathArgs.containsKey("acl")) {
                    buf.append("?acl");
                }
                else if(pathArgs.containsKey("torrent")) {
                    buf.append("?torrent");
                }
                else if(pathArgs.containsKey("logging")) {
                    buf.append("?logging");
                }
                else if(pathArgs.containsKey("location")) {
                    buf.append("?location");
                }
            }

            return buf.toString();

        }

        /**
         * Calculate the HMAC/SHA1 on a string.
         * @return Signature
         * @throws java.security.NoSuchAlgorithmException
         *          If the algorithm does not exist.  Unlikely
         * @throws java.security.InvalidKeyException
         *          If the key is invalid.
         */
        static String encode(String awsSecretAccessKey, String canonicalString,
                             boolean urlencode) {
            // The following HMAC/SHA1 code for the signature is taken from the
            // AWS Platform's implementation of RFC2104 (amazon.webservices.common.Signature)
            //
            // Acquire an HMAC/SHA1 from the raw key bytes.
            SecretKeySpec signingKey=
                    new SecretKeySpec(awsSecretAccessKey.getBytes(), HMAC_SHA1_ALGORITHM);

            // Acquire the MAC instance and initialize with the signing key.
            Mac mac=null;
            try {
                mac=Mac.getInstance(HMAC_SHA1_ALGORITHM);
            }
            catch(NoSuchAlgorithmException e) {
                // should not happen
                throw new RuntimeException("Could not find sha1 algorithm", e);
            }
            try {
                mac.init(signingKey);
            }
            catch(InvalidKeyException e) {
                // also should not happen
                throw new RuntimeException("Could not initialize the MAC algorithm", e);
            }

            // Compute the HMAC on the digest, and set it.
            String b64=Base64.encodeBytes(mac.doFinal(canonicalString.getBytes()));

            if(urlencode) {
                return urlencode(b64);
            }
            else {
                return b64;
            }
        }

        static Map paramsForListOptions(String prefix, String marker, Integer maxKeys) {
            return paramsForListOptions(prefix, marker, maxKeys, null);
        }

        static Map paramsForListOptions(String prefix, String marker, Integer maxKeys, String delimiter) {

            Map argParams=new HashMap();
            // these three params must be url encoded
            if(prefix != null)
                argParams.put("prefix", urlencode(prefix));
            if(marker != null)
                argParams.put("marker", urlencode(marker));
            if(delimiter != null)
                argParams.put("delimiter", urlencode(delimiter));

            if(maxKeys != null)
                argParams.put("max-keys", Integer.toString(maxKeys.intValue()));

            return argParams;

        }

        /**
         * Converts the Path Arguments from a map to String which can be used in url construction
         * @param pathArgs a map of arguments
         * @return a string representation of pathArgs
         */
        public static String convertPathArgsHashToString(Map pathArgs) {
            StringBuilder pathArgsString=new StringBuilder();
            String argumentValue;
            boolean firstRun=true;
            if(pathArgs != null) {
                for(Iterator argumentIterator=pathArgs.keySet().iterator(); argumentIterator.hasNext();) {
                    String argument=(String)argumentIterator.next();
                    if(firstRun) {
                        firstRun=false;
                        pathArgsString.append("?");
                    }
                    else {
                        pathArgsString.append("&");
                    }

                    argumentValue=(String)pathArgs.get(argument);
                    pathArgsString.append(argument);
                    if(argumentValue != null) {
                        pathArgsString.append("=");
                        pathArgsString.append(argumentValue);
                    }
                }
            }

            return pathArgsString.toString();
        }


        static String urlencode(String unencoded) {
            try {
                return URLEncoder.encode(unencoded, "UTF-8");
            }
            catch(UnsupportedEncodingException e) {
                // should never happen
                throw new RuntimeException("Could not url encode to UTF-8", e);
            }
        }

        static XMLReader createXMLReader() {
            try {
                return XMLReaderFactory.createXMLReader();
            }
            catch(SAXException e) {
                // oops, lets try doing this (needed in 1.4)
                System.setProperty("org.xml.sax.driver", "org.apache.crimson.parser.XMLReaderImpl");
            }
            try {
                // try once more
                return XMLReaderFactory.createXMLReader();
            }
            catch(SAXException e) {
                throw new RuntimeException("Couldn't initialize a sax driver for the XMLReader");
            }
        }

        /**
         * Concatenates a bunch of header values, seperating them with a comma.
         * @param values List of header values.
         * @return String of all headers, with commas.
         */
        private static String concatenateList(List values) {
            StringBuilder buf=new StringBuilder();
            for(int i=0, size=values.size(); i < size; ++i) {
                buf.append(((String)values.get(i)).replaceAll("\n", "").trim());
                if(i != (size - 1)) {
                    buf.append(",");
                }
            }
            return buf.toString();
        }

        /**
         * Validate bucket-name
         */
        static boolean validateBucketName(String bucketName, CallingFormat callingFormat) {
            if(callingFormat == CallingFormat.getPathCallingFormat()) {
                final int MIN_BUCKET_LENGTH=3;
                final int MAX_BUCKET_LENGTH=255;
                final String BUCKET_NAME_REGEX="^[0-9A-Za-z\\.\\-_]*$";

                return null != bucketName &&
                        bucketName.length() >= MIN_BUCKET_LENGTH &&
                        bucketName.length() <= MAX_BUCKET_LENGTH &&
                        bucketName.matches(BUCKET_NAME_REGEX);
            }
            else {
                return isValidSubdomainBucketName(bucketName);
            }
        }

        static boolean isValidSubdomainBucketName(String bucketName) {
            final int MIN_BUCKET_LENGTH=3;
            final int MAX_BUCKET_LENGTH=63;
            // don't allow names that look like 127.0.0.1
            final String IPv4_REGEX="^[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+$";
            // dns sub-name restrictions
            final String BUCKET_NAME_REGEX="^[a-z0-9]([a-z0-9\\-\\_]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9\\-\\_]*[a-z0-9])?)*$";

            // If there wasn't a location-constraint, then the current actual
            // restriction is just that no 'part' of the name (i.e. sequence
            // of characters between any 2 '.'s has to be 63) but the recommendation
            // is to keep the entire bucket name under 63.
            return null != bucketName &&
                    bucketName.length() >= MIN_BUCKET_LENGTH &&
                    bucketName.length() <= MAX_BUCKET_LENGTH &&
                    !bucketName.matches(IPv4_REGEX) &&
                    bucketName.matches(BUCKET_NAME_REGEX);
        }

        static CallingFormat getCallingFormatForBucket(CallingFormat desiredFormat, String bucketName) {
            CallingFormat callingFormat=desiredFormat;
            if(callingFormat == CallingFormat.getSubdomainCallingFormat() && !Utils.isValidSubdomainBucketName(bucketName)) {
                callingFormat=CallingFormat.getPathCallingFormat();
            }
            return callingFormat;
        }

        public static String generateQueryStringAuthentication(String awsAccessKey, String awsSecretAccessKey,
                                                               String method, String bucket, String key,
                                                               Map pathArgs, Map headers) {
            int defaultExpiresIn = 300; // 5 minutes
            long expirationDate = (System.currentTimeMillis() / 1000) + defaultExpiresIn;
            return generateQueryStringAuthentication(awsAccessKey, awsSecretAccessKey,
                                                     method, bucket, key,
                                                     pathArgs, headers, expirationDate);
        }

        public static String generateQueryStringAuthentication(String awsAccessKey, String awsSecretAccessKey,
                                                               String method, String bucket, String key,
                                                               Map pathArgs, Map headers, long expirationDate) {
            method = method.toUpperCase(); // Method should always be uppercase
            String canonicalString =
                makeCanonicalString(method, bucket, key, pathArgs, headers, "" + expirationDate);
            String encodedCanonical = encode(awsSecretAccessKey, canonicalString, true);
            return "http://" + DEFAULT_HOST + "/" + bucket + "/" + key + "?" +
                "AWSAccessKeyId=" + awsAccessKey + "&Expires=" + expirationDate +
                "&Signature=" + encodedCanonical;
        }
    }


//
//  NOTE: The following source code is the iHarder.net public domain
//  Base64 library and is provided here as a convenience.  For updates,
//  problems, questions, etc. regarding this code, please visit:
//  http://iharder.sourceforge.net/current/java/base64/
//


    /**
     * Encodes and decodes to and from Base64 notation.
     * <p/>
     * <p>
     * Change Log:
     * </p>
     * <ul>
     * <li>v2.1 - Cleaned up javadoc comments and unused variables and methods. Added
     * some convenience methods for reading and writing to and from files.</li>
     * <li>v2.0.2 - Now specifies UTF-8 encoding in places where the code fails on systems
     * with other encodings (like EBCDIC).</li>
     * <li>v2.0.1 - Fixed an error when decoding a single byte, that is, when the
     * encoded data was a single byte.</li>
     * <li>v2.0 - I got rid of methods that used booleans to set options.
     * Now everything is more consolidated and cleaner. The code now detects
     * when data that's being decoded is gzip-compressed and will decompress it
     * automatically. Generally things are cleaner. You'll probably have to
     * change some method calls that you were making to support the new
     * options format (<tt>int</tt>s that you "OR" together).</li>
     * <li>v1.5.1 - Fixed bug when decompressing and decoding to a
     * byte[] using <tt>decode( String s, boolean gzipCompressed )</tt>.
     * Added the ability to "suspend" encoding in the Output Stream so
     * you can turn on and off the encoding if you need to embed base64
     * data in an otherwise "normal" stream (like an XML file).</li>
     * <li>v1.5 - Output stream pases on flush() command but doesn't do anything itself.
     * This helps when using GZIP streams.
     * Added the ability to GZip-compress objects before encoding them.</li>
     * <li>v1.4 - Added helper methods to read/write files.</li>
     * <li>v1.3.6 - Fixed OutputStream.flush() so that 'position' is reset.</li>
     * <li>v1.3.5 - Added flag to turn on and off line breaks. Fixed bug in input stream
     * where last buffer being read, if not completely full, was not returned.</li>
     * <li>v1.3.4 - Fixed when "improperly padded stream" error was thrown at the wrong time.</li>
     * <li>v1.3.3 - Fixed I/O streams which were totally messed up.</li>
     * </ul>
     * <p/>
     * <p>
     * I am placing this code in the Public Domain. Do with it as you will.
     * This software comes with no guarantees or warranties but with
     * plenty of well-wishing instead!
     * Please visit <a href="http://iharder.net/base64">http://iharder.net/base64</a>
     * periodically to check for updates or to contribute improvements.
     * </p>
     * @author Robert Harder
     * @author rob@iharder.net
     * @version 2.1
     */
    static class Base64 {

/* ********  P U B L I C   F I E L D S  ******** */


        /**
         * No options specified. Value is zero.
         */
        public final static int NO_OPTIONS=0;

        /**
         * Specify encoding.
         */
        public final static int ENCODE=1;


        /**
         * Specify decoding.
         */
        public final static int DECODE=0;


        /**
         * Specify that data should be gzip-compressed.
         */
        public final static int GZIP=2;


        /**
         * Don't break lines when encoding (violates strict Base64 specification)
         */
        public final static int DONT_BREAK_LINES=8;


/* ********  P R I V A T E   F I E L D S  ******** */


        /**
         * Maximum line length (76) of Base64 output.
         */
        private final static int MAX_LINE_LENGTH=76;


        /**
         * The equals sign (=) as a byte.
         */
        private final static byte EQUALS_SIGN=(byte)'=';


        /**
         * The new line character (\n) as a byte.
         */
        private final static byte NEW_LINE=(byte)'\n';


        /**
         * Preferred encoding.
         */
        private final static String PREFERRED_ENCODING="UTF-8";


        /**
         * The 64 valid Base64 values.
         */
        private static final byte[] ALPHABET;
        private static final byte[] _NATIVE_ALPHABET= /* May be something funny like EBCDIC */
                {
                        (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G',
                        (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N',
                        (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U',
                        (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z',
                        (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g',
                        (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n',
                        (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u',
                        (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z',
                        (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5',
                        (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'+', (byte)'/'
                };

        /** Determine which ALPHABET to use. */
        static {
            byte[] __bytes;
            try {
                __bytes="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".getBytes(PREFERRED_ENCODING);
            }   // end try
            catch(java.io.UnsupportedEncodingException use) {
                __bytes=_NATIVE_ALPHABET; // Fall back to native encoding
            }   // end catch
            ALPHABET=__bytes;
        }   // end static


        /**
         * Translates a Base64 value to either its 6-bit reconstruction value
         * or a negative number indicating some other meaning.
         */
        private final static byte[] DECODABET=
                {
                        -9, -9, -9, -9, -9, -9, -9, -9, -9,                 // Decimal  0 -  8
                        -5, -5,                                      // Whitespace: Tab and Linefeed
                        -9, -9,                                      // Decimal 11 - 12
                        -5,                                         // Whitespace: Carriage Return
                        -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9,     // Decimal 14 - 26
                        -9, -9, -9, -9, -9,                             // Decimal 27 - 31
                        -5,                                         // Whitespace: Space
                        -9, -9, -9, -9, -9, -9, -9, -9, -9, -9,              // Decimal 33 - 42
                        62,                                         // Plus sign at decimal 43
                        -9, -9, -9,                                   // Decimal 44 - 46
                        63,                                         // Slash at decimal 47
                        52, 53, 54, 55, 56, 57, 58, 59, 60, 61,              // Numbers zero through nine
                        -9, -9, -9,                                   // Decimal 58 - 60
                        -1,                                         // Equals sign at decimal 61
                        -9, -9, -9,                                      // Decimal 62 - 64
                        0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,            // Letters 'A' through 'N'
                        14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,        // Letters 'O' through 'Z'
                        -9, -9, -9, -9, -9, -9,                          // Decimal 91 - 96
                        26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38,     // Letters 'a' through 'm'
                        39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51,     // Letters 'n' through 'z'
                        -9, -9, -9, -9                                 // Decimal 123 - 126
                        /*,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 127 - 139
          -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 140 - 152
          -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 153 - 165
          -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 166 - 178
          -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 179 - 191
          -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 192 - 204
          -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 205 - 217
          -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 218 - 230
          -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 231 - 243
          -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9         // Decimal 244 - 255 */
                };

        // I think I end up not using the BAD_ENCODING indicator.
        //private final static byte BAD_ENCODING    = -9; // Indicates error in encoding
        private final static byte WHITE_SPACE_ENC=-5; // Indicates white space in encoding
        private final static byte EQUALS_SIGN_ENC=-1; // Indicates equals sign in encoding


        /**
         * Defeats instantiation.
         */
        private Base64() {
        }


/* ********  E N C O D I N G   M E T H O D S  ******** */


        /**
         * Encodes up to the first three bytes of array <var>threeBytes</var>
         * and returns a four-byte array in Base64 notation.
         * The actual number of significant bytes in your array is
         * given by <var>numSigBytes</var>.
         * The array <var>threeBytes</var> needs only be as big as
         * <var>numSigBytes</var>.
         * Code can reuse a byte array by passing a four-byte array as <var>b4</var>.
         * @param b4          A reusable byte array to reduce array instantiation
         * @param threeBytes  the array to convert
         * @param numSigBytes the number of significant bytes in your array
         * @return four byte array in Base64 notation.
         * @since 1.5.1
         */
        private static byte[] encode3to4(byte[] b4, byte[] threeBytes, int numSigBytes) {
            encode3to4(threeBytes, 0, numSigBytes, b4, 0);
            return b4;
        }   // end encode3to4


        /**
         * Encodes up to three bytes of the array <var>source</var>
         * and writes the resulting four Base64 bytes to <var>destination</var>.
         * The source and destination arrays can be manipulated
         * anywhere along their length by specifying
         * <var>srcOffset</var> and <var>destOffset</var>.
         * This method does not check to make sure your arrays
         * are large enough to accomodate <var>srcOffset</var> + 3 for
         * the <var>source</var> array or <var>destOffset</var> + 4 for
         * the <var>destination</var> array.
         * The actual number of significant bytes in your array is
         * given by <var>numSigBytes</var>.
         * @param source      the array to convert
         * @param srcOffset   the index where conversion begins
         * @param numSigBytes the number of significant bytes in your array
         * @param destination the array to hold the conversion
         * @param destOffset  the index where output will be put
         * @return the <var>destination</var> array
         * @since 1.3
         */
        private static byte[] encode3to4(
                byte[] source, int srcOffset, int numSigBytes,
                byte[] destination, int destOffset) {
            //           1         2         3
            // 01234567890123456789012345678901 Bit position
            // --------000000001111111122222222 Array position from threeBytes
            // --------|    ||    ||    ||    | Six bit groups to index ALPHABET
            //          >>18  >>12  >> 6  >> 0  Right shift necessary
            //                0x3f  0x3f  0x3f  Additional AND

            // Create buffer with zero-padding if there are only one or two
            // significant bytes passed in the array.
            // We have to shift left 24 in order to flush out the 1's that appear
            // when Java treats a value as negative that is cast from a byte to an int.
            int inBuff=(numSigBytes > 0? ((source[srcOffset] << 24) >>> 8) : 0)
                    | (numSigBytes > 1? ((source[srcOffset + 1] << 24) >>> 16) : 0)
                    | (numSigBytes > 2? ((source[srcOffset + 2] << 24) >>> 24) : 0);

            switch(numSigBytes) {
                case 3:
                    destination[destOffset]=ALPHABET[(inBuff >>> 18)];
                    destination[destOffset + 1]=ALPHABET[(inBuff >>> 12) & 0x3f];
                    destination[destOffset + 2]=ALPHABET[(inBuff >>> 6) & 0x3f];
                    destination[destOffset + 3]=ALPHABET[(inBuff) & 0x3f];
                    return destination;

                case 2:
                    destination[destOffset]=ALPHABET[(inBuff >>> 18)];
                    destination[destOffset + 1]=ALPHABET[(inBuff >>> 12) & 0x3f];
                    destination[destOffset + 2]=ALPHABET[(inBuff >>> 6) & 0x3f];
                    destination[destOffset + 3]=EQUALS_SIGN;
                    return destination;

                case 1:
                    destination[destOffset]=ALPHABET[(inBuff >>> 18)];
                    destination[destOffset + 1]=ALPHABET[(inBuff >>> 12) & 0x3f];
                    destination[destOffset + 2]=EQUALS_SIGN;
                    destination[destOffset + 3]=EQUALS_SIGN;
                    return destination;

                default:
                    return destination;
            }   // end switch
        }   // end encode3to4


        /**
         * Serializes an object and returns the Base64-encoded
         * version of that serialized object. If the object
         * cannot be serialized or there is another error,
         * the method will return <tt>null</tt>.
         * The object is not GZip-compressed before being encoded.
         * @param serializableObject The object to encode
         * @return The Base64-encoded object
         * @since 1.4
         */
        public static String encodeObject(java.io.Serializable serializableObject) {
            return encodeObject(serializableObject, NO_OPTIONS);
        }   // end encodeObject


        /**
         * Serializes an object and returns the Base64-encoded
         * version of that serialized object. If the object
         * cannot be serialized or there is another error,
         * the method will return <tt>null</tt>.
         * <p/>
         * Valid options:<pre>
         *   GZIP: gzip-compresses object before encoding it.
         *   DONT_BREAK_LINES: don't break lines at 76 characters
         *     <i>Note: Technically, this makes your encoding non-compliant.</i>
         * </pre>
         * <p/>
         * Example: <code>encodeObject( myObj, Base64.GZIP )</code> or
         * <p/>
         * Example: <code>encodeObject( myObj, Base64.GZIP | Base64.DONT_BREAK_LINES )</code>
         * @param serializableObject The object to encode
         * @param options            Specified options
         * @return The Base64-encoded object
         * @see Base64#GZIP
         * @see Base64#DONT_BREAK_LINES
         * @since 2.0
         */
        public static String encodeObject(java.io.Serializable serializableObject, int options) {
            // Streams
            java.io.ByteArrayOutputStream baos=null;
            java.io.OutputStream b64os=null;
            java.io.ObjectOutputStream oos=null;
            java.util.zip.GZIPOutputStream gzos=null;

            // Isolate options
            int gzip=(options & GZIP);
            int dontBreakLines=(options & DONT_BREAK_LINES);

            try {
                // ObjectOutputStream -> (GZIP) -> Base64 -> ByteArrayOutputStream
                baos=new java.io.ByteArrayOutputStream();
                b64os=new Base64.OutputStream(baos, ENCODE | dontBreakLines);

                // GZip?
                if(gzip == GZIP) {
                    gzos=new java.util.zip.GZIPOutputStream(b64os);
                    oos=new java.io.ObjectOutputStream(gzos);
                }   // end if: gzip
                else
                    oos=new java.io.ObjectOutputStream(b64os);

                oos.writeObject(serializableObject);
            }   // end try
            catch(java.io.IOException e) {
                e.printStackTrace();
                return null;
            }   // end catch
            finally {
                try {
                    oos.close();
                }
                catch(Exception e) {
                }
                try {
                    gzos.close();
                }
                catch(Exception e) {
                }
                try {
                    b64os.close();
                }
                catch(Exception e) {
                }
                try {
                    baos.close();
                }
                catch(Exception e) {
                }
            }   // end finally

            // Return value according to relevant encoding.
            try {
                return new String(baos.toByteArray(), PREFERRED_ENCODING);
            }   // end try
            catch(java.io.UnsupportedEncodingException uue) {
                return new String(baos.toByteArray());
            }   // end catch

        }   // end encode


        /**
         * Encodes a byte array into Base64 notation.
         * Does not GZip-compress data.
         * @param source The data to convert
         * @since 1.4
         */
        public static String encodeBytes(byte[] source) {
            return encodeBytes(source, 0, source.length, NO_OPTIONS);
        }   // end encodeBytes


        /**
         * Encodes a byte array into Base64 notation.
         * <p/>
         * Valid options:<pre>
         *   GZIP: gzip-compresses object before encoding it.
         *   DONT_BREAK_LINES: don't break lines at 76 characters
         *     <i>Note: Technically, this makes your encoding non-compliant.</i>
         * </pre>
         * <p/>
         * Example: <code>encodeBytes( myData, Base64.GZIP )</code> or
         * <p/>
         * Example: <code>encodeBytes( myData, Base64.GZIP | Base64.DONT_BREAK_LINES )</code>
         * @param source  The data to convert
         * @param options Specified options
         * @see Base64#GZIP
         * @see Base64#DONT_BREAK_LINES
         * @since 2.0
         */
        public static String encodeBytes(byte[] source, int options) {
            return encodeBytes(source, 0, source.length, options);
        }   // end encodeBytes


        /**
         * Encodes a byte array into Base64 notation.
         * Does not GZip-compress data.
         * @param source The data to convert
         * @param off    Offset in array where conversion should begin
         * @param len    Length of data to convert
         * @since 1.4
         */
        public static String encodeBytes(byte[] source, int off, int len) {
            return encodeBytes(source, off, len, NO_OPTIONS);
        }   // end encodeBytes


        /**
         * Encodes a byte array into Base64 notation.
         * <p/>
         * Valid options:<pre>
         *   GZIP: gzip-compresses object before encoding it.
         *   DONT_BREAK_LINES: don't break lines at 76 characters
         *     <i>Note: Technically, this makes your encoding non-compliant.</i>
         * </pre>
         * <p/>
         * Example: <code>encodeBytes( myData, Base64.GZIP )</code> or
         * <p/>
         * Example: <code>encodeBytes( myData, Base64.GZIP | Base64.DONT_BREAK_LINES )</code>
         * @param source  The data to convert
         * @param off     Offset in array where conversion should begin
         * @param len     Length of data to convert
         * @param options Specified options
         * @see Base64#GZIP
         * @see Base64#DONT_BREAK_LINES
         * @since 2.0
         */
        public static String encodeBytes(byte[] source, int off, int len, int options) {
            // Isolate options
            int dontBreakLines=(options & DONT_BREAK_LINES);
            int gzip=(options & GZIP);

            // Compress?
            if(gzip == GZIP) {
                java.io.ByteArrayOutputStream baos=null;
                java.util.zip.GZIPOutputStream gzos=null;
                Base64.OutputStream b64os=null;


                try {
                    // GZip -> Base64 -> ByteArray
                    baos=new java.io.ByteArrayOutputStream();
                    b64os=new Base64.OutputStream(baos, ENCODE | dontBreakLines);
                    gzos=new java.util.zip.GZIPOutputStream(b64os);

                    gzos.write(source, off, len);
                    gzos.close();
                }   // end try
                catch(java.io.IOException e) {
                    e.printStackTrace();
                    return null;
                }   // end catch
                finally {
                    try {
                        gzos.close();
                    }
                    catch(Exception e) {
                    }
                    try {
                        b64os.close();
                    }
                    catch(Exception e) {
                    }
                    try {
                        baos.close();
                    }
                    catch(Exception e) {
                    }
                }   // end finally

                // Return value according to relevant encoding.
                try {
                    return new String(baos.toByteArray(), PREFERRED_ENCODING);
                }   // end try
                catch(java.io.UnsupportedEncodingException uue) {
                    return new String(baos.toByteArray());
                }   // end catch
            }   // end if: compress

            // Else, don't compress. Better not to use streams at all then.
            else {
                // Convert option to boolean in way that code likes it.
                boolean breakLines=dontBreakLines == 0;

                int len43=len * 4 / 3;
                byte[] outBuff=new byte[(len43)                      // Main 4:3
                        + ((len % 3) > 0? 4 : 0)      // Account for padding
                        + (breakLines? (len43 / MAX_LINE_LENGTH) : 0)]; // New lines
                int d=0;
                int e=0;
                int len2=len - 2;
                int lineLength=0;
                for(; d < len2; d+=3, e+=4) {
                    encode3to4(source, d + off, 3, outBuff, e);

                    lineLength+=4;
                    if(breakLines && lineLength == MAX_LINE_LENGTH) {
                        outBuff[e + 4]=NEW_LINE;
                        e++;
                        lineLength=0;
                    }   // end if: end of line
                }   // en dfor: each piece of array

                if(d < len) {
                    encode3to4(source, d + off, len - d, outBuff, e);
                    e+=4;
                }   // end if: some padding needed


                // Return value according to relevant encoding.
                try {
                    return new String(outBuff, 0, e, PREFERRED_ENCODING);
                }   // end try
                catch(java.io.UnsupportedEncodingException uue) {
                    return new String(outBuff, 0, e);
                }   // end catch

            }   // end else: don't compress

        }   // end encodeBytes


/* ********  D E C O D I N G   M E T H O D S  ******** */


        /**
         * Decodes four bytes from array <var>source</var>
         * and writes the resulting bytes (up to three of them)
         * to <var>destination</var>.
         * The source and destination arrays can be manipulated
         * anywhere along their length by specifying
         * <var>srcOffset</var> and <var>destOffset</var>.
         * This method does not check to make sure your arrays
         * are large enough to accomodate <var>srcOffset</var> + 4 for
         * the <var>source</var> array or <var>destOffset</var> + 3 for
         * the <var>destination</var> array.
         * This method returns the actual number of bytes that
         * were converted from the Base64 encoding.
         * @param source      the array to convert
         * @param srcOffset   the index where conversion begins
         * @param destination the array to hold the conversion
         * @param destOffset  the index where output will be put
         * @return the number of decoded bytes converted
         * @since 1.3
         */
        private static int decode4to3(byte[] source, int srcOffset, byte[] destination, int destOffset) {
            // Example: Dk==
            if(source[srcOffset + 2] == EQUALS_SIGN) {
                // Two ways to do the same thing. Don't know which way I like best.
                //int outBuff =   ( ( DECODABET[ source[ srcOffset    ] ] << 24 ) >>>  6 )
                //              | ( ( DECODABET[ source[ srcOffset + 1] ] << 24 ) >>> 12 );
                int outBuff=((DECODABET[source[srcOffset]] & 0xFF) << 18)
                        | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12);

                destination[destOffset]=(byte)(outBuff >>> 16);
                return 1;
            }

            // Example: DkL=
            else if(source[srcOffset + 3] == EQUALS_SIGN) {
                // Two ways to do the same thing. Don't know which way I like best.
                //int outBuff =   ( ( DECODABET[ source[ srcOffset     ] ] << 24 ) >>>  6 )
                //              | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 )
                //              | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 );
                int outBuff=((DECODABET[source[srcOffset]] & 0xFF) << 18)
                        | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12)
                        | ((DECODABET[source[srcOffset + 2]] & 0xFF) << 6);

                destination[destOffset]=(byte)(outBuff >>> 16);
                destination[destOffset + 1]=(byte)(outBuff >>> 8);
                return 2;
            }

            // Example: DkLE
            else {
                try {
                    // Two ways to do the same thing. Don't know which way I like best.
                    //int outBuff =   ( ( DECODABET[ source[ srcOffset     ] ] << 24 ) >>>  6 )
                    //              | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 )
                    //              | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 )
                    //              | ( ( DECODABET[ source[ srcOffset + 3 ] ] << 24 ) >>> 24 );
                    int outBuff=((DECODABET[source[srcOffset]] & 0xFF) << 18)
                            | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12)
                            | ((DECODABET[source[srcOffset + 2]] & 0xFF) << 6)
                            | ((DECODABET[source[srcOffset + 3]] & 0xFF));


                    destination[destOffset]=(byte)(outBuff >> 16);
                    destination[destOffset + 1]=(byte)(outBuff >> 8);
                    destination[destOffset + 2]=(byte)(outBuff);

                    return 3;
                }
                catch(Exception e) {
                    System.out.println(valueOf(source[srcOffset]) + ": " + (DECODABET[source[srcOffset]]));
                    System.out.println(valueOf(source[srcOffset + 1]) + ": " + (DECODABET[source[srcOffset + 1]]));
                    System.out.println(valueOf(source[srcOffset + 2]) + ": " + (DECODABET[source[srcOffset + 2]]));
                    System.out.println(String.valueOf(source[srcOffset + 3]) + ": " + (DECODABET[source[srcOffset + 3]]));
                    return -1;
                }   //e nd catch
            }
        }   // end decodeToBytes


        /**
         * Very low-level access to decoding ASCII characters in
         * the form of a byte array. Does not support automatically
         * gunzipping or any other "fancy" features.
         * @param source The Base64 encoded data
         * @param off    The offset of where to begin decoding
         * @param len    The length of characters to decode
         * @return decoded data
         * @since 1.3
         */
        public static byte[] decode(byte[] source, int off, int len) {
            int len34=len * 3 / 4;
            byte[] outBuff=new byte[len34]; // Upper limit on size of output
            int outBuffPosn=0;

            byte[] b4=new byte[4];
            int b4Posn=0;
            int i=0;
            byte sbiCrop=0;
            byte sbiDecode=0;
            for(i=off; i < off + len; i++) {
                sbiCrop=(byte)(source[i] & 0x7f); // Only the low seven bits
                sbiDecode=DECODABET[sbiCrop];

                if(sbiDecode >= WHITE_SPACE_ENC) // White space, Equals sign or better
                {
                    if(sbiDecode >= EQUALS_SIGN_ENC) {
                        b4[b4Posn++]=sbiCrop;
                        if(b4Posn > 3) {
                            outBuffPosn+=decode4to3(b4, 0, outBuff, outBuffPosn);
                            b4Posn=0;

                            // If that was the equals sign, break out of 'for' loop
                            if(sbiCrop == EQUALS_SIGN)
                                break;
                        }   // end if: quartet built

                    }   // end if: equals sign or better

                }   // end if: white space, equals sign or better
                else {
                    System.err.println("Bad Base64 input character at " + i + ": " + source[i] + "(decimal)");
                    return null;
                }   // end else:
            }   // each input character

            byte[] out=new byte[outBuffPosn];
            System.arraycopy(outBuff, 0, out, 0, outBuffPosn);
            return out;
        }   // end decode


        /**
         * Decodes data from Base64 notation, automatically
         * detecting gzip-compressed data and decompressing it.
         * @param s the string to decode
         * @return the decoded data
         * @since 1.4
         */
        public static byte[] decode(String s) {
            byte[] bytes;
            try {
                bytes=s.getBytes(PREFERRED_ENCODING);
            }   // end try
            catch(java.io.UnsupportedEncodingException uee) {
                bytes=s.getBytes();
            }   // end catch
            //</change>

            // Decode
            bytes=decode(bytes, 0, bytes.length);


            // Check to see if it's gzip-compressed
            // GZIP Magic Two-Byte Number: 0x8b1f (35615)
            if(bytes != null && bytes.length >= 4) {

                int head=((int)bytes[0] & 0xff) | ((bytes[1] << 8) & 0xff00);
                if(java.util.zip.GZIPInputStream.GZIP_MAGIC == head) {
                    java.io.ByteArrayInputStream bais=null;
                    java.util.zip.GZIPInputStream gzis=null;
                    java.io.ByteArrayOutputStream baos=null;
                    byte[] buffer=new byte[2048];
                    int length=0;

                    try {
                        baos=new java.io.ByteArrayOutputStream();
                        bais=new java.io.ByteArrayInputStream(bytes);
                        gzis=new java.util.zip.GZIPInputStream(bais);

                        while((length=gzis.read(buffer)) >= 0) {
                            baos.write(buffer, 0, length);
                        }   // end while: reading input

                        // No error? Get new bytes.
                        bytes=baos.toByteArray();

                    }   // end try
                    catch(java.io.IOException e) {
                        // Just return originally-decoded bytes
                    }   // end catch
                    finally {
                        try {
                            baos.close();
                        }
                        catch(Exception e) {
                        }
                        try {
                            gzis.close();
                        }
                        catch(Exception e) {
                        }
                        try {
                            bais.close();
                        }
                        catch(Exception e) {
                        }
                    }   // end finally

                }   // end if: gzipped
            }   // end if: bytes.length >= 2

            return bytes;
        }   // end decode


        /**
         * Attempts to decode Base64 data and deserialize a Java
         * Object within. Returns <tt>null</tt> if there was an error.
         * @param encodedObject The Base64 data to decode
         * @return The decoded and deserialized object
         * @since 1.5
         */
        public static Object decodeToObject(String encodedObject) {
            // Decode and gunzip if necessary
            byte[] objBytes=decode(encodedObject);

            java.io.ByteArrayInputStream bais=null;
            java.io.ObjectInputStream ois=null;
            Object obj=null;

            try {
                bais=new java.io.ByteArrayInputStream(objBytes);
                ois=new java.io.ObjectInputStream(bais);

                obj=ois.readObject();
            }   // end try
            catch(java.io.IOException e) {
                e.printStackTrace();
                obj=null;
            }   // end catch
            catch(java.lang.ClassNotFoundException e) {
                e.printStackTrace();
                obj=null;
            }   // end catch
            finally {
                try {
                    if(bais != null)
                        bais.close();
                }
                catch(Exception e) {
                }
                try {
                    if(ois != null)
                        ois.close();
                }
                catch(Exception e) {
                }
            }   // end finally

            return obj;
        }   // end decodeObject


        /**
         * Convenience method for encoding data to a file.
         * @param dataToEncode byte array of data to encode in base64 form
         * @param filename     Filename for saving encoded data
         * @return <tt>true</tt> if successful, <tt>false</tt> otherwise
         * @since 2.1
         */
        public static boolean encodeToFile(byte[] dataToEncode, String filename) {
            boolean success=false;
            Base64.OutputStream bos=null;
            try {
                bos=new Base64.OutputStream(
                        new java.io.FileOutputStream(filename), Base64.ENCODE);
                bos.write(dataToEncode);
                success=true;
            }   // end try
            catch(java.io.IOException e) {

                success=false;
            }   // end catch: IOException
            finally {
                try {
                    if(bos != null)
                        bos.close();
                }
                catch(Exception e) {
                }
            }   // end finally

            return success;
        }   // end encodeToFile


        /**
         * Convenience method for decoding data to a file.
         * @param dataToDecode Base64-encoded data as a string
         * @param filename     Filename for saving decoded data
         * @return <tt>true</tt> if successful, <tt>false</tt> otherwise
         * @since 2.1
         */
        public static boolean decodeToFile(String dataToDecode, String filename) {
            boolean success=false;
            Base64.OutputStream bos=null;
            try {
                bos=new Base64.OutputStream(
                        new java.io.FileOutputStream(filename), Base64.DECODE);
                bos.write(dataToDecode.getBytes(PREFERRED_ENCODING));
                success=true;
            }   // end try
            catch(java.io.IOException e) {
                success=false;
            }   // end catch: IOException
            finally {
                try {
                    if(bos != null)
                        bos.close();
                }
                catch(Exception e) {
                }
            }   // end finally

            return success;
        }   // end decodeToFile


        /**
         * Convenience method for reading a base64-encoded
         * file and decoding it.
         * @param filename Filename for reading encoded data
         * @return decoded byte array or null if unsuccessful
         * @since 2.1
         */
        public static byte[] decodeFromFile(String filename) {
            byte[] decodedData=null;
            Base64.InputStream bis=null;
            try {
                // Set up some useful variables
                java.io.File file=new java.io.File(filename);
                byte[] buffer=null;
                int length=0;
                int numBytes=0;

                // Check for size of file
                if(file.length() > Integer.MAX_VALUE) {
                    System.err.println("File is too big for this convenience method (" + file.length() + " bytes).");
                    return null;
                }   // end if: file too big for int index
                buffer=new byte[(int)file.length()];

                // Open a stream
                bis=new Base64.InputStream(
                        new java.io.BufferedInputStream(
                                new java.io.FileInputStream(file)), Base64.DECODE);

                // Read until done
                while((numBytes=bis.read(buffer, length, 4096)) >= 0)
                    length+=numBytes;

                // Save in a variable to return
                decodedData=new byte[length];
                System.arraycopy(buffer, 0, decodedData, 0, length);

            }   // end try
            catch(java.io.IOException e) {
                System.err.println("Error decoding from file " + filename);
            }   // end catch: IOException
            finally {
                try {
                    if(bis != null)
                        bis.close();
                }
                catch(Exception e) {
                }
            }   // end finally

            return decodedData;
        }   // end decodeFromFile


        /**
         * Convenience method for reading a binary file
         * and base64-encoding it.
         * @param filename Filename for reading binary data
         * @return base64-encoded string or null if unsuccessful
         * @since 2.1
         */
        public static String encodeFromFile(String filename) {
            String encodedData=null;
            Base64.InputStream bis=null;
            try {
                // Set up some useful variables
                java.io.File file=new java.io.File(filename);
                byte[] buffer=new byte[(int)(file.length() * 1.4)];
                int length=0;
                int numBytes=0;

                // Open a stream
                bis=new Base64.InputStream(
                        new java.io.BufferedInputStream(
                                new java.io.FileInputStream(file)), Base64.ENCODE);

                // Read until done
                while((numBytes=bis.read(buffer, length, 4096)) >= 0)
                    length+=numBytes;

                // Save in a variable to return
                encodedData=new String(buffer, 0, length, Base64.PREFERRED_ENCODING);

            }   // end try
            catch(java.io.IOException e) {
                System.err.println("Error encoding from file " + filename);
            }   // end catch: IOException
            finally {
                try {
                    if(bis != null)
                        bis.close();
                }
                catch(Exception e) {
                }
            }   // end finally

            return encodedData;
        }   // end encodeFromFile


        /* ********  I N N E R   C L A S S   I N P U T S T R E A M  ******** */


        /**
         * A {@link Base64.InputStream} will read data from another
         * <tt>java.io.InputStream</tt>, given in the constructor,
         * and encode/decode to/from Base64 notation on the fly.
         * @see Base64
         * @since 1.3
         */
        public static class InputStream extends java.io.FilterInputStream {
            private boolean encode;         // Encoding or decoding
            private int position;       // Current position in the buffer
            private byte[] buffer;         // Small buffer holding converted data
            private int bufferLength;   // Length of buffer (3 or 4)
            private int numSigBytes;    // Number of meaningful bytes in the buffer
            private int lineLength;
            private boolean breakLines;     // Break lines at less than 80 characters


            /**
             * Constructs a {@link Base64.InputStream} in DECODE mode.
             * @param in the <tt>java.io.InputStream</tt> from which to read data.
             * @since 1.3
             */
            public InputStream(java.io.InputStream in) {
                this(in, DECODE);
            }   // end constructor


            /**
             * Constructs a {@link Base64.InputStream} in
             * either ENCODE or DECODE mode.
             * <p/>
             * Valid options:<pre>
             *   ENCODE or DECODE: Encode or Decode as data is read.
             *   DONT_BREAK_LINES: don't break lines at 76 characters
             *     (only meaningful when encoding)
             *     <i>Note: Technically, this makes your encoding non-compliant.</i>
             * </pre>
             * <p/>
             * Example: <code>new Base64.InputStream( in, Base64.DECODE )</code>
             * @param in      the <tt>java.io.InputStream</tt> from which to read data.
             * @param options Specified options
             * @see Base64#ENCODE
             * @see Base64#DECODE
             * @see Base64#DONT_BREAK_LINES
             * @since 2.0
             */
            public InputStream(java.io.InputStream in, int options) {
                super(in);
                this.breakLines=(options & DONT_BREAK_LINES) != DONT_BREAK_LINES;
                this.encode=(options & ENCODE) == ENCODE;
                this.bufferLength=encode? 4 : 3;
                this.buffer=new byte[bufferLength];
                this.position=-1;
                this.lineLength=0;
            }   // end constructor

            /**
             * Reads enough of the input stream to convert
             * to/from Base64 and returns the next byte.
             * @return next byte
             * @since 1.3
             */
            public int read() throws java.io.IOException {
                // Do we need to get data?
                if(position < 0) {
                    if(encode) {
                        byte[] b3=new byte[3];
                        int numBinaryBytes=0;
                        for(int i=0; i < 3; i++) {
                            try {
                                int b=in.read();

                                // If end of stream, b is -1.
                                if(b >= 0) {
                                    b3[i]=(byte)b;
                                    numBinaryBytes++;
                                }   // end if: not end of stream

                            }   // end try: read
                            catch(java.io.IOException e) {
                                // Only a problem if we got no data at all.
                                if(i == 0)
                                    throw e;

                            }   // end catch
                        }   // end for: each needed input byte

                        if(numBinaryBytes > 0) {
                            encode3to4(b3, 0, numBinaryBytes, buffer, 0);
                            position=0;
                            numSigBytes=4;
                        }   // end if: got data
                        else {
                            return -1;
                        }   // end else
                    }   // end if: encoding

                    // Else decoding
                    else {
                        byte[] b4=new byte[4];
                        int i=0;
                        for(i=0; i < 4; i++) {
                            // Read four "meaningful" bytes:
                            int b=0;
                            do {
                                b=in.read();
                            }
                            while(b >= 0 && DECODABET[b & 0x7f] <= WHITE_SPACE_ENC);

                            if(b < 0)
                                break; // Reads a -1 if end of stream

                            b4[i]=(byte)b;
                        }   // end for: each needed input byte

                        if(i == 4) {
                            numSigBytes=decode4to3(b4, 0, buffer, 0);
                            position=0;
                        }   // end if: got four characters
                        else if(i == 0) {
                            return -1;
                        }   // end else if: also padded correctly
                        else {
                            // Must have broken out from above.
                            throw new java.io.IOException("Improperly padded Base64 input.");
                        }   // end

                    }   // end else: decode
                }   // end else: get data

                // Got data?
                if(position >= 0) {
                    // End of relevant data?
                    if( /*!encode &&*/ position >= numSigBytes)
                        return -1;

                    if(encode && breakLines && lineLength >= MAX_LINE_LENGTH) {
                        lineLength=0;
                        return '\n';
                    }   // end if
                    else {
                        lineLength++;   // This isn't important when decoding
                        // but throwing an extra "if" seems
                        // just as wasteful.

                        int b=buffer[position++];

                        if(position >= bufferLength)
                            position=-1;

                        return b & 0xFF; // This is how you "cast" a byte that's
                        // intended to be unsigned.
                    }   // end else
                }   // end if: position >= 0

                // Else error
                else {
                    // When JDK1.4 is more accepted, use an assertion here.
                    throw new java.io.IOException("Error in Base64 code reading stream.");
                }   // end else
            }   // end read


            /**
             * Calls {@link #read()} repeatedly until the end of stream
             * is reached or <var>len</var> bytes are read.
             * Returns number of bytes read into array or -1 if
             * end of stream is encountered.
             * @param dest array to hold values
             * @param off  offset for array
             * @param len  max number of bytes to read into array
             * @return bytes read into array or -1 if end of stream is encountered.
             * @since 1.3
             */
            public int read(byte[] dest, int off, int len) throws java.io.IOException {
                int i;
                int b;
                for(i=0; i < len; i++) {
                    b=read();

                    //if( b < 0 && i == 0 )
                    //    return -1;

                    if(b >= 0)
                        dest[off + i]=(byte)b;
                    else if(i == 0)
                        return -1;
                    else
                        break; // Out of 'for' loop
                }   // end for: each byte read
                return i;
            }   // end read

        }   // end inner class InputStream


        /* ********  I N N E R   C L A S S   O U T P U T S T R E A M  ******** */


        /**
         * A {@link Base64.OutputStream} will write data to another
         * <tt>java.io.OutputStream</tt>, given in the constructor,
         * and encode/decode to/from Base64 notation on the fly.
         * @see Base64
         * @since 1.3
         */
        public static class OutputStream extends java.io.FilterOutputStream {
            private boolean encode;
            private int position;
            private byte[] buffer;
            private int bufferLength;
            private int lineLength;
            private boolean breakLines;
            private byte[] b4; // Scratch used in a few places
            private boolean suspendEncoding;

            /**
             * Constructs a {@link Base64.OutputStream} in ENCODE mode.
             * @param out the <tt>java.io.OutputStream</tt> to which data will be written.
             * @since 1.3
             */
            public OutputStream(java.io.OutputStream out) {
                this(out, ENCODE);
            }   // end constructor


            /**
             * Constructs a {@link Base64.OutputStream} in
             * either ENCODE or DECODE mode.
             * <p/>
             * Valid options:<pre>
             *   ENCODE or DECODE: Encode or Decode as data is read.
             *   DONT_BREAK_LINES: don't break lines at 76 characters
             *     (only meaningful when encoding)
             *     <i>Note: Technically, this makes your encoding non-compliant.</i>
             * </pre>
             * <p/>
             * Example: <code>new Base64.OutputStream( out, Base64.ENCODE )</code>
             * @param out     the <tt>java.io.OutputStream</tt> to which data will be written.
             * @param options Specified options.
             * @see Base64#ENCODE
             * @see Base64#DECODE
             * @see Base64#DONT_BREAK_LINES
             * @since 1.3
             */
            public OutputStream(java.io.OutputStream out, int options) {
                super(out);
                this.breakLines=(options & DONT_BREAK_LINES) != DONT_BREAK_LINES;
                this.encode=(options & ENCODE) == ENCODE;
                this.bufferLength=encode? 3 : 4;
                this.buffer=new byte[bufferLength];
                this.position=0;
                this.lineLength=0;
                this.suspendEncoding=false;
                this.b4=new byte[4];
            }   // end constructor


            /**
             * Writes the byte to the output stream after
             * converting to/from Base64 notation.
             * When encoding, bytes are buffered three
             * at a time before the output stream actually
             * gets a write() call.
             * When decoding, bytes are buffered four
             * at a time.
             * @param theByte the byte to write
             * @since 1.3
             */
            public void write(int theByte) throws java.io.IOException {
                // Encoding suspended?
                if(suspendEncoding) {
                    super.out.write(theByte);
                    return;
                }   // end if: supsended

                // Encode?
                if(encode) {
                    buffer[position++]=(byte)theByte;
                    if(position >= bufferLength)  // Enough to encode.
                    {
                        out.write(encode3to4(b4, buffer, bufferLength));

                        lineLength+=4;
                        if(breakLines && lineLength >= MAX_LINE_LENGTH) {
                            out.write(NEW_LINE);
                            lineLength=0;
                        }   // end if: end of line

                        position=0;
                    }   // end if: enough to output
                }   // end if: encoding

                // Else, Decoding
                else {
                    // Meaningful Base64 character?
                    if(DECODABET[theByte & 0x7f] > WHITE_SPACE_ENC) {
                        buffer[position++]=(byte)theByte;
                        if(position >= bufferLength)  // Enough to output.
                        {
                            int len=Base64.decode4to3(buffer, 0, b4, 0);
                            out.write(b4, 0, len);
                            //out.write( Base64.decode4to3( buffer ) );
                            position=0;
                        }   // end if: enough to output
                    }   // end if: meaningful base64 character
                    else if(DECODABET[theByte & 0x7f] != WHITE_SPACE_ENC) {
                        throw new java.io.IOException("Invalid character in Base64 data.");
                    }   // end else: not white space either
                }   // end else: decoding
            }   // end write


            /**
             * Calls {@link #write(int)} repeatedly until <var>len</var>
             * bytes are written.
             * @param theBytes array from which to read bytes
             * @param off      offset for array
             * @param len      max number of bytes to read into array
             * @since 1.3
             */
            public void write(byte[] theBytes, int off, int len) throws java.io.IOException {
                // Encoding suspended?
                if(suspendEncoding) {
                    super.out.write(theBytes, off, len);
                    return;
                }   // end if: supsended

                for(int i=0; i < len; i++) {
                    write(theBytes[off + i]);
                }   // end for: each byte written

            }   // end write


            /**
             * Method added by PHIL. [Thanks, PHIL. -Rob]
             * This pads the buffer without closing the stream.
             */
            public void flushBase64() throws java.io.IOException {
                if(position > 0) {
                    if(encode) {
                        out.write(encode3to4(b4, buffer, position));
                        position=0;
                    }   // end if: encoding
                    else {
                        throw new java.io.IOException("Base64 input not properly padded.");
                    }   // end else: decoding
                }   // end if: buffer partially full

            }   // end flush


            /**
             * Flushes and closes (I think, in the superclass) the stream.
             * @since 1.3
             */
            public void close() throws java.io.IOException {
                // 1. Ensure that pending characters are written
                flushBase64();

                // 2. Actually close the stream
                // Base class both flushes and closes.
                super.close();

                buffer=null;
                out=null;
            }   // end close


            /**
             * Suspends encoding of the stream.
             * May be helpful if you need to embed a piece of
             * base640-encoded data in a stream.
             * @since 1.5.1
             */
            public void suspendEncoding() throws java.io.IOException {
                flushBase64();
                this.suspendEncoding=true;
            }   // end suspendEncoding


            /**
             * Resumes encoding of the stream.
             * May be helpful if you need to embed a piece of
             * base640-encoded data in a stream.
             * @since 1.5.1
             */
            public void resumeEncoding() {
                this.suspendEncoding=false;
            }   // end resumeEncoding


        }   // end inner class OutputStream


    }   // end class Base64

}





