com.emc.esu.api.rest.AbstractEsuRestApi.java Source code

Java tutorial

Introduction

Here is the source code for com.emc.esu.api.rest.AbstractEsuRestApi.java

Source

/*
 * Copyright 2014 EMC Corporation. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 * http://www.apache.org/licenses/LICENSE-2.0.txt
 *
 * or in the "license" file accompanying this file. This file is distributed
 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */
package com.emc.esu.api.rest;

import com.emc.esu.api.*;
import com.emc.esu.api.Grantee.GRANT_TYPE;
import com.emc.util.HttpUtil;
import org.apache.commons.codec.binary.Base64;
import org.apache.log4j.Logger;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.Namespace;
import org.jdom.input.SAXBuilder;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.net.*;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Encapsulates common REST API functionality that is independant of 
 * the transport layer, e.g. signature generation and getShareableUrl.
 * @author Jason Cwik
 */
public abstract class AbstractEsuRestApi implements EsuApi {
    private static final DateFormat HEADER_FORMAT = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss z",
            Locale.ENGLISH);
    private static final String ISO8601_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";
    private static final Pattern OBJECTID_EXTRACTOR = Pattern.compile("/\\w+/objects/([0-9a-f]{44,})");
    private static final Logger l4j = Logger.getLogger(AbstractEsuRestApi.class);

    protected String host;
    protected int port;
    protected String uid;
    protected byte[] secret;

    protected String context = "/rest";
    protected String proto;

    protected boolean unicodeEnabled = false;

    protected boolean readChecksum;

    private long serverOffset;

    /**
     * Creates a new AbstractEsuRestApi
     * @param host the host running the web services
     * @param port the port number, e.g. 80 or 443
     * @param uid the web service UID
     * @param sharedSecret the UID's shared secret key
     */
    public AbstractEsuRestApi(String host, int port, String uid, String sharedSecret) {
        try {
            this.secret = Base64.decodeBase64(sharedSecret.getBytes("UTF-8"));
        } catch (UnsupportedEncodingException e) {
            throw new EsuException("Could not decode shared secret", e);
        }
        this.host = host;
        this.uid = uid;
        this.port = port;

        if (port == 443) {
            proto = "https";
        } else {
            proto = "http";
        }
    }

    /**
     * Gets the context root of the REST api. By default this is /rest.
     * 
     * @return the context
     */
    public String getContext() {
        return context;
    }

    /**
     * Overrides the default context root of the REST api.
     * 
     * @param context the context to set
     */
    public void setContext(String context) {
        this.context = context;
    }

    /**
     * Returns the protocol being used (http or https).
     * 
     * @return the proto
     */
    public String getProtocol() {
        return proto;
    }

    /**
     * Overrides the protocol selection. By default, https will be used for port
     * 443. Http will be used otherwise
     * 
     * @param proto the proto to set
     */
    public void setProtocol(String proto) {
        this.proto = proto;
    }

    /**
     * Creates a new object in the cloud.
     * 
     * @param acl Access control list for the new object. May be null to use a
     *            default ACL
     * @param metadata Metadata for the new object. May be null for no metadata.
     * @param data The initial contents of the object. May be appended to later.
     *            May be null to create an object with no content.
     * @param mimeType the MIME type of the content. Optional, may be null. If
     *            data is non-null and mimeType is null, the MIME type will
     *            default to application/octet-stream.
     * @return Identifier of the newly created object.
     * @throws EsuException if the request fails.
     */
    public ObjectId createObject(Acl acl, MetadataList metadata, byte[] data, String mimeType) {
        return createObjectFromSegment(acl, metadata, data == null ? null : new BufferSegment(data), mimeType,
                null);
    }

    /**
     * Creates a new object in the cloud.
     * 
     * @param acl Access control list for the new object. May be null to use a
     *            default ACL
     * @param metadata Metadata for the new object. May be null for no metadata.
     * @param data The initial contents of the object. May be appended to later.
     *            May be null to create an object with no content.
     * @param mimeType the MIME type of the content. Optional, may be null. If
     *            data is non-null and mimeType is null, the MIME type will
     *            default to application/octet-stream.
     * @param checksum if not null, use the Checksum object to compute
     * the checksum for the create object request.  If appending
     * to the object with subsequent requests, use the same
     * checksum object for each request.
     * @return Identifier of the newly created object.
     * @throws EsuException if the request fails.
     */
    public ObjectId createObject(Acl acl, MetadataList metadata, byte[] data, String mimeType, Checksum checksum) {
        return createObjectFromSegment(acl, metadata, data == null ? null : new BufferSegment(data), mimeType,
                checksum);
    }

    /**
     * Creates a new object in the cloud using a BufferSegment.
     * 
     * @param acl Access control list for the new object. May be null to use a
     *            default ACL
     * @param metadata Metadata for the new object. May be null for no metadata.
     * @param data The initial contents of the object. May be appended to later.
     *            May be null to create an object with no content.
     * @param mimeType the MIME type of the content. Optional, may be null. If
     *            data is non-null and mimeType is null, the MIME type will
     *            default to application/octet-stream.
     * @return Identifier of the newly created object.
     * @throws EsuException if the request fails.
     */
    public ObjectId createObjectFromSegment(Acl acl, MetadataList metadata, BufferSegment data, String mimeType) {
        return createObjectFromSegment(acl, metadata, data, mimeType, null);
    }

    /**
     * Creates a new object in the cloud on the specified path.
     * 
     * @param path The path to create the object on.
     * @param acl Access control list for the new object. May be null to use a
     *            default ACL
     * @param metadata Metadata for the new object. May be null for no metadata.
     * @param data The initial contents of the object. May be appended to later.
     *            May be null to create an object with no content.
     * @param mimeType the MIME type of the content. Optional, may be null. If
     *            data is non-null and mimeType is null, the MIME type will
     *            default to application/octet-stream.
     * @return the ObjectId of the newly-created object for references by ID.
     * @throws EsuException if the request fails.
     */
    public ObjectId createObjectOnPath(ObjectPath path, Acl acl, MetadataList metadata, byte[] data,
            String mimeType) {
        return createObjectFromSegmentOnPath(path, acl, metadata, data == null ? null : new BufferSegment(data),
                mimeType, null);

    }

    /**
     * Creates a new object in the cloud on the specified path.
     * 
     * @param path The path to create the object on.
     * @param acl Access control list for the new object. May be null to use a
     *            default ACL
     * @param metadata Metadata for the new object. May be null for no metadata.
     * @param data The initial contents of the object. May be appended to later.
     *            May be null to create an object with no content.
     * @param mimeType the MIME type of the content. Optional, may be null. If
     *            data is non-null and mimeType is null, the MIME type will
     *            default to application/octet-stream.
     * @param checksum if not null, use the Checksum object to compute
     * the checksum for the create object request.  If appending
     * to the object with subsequent requests, use the same
     * checksum object for each request.
     * @return the ObjectId of the newly-created object for references by ID.
     * @throws EsuException if the request fails.
     */
    public ObjectId createObjectOnPath(ObjectPath path, Acl acl, MetadataList metadata, byte[] data,
            String mimeType, Checksum checksum) {
        return createObjectFromSegmentOnPath(path, acl, metadata, data == null ? null : new BufferSegment(data),
                mimeType, checksum);

    }

    /**
     * Creates a new object in the cloud using a BufferSegment on the given
     * path.
     * 
     * @param path the path to create the object on.
     * @param acl Access control list for the new object. May be null to use a
     *            default ACL
     * @param metadata Metadata for the new object. May be null for no metadata.
     * @param data The initial contents of the object. May be appended to later.
     *            May be null to create an object with no content.
     * @param mimeType the MIME type of the content. Optional, may be null. If
     *            data is non-null and mimeType is null, the MIME type will
     *            default to application/octet-stream.
     * @return the ObjectId of the newly-created object for references by ID.
     * @throws EsuException if the request fails.
     */
    public ObjectId createObjectFromSegmentOnPath(ObjectPath path, Acl acl, MetadataList metadata,
            BufferSegment data, String mimeType) {
        return createObjectFromSegmentOnPath(path, acl, metadata, data, mimeType, null);
    }

    /**
     * Updates an object in the cloud and optionally its metadata and ACL.
     * 
     * @param id The ID of the object to update
     * @param acl Access control list for the new object. Optional, default is
     *            NULL to leave the ACL unchanged.
     * @param metadata Metadata list for the new object. Optional, default is
     *            NULL for no changes to the metadata.
     * @param data The new contents of the object. May be appended to later.
     *            Optional, default is NULL (no content changes).
     * @param extent portion of the object to update. May be null to indicate
     *            the whole object is to be replaced. If not null, the extent
     *            size must match the data size.
     * @param mimeType the MIME type of the content. Optional, may be null. If
     *            data is non-null and mimeType is null, the MIME type will
     *            default to application/octet-stream.
     * @throws EsuException if the request fails.
     */
    public void updateObject(Identifier id, Acl acl, MetadataList metadata, Extent extent, byte[] data,
            String mimeType) {
        updateObjectFromSegment(id, acl, metadata, extent, data == null ? null : new BufferSegment(data), mimeType,
                null);
    }

    /**
     * Updates an object in the cloud and optionally its metadata and ACL.
     * 
     * @param id The ID of the object to update
     * @param acl Access control list for the new object. Optional, default is
     *            NULL to leave the ACL unchanged.
     * @param metadata Metadata list for the new object. Optional, default is
     *            NULL for no changes to the metadata.
     * @param data The new contents of the object. May be appended to later.
     *            Optional, default is NULL (no content changes).
     * @param extent portion of the object to update. May be null to indicate
     *            the whole object is to be replaced. If not null, the extent
     *            size must match the data size.
     * @param mimeType the MIME type of the content. Optional, may be null. If
     *            data is non-null and mimeType is null, the MIME type will
     *            default to application/octet-stream.
     * @param checksum if not null, use the Checksum object to compute
     * the checksum for the update object request.  If appending
     * to the object with subsequent requests, use the same
     * checksum object for each request.
     * @throws EsuException if the request fails.
     */
    public void updateObject(Identifier id, Acl acl, MetadataList metadata, Extent extent, byte[] data,
            String mimeType, Checksum checksum) {
        updateObjectFromSegment(id, acl, metadata, extent, data == null ? null : new BufferSegment(data), mimeType,
                checksum);
    }

    /**
     * Updates an object in the cloud and optionally its metadata and ACL.
     * 
     * @param id The ID of the object to update
     * @param acl Access control list for the new object. Optional, default is
     *            NULL to leave the ACL unchanged.
     * @param metadata Metadata list for the new object. Optional, default is
     *            NULL for no changes to the metadata.
     * @param data The new contents of the object. May be appended to later.
     *            Optional, default is NULL (no content changes).
     * @param extent portion of the object to update. May be null to indicate
     *            the whole object is to be replaced. If not null, the extent
     *            size must match the data size.
     * @param mimeType the MIME type of the content. Optional, may be null. If
     *            data is non-null and mimeType is null, the MIME type will
     *            default to application/octet-stream.
     * @throws EsuException if the request fails.
     */
    public void updateObjectFromSegment(Identifier id, Acl acl, MetadataList metadata, Extent extent,
            BufferSegment data, String mimeType) {
        updateObjectFromSegment(id, acl, metadata, extent, data, mimeType, null);
    }

    /**
     * Reads an object's content.
     * 
     * @param id the identifier of the object whose content to read.
     * @param extent the portion of the object data to read. Optional. Default
     *            is null to read the entire object.
     * @param buffer the buffer to use to read the extent. Must be large enough
     *            to read the response or an error will be thrown. If null, a
     *            buffer will be allocated to hold the response data. If you
     *            pass a buffer that is larger than the extent, only
     *            extent.getSize() bytes will be valid.
     * @return the object data read as a byte array.
     */
    public byte[] readObject(Identifier id, Extent extent, byte[] buffer) {
        return readObject(id, extent, buffer, null);
    }

    /**
     * Lists all objects with the given tag.
     * 
     * @param tag the tag to search for
     * @return The list of objects with the given tag. If no objects are found
     *         the array will be empty.
     * @throws EsuException if no objects are found (code 1003)
     */
    public List<Identifier> listObjects(MetadataTag tag) {
        return filterIdList(listObjects(tag.getName(), null));
    }

    /**
      * Lists all objects with the given tag.
      * 
      * @param tag the tag to search for
      * @param options the options for listing the objects
      * @return The list of objects with the given tag. If no objects are found
      *         the array will be empty.
      * @throws EsuException if no objects are found (code 1003)
      */
    public List<ObjectResult> listObjects(MetadataTag tag, ListOptions options) {
        return listObjects(tag.getName(), options);
    }

    /**
     * Lists all objects with the given tag.
     * 
     * @param tag the tag to search for
     * @return The list of objects with the given tag. If no objects are found
     *         the array will be empty.
     * @throws EsuException if no objects are found (code 1003)
     */
    public List<Identifier> listObjects(String tag) {
        return filterIdList(listObjects(tag, null));
    }

    /**
      * Lists all objects with the given tag and returns both their IDs and their
      * metadata.
      * 
      * @param tag the tag to search for
      * @return The list of objects with the given tag. If no objects are found
      *         the array will be empty.
      */
    public List<ObjectResult> listObjectsWithMetadata(MetadataTag tag) {
        return listObjectsWithMetadata(tag.getName());
    }

    /**
     * Lists all objects with the given tag and returns both their IDs and their
     * metadata.
     * 
     * @param tag the tag to search for
     * @return The list of objects with the given tag. If no objects are found
     *         the array will be empty.
     */
    public List<ObjectResult> listObjectsWithMetadata(String tag) {
        ListOptions options = new ListOptions();
        options.setIncludeMetadata(true);
        return listObjects(tag, options);
    }

    /**
     * Lists the contents of a directory.
     * @param path the path to list.  Must be a directory.
     * @return the directory entries in the directory.
     * @deprecated Use the version with ListOptions to control the result
     * count and handle large result sets.
     */
    public List<DirectoryEntry> listDirectory(ObjectPath path) {
        return listDirectory(path, null);
    }

    /**
     * Generates an HMAC-SHA1 signature of the given input string using the
     * shared secret key.
     * @param input the string to sign
     * @return the HMAC-SHA1 signature in Base64 format
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeyException
     * @throws IllegalStateException
     * @throws UnsupportedEncodingException
     */
    public String sign(String input) throws NoSuchAlgorithmException, InvalidKeyException, IllegalStateException,
            UnsupportedEncodingException {
        // Compute the signature hash
        l4j.debug("Hashing: \n" + input.toString());

        String hashOut = sign(input.getBytes("UTF-8"));

        l4j.debug("Hash: " + hashOut);

        return hashOut;
    }

    public String sign(byte[] input)
            throws UnsupportedEncodingException, InvalidKeyException, NoSuchAlgorithmException {
        Mac mac = Mac.getInstance("HmacSHA1");
        SecretKeySpec key = new SecretKeySpec(secret, "HmacSHA1");
        mac.init(key);

        byte[] hashData = mac.doFinal(input);

        // Encode the hash in Base64.
        return new String(Base64.encodeBase64(hashData), "UTF-8");
    }

    /**
     * An Atmos user (UID) can construct a pre-authenticated URL to an 
     * object, which may then be used by anyone to retrieve the 
     * object (e.g., through a browser). This allows an Atmos user 
     * to let a non-Atmos user download a specific object. The 
     * entire object/file is read.
     * @param id the object to generate the URL for
     * @param expiration the expiration date of the URL
     * @return a URL that can be used to share the object's content
     */
    public URL getShareableUrl(Identifier id, Date expiration, String disposition) {
        try {
            String resource = getResourcePath(context, id);

            StringBuffer sb = new StringBuffer();
            sb.append("GET\n");
            sb.append(resource.toLowerCase() + "\n");
            sb.append(uid + "\n");
            sb.append("" + (expiration.getTime() / 1000));
            if (disposition != null) {
                sb.append("\n" + disposition);
            }

            String signature = sign(sb.toString());
            String query = "uid=" + encodeUtf8(uid) + "&expires=" + (expiration.getTime() / 1000) + "&signature="
                    + encodeUtf8(signature);
            if (disposition != null) {
                disposition = encodeUtf8(disposition);

                query += "&disposition=" + disposition;
            }

            // We do this a little strangely here.  Technically, the trailing "=" in the Base-64 signature
            // should be encoded since it's a "reserved" character.  Atmos 1.2 is strict about this, but
            // 1.3 relaxes the rules a bit.  Most URL generators (java.net.URI included) don't have facilities
            // to break down the query components and encode them individually.  Therefore, we encode the
            // query ourselves here and append it to the generated URL.  This will then work with both
            // 1.2 and 1.3.
            URL u = buildUrl(resource, null);
            u = new URL(u.toString() + "?" + query);

            l4j.debug("URL: " + u);

            return u;
        } catch (UnsupportedEncodingException e) {
            throw new EsuException("Unsupported encoding", e);
        } catch (InvalidKeyException e) {
            throw new EsuException("Invalid secret key", e);
        } catch (NoSuchAlgorithmException e) {
            throw new EsuException("Missing signature algorithm", e);
        } catch (IllegalStateException e) {
            throw new EsuException("Error signing request", e);
        } catch (MalformedURLException e) {
            throw new EsuException("Invalid URL format", e);
        } catch (URISyntaxException e) {
            throw new EsuException("Invalid URL", e);
        }
    }

    /**
     * An Atmos user (UID) can construct a pre-authenticated URL to an 
     * object, which may then be used by anyone to retrieve the 
     * object (e.g., through a browser). This allows an Atmos user 
     * to let a non-Atmos user download a specific object. The 
     * entire object/file is read.
     * @param id the object to generate the URL for
     * @param expiration the expiration date of the URL
     * @return a URL that can be used to share the object's content
     */
    public URL getShareableUrl(Identifier id, Date expiration) {
        return getShareableUrl(id, expiration, null);
    }

    /**
     * Gets the appropriate resource path depending on identifier
     * type.
     */
    protected String getResourcePath(String ctx, Identifier id) {
        if (id instanceof ObjectId) {
            return ctx + "/objects/" + id;
        } else {
            return ctx + "/namespace" + id;
        }
    }

    /**
     * Builds a new URL to the given resource
     * @throws URISyntaxException 
     * @throws MalformedURLException 
     */
    protected URL buildUrl(String resource, String query) throws URISyntaxException, MalformedURLException {
        int uriport = 0;
        if ("http".equals(proto) && port == 80) {
            // Default port
            uriport = -1;
        } else if ("https".equals(proto) && port == 443) {
            uriport = -1;
        } else {
            uriport = port;
        }
        URI uri = new URI(proto, null, host, uriport, resource, query, null);
        l4j.debug("URI: " + uri);
        URL u = new URL(uri.toASCIIString());
        l4j.debug("URL: " + u);
        return u;
    }

    /**
     * Helper method that closes a stream ignoring errors.
     * @param out the OutputStream to close
     */
    protected void silentClose(OutputStream out) {
        if (out == null) {
            return;
        }
        try {
            out.close();
        } catch (IOException e) {
            // ignore
        }
    }

    /**
     * Parses the given header text and appends to the metadata list
     * @param meta the metadata list to append to
     * @param header the metadata header to parse
     * @param listable true if the header being parsed contains listable metadata.
     * @throws UnsupportedEncodingException 
     */
    protected void readMetadata(MetadataList meta, String header, boolean listable)
            throws UnsupportedEncodingException {
        if (header == null) {
            return;
        }

        String[] attrs = header.split(",(?=[^,]+=)");
        for (int i = 0; i < attrs.length; i++) {
            String[] nvpair = attrs[i].split("=", 2);
            String name = nvpair[0];
            String value = nvpair.length > 1 ? nvpair[1] : null;

            name = name.trim();

            if (unicodeEnabled) {
                name = decodeUtf8(name);
                value = decodeUtf8(value);
            }

            Metadata m = new Metadata(name, value, listable);
            l4j.debug("Meta: " + m);
            meta.addMetadata(m);
        }
    }

    /**
     * Enumerates the given list of metadata tags and sets the x-emc-tags
     * header.
     * @param tags the tag list to enumerate
     * @param headers the HTTP request headers
     * @throws UnsupportedEncodingException
     */
    protected void processTags(MetadataTags tags, Map<String, String> headers) throws UnsupportedEncodingException {
        StringBuffer taglist = new StringBuffer();

        l4j.debug("Processing " + tags.count() + " metadata tag entries");

        if (unicodeEnabled) {
            headers.put("x-emc-utf8", "true");
        }

        for (Iterator<MetadataTag> i = tags.iterator(); i.hasNext();) {
            MetadataTag tag = i.next();
            if (taglist.length() > 0) {
                taglist.append(",");
            }
            taglist.append(unicodeEnabled ? encodeUtf8(tag.getName()) : tag.getName());
        }

        if (taglist.length() > 0) {
            headers.put("x-emc-tags", taglist.toString());
        }
    }

    /**
     * Parses the value of an ACL response header and builds an ACL
     * @param acl a reference to the ACL to append to
     * @param header the acl response header
     * @param type the type of Grantees in the header (user or group)
     */
    protected void readAcl(Acl acl, String header, GRANT_TYPE type) {
        l4j.debug("readAcl: " + header);
        String[] grants = header.split(",");
        for (int i = 0; i < grants.length; i++) {
            String[] nvpair = grants[i].split("=", 2);
            String grantee = nvpair[0];
            String permission = nvpair[1];

            grantee = grantee.trim();

            // Currently, the server returns "FULL" instead of "FULL_CONTROL".
            // For consistency, change this to value use in the request
            if ("FULL".equals(permission)) {
                permission = Permission.FULL_CONTROL;
            }

            l4j.debug("grant: " + grantee + "." + permission + " (" + type + ")");

            Grantee ge = new Grantee(grantee, type);
            Grant gr = new Grant(ge, permission);
            l4j.debug("Grant: " + gr);
            acl.addGrant(gr);
        }
    }

    /**
     * Parses an XML response and extracts the list of ObjectIDs.
     * @param response the response byte array to parse as XML
     * @return the list of object IDs contained in the response.
     */
    @SuppressWarnings("rawtypes")
    protected List<ObjectId> parseObjectList(byte[] response) {
        List<ObjectId> objs = new ArrayList<ObjectId>();

        // Use JDOM to parse the XML
        SAXBuilder sb = new SAXBuilder();
        try {
            Document d = sb.build(new ByteArrayInputStream(response));

            // The ObjectID element is part of a namespace so we need to use
            // the namespace to identify the elements.
            Namespace esuNs = Namespace.getNamespace("http://www.emc.com/cos/");

            List children = d.getRootElement().getChildren("ObjectID", esuNs);

            l4j.debug("Found " + children.size() + " objects");
            for (Iterator i = children.iterator(); i.hasNext();) {
                Object o = i.next();
                if (o instanceof Element) {
                    ObjectId oid = new ObjectId(((Element) o).getText());
                    l4j.debug(oid.toString());
                    objs.add(oid);
                } else {
                    l4j.debug(o + " is not an Element!");
                }
            }

        } catch (JDOMException e) {
            throw new EsuException("Error parsing response", e);
        } catch (IOException e) {
            throw new EsuException("Error reading response", e);
        }

        return objs;
    }

    @SuppressWarnings("rawtypes")
    protected List<Identifier> parseVersionList(byte[] response) {
        List<Identifier> objs = new ArrayList<Identifier>();

        // Use JDOM to parse the XML
        SAXBuilder sb = new SAXBuilder();
        try {
            Document d = sb.build(new ByteArrayInputStream(response));

            // The ObjectID element is part of a namespace so we need to use
            // the namespace to identify the elements.
            Namespace esuNs = Namespace.getNamespace("http://www.emc.com/cos/");

            List children = d.getRootElement().getChildren("Ver", esuNs);

            l4j.debug("Found " + children.size() + " objects");
            for (Iterator i = children.iterator(); i.hasNext();) {
                Object o = i.next();
                if (o instanceof Element) {
                    Element objectIdElement = (Element) ((Element) o).getChildren("OID", esuNs).get(0);
                    ObjectId oid = new ObjectId(objectIdElement.getText());
                    l4j.debug(oid.toString());
                    objs.add(oid);
                } else {
                    l4j.debug(o + " is not an Element!");
                }
            }

        } catch (JDOMException e) {
            throw new EsuException("Error parsing response", e);
        } catch (IOException e) {
            throw new EsuException("Error reading response", e);
        }

        return objs;
    }

    @SuppressWarnings("rawtypes")
    protected List<Version> parseVersionListLong(byte[] response) {
        List<Version> objs = new ArrayList<Version>();
        DateFormat itimeParser = new SimpleDateFormat(ISO8601_FORMAT);
        itimeParser.setTimeZone(TimeZone.getTimeZone("UTC"));

        // Use JDOM to parse the XML
        SAXBuilder sb = new SAXBuilder();
        try {
            Document d = sb.build(new ByteArrayInputStream(response));

            // The ObjectID element is part of a namespace so we need to use
            // the namespace to identify the elements.
            Namespace esuNs = Namespace.getNamespace("http://www.emc.com/cos/");

            List children = d.getRootElement().getChildren("Ver", esuNs);

            l4j.debug("Found " + children.size() + " objects");
            for (Iterator i = children.iterator(); i.hasNext();) {
                Object o = i.next();
                if (o instanceof Element) {
                    Element e = (Element) o;
                    ObjectId id = new ObjectId(e.getChildText("OID", esuNs));
                    int versionNumber = Integer.parseInt(e.getChildText("VerNum", esuNs));
                    String sitime = e.getChildText("itime", esuNs);
                    Date itime = null;
                    try {
                        itime = itimeParser.parse(sitime);
                    } catch (ParseException e1) {
                        throw new EsuException("Could not parse itime: " + sitime, e1);
                    }

                    objs.add(new Version(id, versionNumber, itime));
                } else {
                    l4j.debug(o + " is not an Element!");
                }
            }

        } catch (JDOMException e) {
            throw new EsuException("Error parsing response", e);
        } catch (IOException e) {
            throw new EsuException("Error reading response", e);
        }

        return objs;
    }

    /**
     * Parses an XML response and extracts the list of ObjectIDs
     * and metadata.
     * @param response the response byte array to parse as XML
     * @return the list of object IDs contained in the response.
     */
    @SuppressWarnings("rawtypes")
    protected List<ObjectResult> parseObjectListWithMetadata(byte[] response) {
        List<ObjectResult> objs = new ArrayList<ObjectResult>();

        // Use JDOM to parse the XML
        SAXBuilder sb = new SAXBuilder();
        try {
            Document d = sb.build(new ByteArrayInputStream(response));

            // The ObjectID element is part of a namespace so we need to use
            // the namespace to identify the elements.
            Namespace esuNs = Namespace.getNamespace("http://www.emc.com/cos/");

            List children = d.getRootElement().getChildren("Object", esuNs);

            l4j.debug("Found " + children.size() + " objects");
            for (Iterator i = children.iterator(); i.hasNext();) {
                Object o = i.next();
                if (o instanceof Element) {
                    Element e = (Element) o;
                    ObjectResult obj = new ObjectResult();
                    Element objectIdElement = e.getChild("ObjectID", esuNs);
                    ObjectId oid = new ObjectId(objectIdElement.getText());
                    obj.setId(oid);

                    // next, get metadata
                    Element sMeta = e.getChild("SystemMetadataList", esuNs);
                    Element uMeta = e.getChild("UserMetadataList", esuNs);
                    obj.setMetadata(new MetadataList());

                    if (sMeta != null) {
                        for (Iterator m = sMeta.getChildren("Metadata", esuNs).iterator(); m.hasNext();) {
                            Element metaElement = (Element) m.next();

                            String mName = metaElement.getChildText("Name", esuNs);
                            String mValue = metaElement.getChildText("Value", esuNs);

                            obj.getMetadata().addMetadata(new Metadata(mName, mValue, false));
                        }
                    }

                    if (uMeta != null) {
                        for (Iterator m = uMeta.getChildren("Metadata", esuNs).iterator(); m.hasNext();) {
                            Element metaElement = (Element) m.next();

                            String mName = metaElement.getChildText("Name", esuNs);
                            String mValue = metaElement.getChildText("Value", esuNs);
                            String mListable = metaElement.getChildText("Listable", esuNs);

                            obj.getMetadata().addMetadata(new Metadata(mName, mValue, "true".equals(mListable)));
                        }
                    }

                    objs.add(obj);
                } else {
                    l4j.debug(o + " is not an Element!");
                }
            }

        } catch (JDOMException e) {
            throw new EsuException("Error parsing response", e);
        } catch (IOException e) {
            throw new EsuException("Error reading response", e);
        }

        return objs;
    }

    @SuppressWarnings("rawtypes")
    protected List<DirectoryEntry> parseDirectoryListing(byte[] data, ObjectPath basePath) {

        // Parse
        List<DirectoryEntry> objs = new ArrayList<DirectoryEntry>();

        // Use JDOM to parse the XML
        SAXBuilder sb = new SAXBuilder();
        try {
            Document d = sb.build(new ByteArrayInputStream(data));

            // The ObjectID element is part of a namespace so we need to use
            // the namespace to identify the elements.
            Namespace esuNs = Namespace.getNamespace("http://www.emc.com/cos/");

            List children = d.getRootElement().getChild("DirectoryList", esuNs).getChildren("DirectoryEntry",
                    esuNs);
            l4j.debug("Found " + children.size() + " objects");
            for (Iterator i = children.iterator(); i.hasNext();) {
                Object o = i.next();
                if (o instanceof Element) {
                    DirectoryEntry de = new DirectoryEntry();
                    de.setId(new ObjectId(((Element) o).getChildText("ObjectID", esuNs)));
                    String name = ((Element) o).getChildText("Filename", esuNs);
                    String type = ((Element) o).getChildText("FileType", esuNs);

                    name = basePath.toString() + name;
                    if ("directory".equals(type)) {
                        name += "/";
                    }
                    de.setPath(new ObjectPath(name));
                    de.setType(type);

                    // next, get metadata
                    Element sMeta = ((Element) o).getChild("SystemMetadataList", esuNs);
                    Element uMeta = ((Element) o).getChild("UserMetadataList", esuNs);

                    if (sMeta != null) {
                        de.setSystemMetadata(new MetadataList());

                        for (Iterator m = sMeta.getChildren("Metadata", esuNs).iterator(); m.hasNext();) {
                            Element metaElement = (Element) m.next();

                            String mName = metaElement.getChildText("Name", esuNs);
                            String mValue = metaElement.getChildText("Value", esuNs);

                            de.getSystemMetadata().addMetadata(new Metadata(mName, mValue, false));
                        }
                    }

                    if (uMeta != null) {
                        de.setUserMetadata(new MetadataList());
                        for (Iterator m = uMeta.getChildren("Metadata", esuNs).iterator(); m.hasNext();) {
                            Element metaElement = (Element) m.next();

                            String mName = metaElement.getChildText("Name", esuNs);
                            String mValue = metaElement.getChildText("Value", esuNs);
                            String mListable = metaElement.getChildText("Listable", esuNs);

                            de.getUserMetadata().addMetadata(new Metadata(mName, mValue, "true".equals(mListable)));
                        }
                    }

                    objs.add(de);
                } else {
                    l4j.debug(o + " is not an Element!");
                }
            }

        } catch (JDOMException e) {
            throw new EsuException("Error parsing response", e);
        } catch (IOException e) {
            throw new EsuException("Error reading response", e);
        }

        return objs;
    }

    /**
     * Parses the given header and appends to the list of metadata tags.
     * @param tags the list of metadata tags to append to
     * @param header the header to parse
     * @param listable true if the metadata tags in the header are listable
     * @throws UnsupportedEncodingException
     */
    protected void readTags(MetadataTags tags, String header, boolean listable)
            throws UnsupportedEncodingException {
        if (header == null) {
            return;
        }

        String[] attrs = header.split(",");
        for (int i = 0; i < attrs.length; i++) {
            String attr = attrs[i].trim();
            tags.addTag(new MetadataTag(unicodeEnabled ? decodeUtf8(attr) : attr, listable));
        }
    }

    /**
     * Iterates through the given metadata and adds the appropriate metadata
     * headers to the request.
     * 
     * @param metadata the metadata to add
     * @param headers the map of request headers.
     * @throws UnsupportedEncodingException 
     */
    protected void processMetadata(MetadataList metadata, Map<String, String> headers)
            throws UnsupportedEncodingException {

        StringBuffer listable = new StringBuffer();
        StringBuffer nonListable = new StringBuffer();

        if (unicodeEnabled) {
            headers.put("x-emc-utf8", "true");
        }

        l4j.debug("Processing " + metadata.count() + " metadata entries");

        for (Iterator<Metadata> i = metadata.iterator(); i.hasNext();) {
            Metadata meta = i.next();
            if (meta.isListable()) {
                if (listable.length() > 0) {
                    listable.append(", ");
                }
                listable.append(formatTag(meta));
            } else {
                if (nonListable.length() > 0) {
                    nonListable.append(", ");
                }
                nonListable.append(formatTag(meta));
            }
        }

        // Only set the headers if there's data
        if (listable.length() > 0) {
            headers.put("x-emc-listable-meta", listable.toString());
        }
        if (nonListable.length() > 0) {
            headers.put("x-emc-meta", nonListable.toString());
        }

    }

    /**
     * Formats a tag value for passing in the header.
     * @throws UnsupportedEncodingException 
     */
    protected String formatTag(Metadata meta) throws UnsupportedEncodingException {
        // strip commas and newlines for now.
        if (unicodeEnabled) {
            String name = encodeUtf8(meta.getName());

            if (meta.getValue() == null) {
                return name + "=";
            }
            String value = encodeUtf8(meta.getValue());
            return name + "=" + value;
        } else {
            if (meta.getValue() == null) {
                return meta.getName() + "=";
            }
            String fixed = meta.getValue().replace("\n", "");
            fixed = fixed.replace(",", "");
            return meta.getName() + "=" + fixed;
        }
    }

    protected String encodeUtf8(String value) throws UnsupportedEncodingException {
        return HttpUtil.encodeUtf8(value);
    }

    protected String decodeUtf8(String value) throws UnsupportedEncodingException {
        return HttpUtil.decodeUtf8(value);
    }

    /**
     * Enumerates the given ACL and creates the appropriate request headers.
     * 
     * @param acl the ACL to enumerate
     * @param headers the set of request headers.
     */
    protected void processAcl(Acl acl, Map<String, String> headers) {
        StringBuffer userGrants = new StringBuffer();
        StringBuffer groupGrants = new StringBuffer();

        for (Iterator<Grant> i = acl.iterator(); i.hasNext();) {
            Grant grant = i.next();
            if (grant.getGrantee().getType() == Grantee.GRANT_TYPE.USER) {
                if (userGrants.length() > 0) {
                    userGrants.append(",");
                }
                userGrants.append(grant.toString());
            } else {
                if (groupGrants.length() > 0) {
                    groupGrants.append(",");
                }
                groupGrants.append(grant.toString());
            }
        }

        headers.put("x-emc-useracl", userGrants.toString());
        headers.put("x-emc-groupacl", groupGrants.toString());
    }

    /**
     * Condenses consecutive spaces into one.
     */
    protected String normalizeSpace(String str) {
        int length = str.length();
        while (true) {
            str = str.replace("  ", " ");
            if (str.length() == length) {
                // unchanged
                break;
            }
            length = str.length();
        }

        // Strip any trailing space
        while (str.endsWith(" ")) {
            str = str.substring(0, str.length() - 1);
        }

        return str;

    }

    /**
      * Gets the current time formatted for HTTP headers
      */
    protected String getDateHeader() {
        TimeZone tz = TimeZone.getTimeZone("GMT");
        l4j.debug("TZ: " + tz);

        // Per the Java documentation, DateFormat objects are not thread safe.
        synchronized (HEADER_FORMAT) {
            HEADER_FORMAT.setTimeZone(tz);
            String dateHeader = HEADER_FORMAT.format(new Date(System.currentTimeMillis() - serverOffset));
            l4j.debug("Date: " + dateHeader);
            return dateHeader;
        }
    }

    protected ObjectId getObjectId(String location) {
        Matcher m = OBJECTID_EXTRACTOR.matcher(location);
        if (m.find()) {
            String vid = m.group(1);
            l4j.debug("vId: " + vid);
            return new ObjectId(vid);
        } else {
            throw new EsuException("Could not find ObjectId in " + location);
        }
    }

    protected byte[] readStream(InputStream in, int contentLength) throws IOException {
        try {
            byte[] output;
            // If we know the content length, read it directly into a buffer.
            if (contentLength != -1) {
                output = new byte[contentLength];

                int c = 0;
                while (c < contentLength) {
                    int read = in.read(output, c, contentLength - c);
                    if (read == -1) {
                        // EOF!
                        throw new EOFException(
                                "EOF reading response at position " + c + " size " + (contentLength - c));
                    }
                    c += read;
                }

                return output;
            } else {
                l4j.debug("Content length is unknown.  Buffering output.");
                // Else, use a ByteArrayOutputStream to collect the response.
                byte[] buffer = new byte[4096];

                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                int c = 0;
                while ((c = in.read(buffer)) != -1) {
                    baos.write(buffer, 0, c);
                }
                baos.close();

                l4j.debug("Buffered " + baos.size() + " response bytes");

                return baos.toByteArray();
            }
        } finally {
            if (in != null) {
                in.close();
            }
        }

    }

    protected ServiceInformation parseServiceInformation(byte[] response, Map<String, List<String>> map) {
        // Use JDOM to parse the XML
        SAXBuilder sb = new SAXBuilder();
        try {
            Document d = sb.build(new ByteArrayInputStream(response));

            ServiceInformation si = new ServiceInformation();

            // The ObjectID element is part of a namespace so we need to use
            // the namespace to identify the elements.
            Namespace esuNs = Namespace.getNamespace("http://www.emc.com/cos/");

            Element ver = d.getRootElement().getChild("Version", esuNs);
            Element atmos = ver.getChild("Atmos", esuNs);

            si.setAtmosVersion(atmos.getTextNormalize());

            // Check for UTF8 support
            for (String key : map.keySet()) {
                if ("x-emc-support-utf8".equalsIgnoreCase(key)) {
                    for (String val : map.get(key)) {
                        if ("true".equalsIgnoreCase(val)) {
                            si.setUnicodeMetadataSupported(true);
                        }
                    }
                }
                if ("x-emc-features".equalsIgnoreCase(key)) {
                    for (String val : map.get(key)) {
                        String[] features = val.split(",");
                        for (String feature : features) {
                            si.addFeature(feature.trim());
                        }
                    }

                }
            }

            return si;
        } catch (JDOMException e) {
            throw new EsuException("Error parsing response", e);
        } catch (IOException e) {
            throw new EsuException("Error reading response", e);
        }
    }

    /**
     * Converts an ObjectResult list to an Identifier list.
     */
    private List<Identifier> filterIdList(List<ObjectResult> list) {
        List<Identifier> result = new ArrayList<Identifier>(list.size());

        for (ObjectResult r : list) {
            result.add(r.getId());
        }

        return result;
    }

    /**
     * Joins a list of Strings using a delimiter (similar to PERL, PHP, etc)
     * @param list the list of Strings
     * @param delimiter the string to join the list with
     * @return the joined String.
     */
    protected String join(List<String> list, String delimiter) {
        boolean first = true;
        StringBuffer sb = new StringBuffer();

        for (String s : list) {
            if (first) {
                first = false;
            } else {
                sb.append(delimiter);
            }
            sb.append(s);
        }

        return sb.toString();
    }

    /**
     * @return the readChecksum
     */
    public boolean isReadChecksum() {
        return readChecksum;
    }

    /**
     * Turns read checksum verification on or off.  Note that 
     * checksums are only returned from the server for erasure coded objects.
     * @param readChecksum the readChecksum to set
     */
    public void setReadChecksum(boolean readChecksum) {
        this.readChecksum = readChecksum;
    }

    /**
     * Returns true if unicode metadata processing is enabled.
     */
    public boolean isUnicodeEnabled() {
        return unicodeEnabled;
    }

    /**
     * Set to true to enable Unicode metadata processing.  
     */
    public void setUnicodeEnabled(boolean unicodeEnabled) {
        this.unicodeEnabled = unicodeEnabled;
    }

    /**
     * Gets the current server offset in milliseconds.  This value can be used
     * to adjust for clock skew between the client and server.
     * @return the serverOffset
     */
    public long getServerOffset() {
        return serverOffset;
    }

    /**
     * Sets the server offset in millesconds.  This value can be used to
     * adjust for clock skew between the client and the server.
     * @param serverOffset the serverOffset to set
     */
    public void setServerOffset(long serverOffset) {
        this.serverOffset = serverOffset;
    }

    /**
     * Makes a request to the server to get the value of the response Date
     * header.  Compares this date with the local system time to calculate
     * the offset between the client and the server.  You can pass this value
     * to the setServerOffset method to adjust for clock skew.
     * @return the offset between the client and server in milliseconds.  If
     * the client is ahead of the server, this will be positive.  If the server
     * is ahead of the client, it will be negative.
     */
    public abstract long calculateServerOffset();

    //---------- Features supported by the Atmos 2.0 REST API. ----------\\

    @Override
    public ObjectId createObjectWithKey(String keyPool, String key, Acl acl, MetadataList metadata, byte[] data,
            long length, String mimeType) {
        return createObjectWithKeyFromSegment(keyPool, key, acl, metadata, new BufferSegment(data, 0, (int) length),
                mimeType);
    }

    @Override
    public ObjectId createObjectWithKey(String keyPool, String key, Acl acl, MetadataList metadata, byte[] data,
            long length, String mimeType, Checksum checksum) {
        return createObjectWithKeyFromSegment(keyPool, key, acl, metadata, new BufferSegment(data, 0, (int) length),
                mimeType, checksum);
    }

    @Override
    public ObjectId createObjectWithKeyFromSegment(String keyPool, String key, Acl acl, MetadataList metadata,
            BufferSegment data, String mimeType) {
        return createObjectWithKeyFromSegment(keyPool, key, acl, metadata, data, mimeType, null);
    }

    @Override
    public byte[] readObjectWithKey(String keyPool, String key, Extent extent, byte[] buffer) {
        return readObjectWithKey(keyPool, key, extent, buffer, null);
    }

    @Override
    public void updateObjectWithKey(String keyPool, String key, Acl acl, MetadataList metadata, Extent extent,
            byte[] data, String mimeType) {
        updateObjectWithKeyFromSegment(keyPool, key, acl, metadata, extent, new BufferSegment(data), mimeType);
    }

    @Override
    public void updateObjectWithKey(String keyPool, String key, Acl acl, MetadataList metadata, Extent extent,
            byte[] data, String mimeType, Checksum checksum) {
        updateObjectWithKeyFromSegment(keyPool, key, acl, metadata, extent, new BufferSegment(data), mimeType,
                checksum);
    }

    @Override
    public void updateObjectWithKeyFromSegment(String keyPool, String key, Acl acl, MetadataList metadata,
            Extent extent, BufferSegment data, String mimeType) {
        updateObjectWithKeyFromSegment(keyPool, key, acl, metadata, extent, data, mimeType, null);
    }
}