All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.emc.esu.api.rest.AbstractEsuRestApi Maven / Gradle / Ivy

There is a newer version: 2.2.2
Show newest version
/*
 * 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 java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

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 com.emc.esu.api.Acl;
import com.emc.esu.api.BufferSegment;
import com.emc.esu.api.Checksum;
import com.emc.esu.api.DirectoryEntry;
import com.emc.esu.api.EsuApi;
import com.emc.esu.api.EsuException;
import com.emc.esu.api.Extent;
import com.emc.esu.api.Grant;
import com.emc.esu.api.Grantee;
import com.emc.esu.api.Grantee.GRANT_TYPE;
import com.emc.esu.api.Identifier;
import com.emc.esu.api.ListOptions;
import com.emc.esu.api.Metadata;
import com.emc.esu.api.MetadataList;
import com.emc.esu.api.MetadataTag;
import com.emc.esu.api.MetadataTags;
import com.emc.esu.api.ObjectId;
import com.emc.esu.api.ObjectPath;
import com.emc.esu.api.ObjectResult;
import com.emc.esu.api.Permission;
import com.emc.esu.api.ServiceInformation;
import com.emc.esu.api.Version;

/**
 * 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 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 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 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 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 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 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 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 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 parseObjectList( byte[] response ) {
        List objs = new ArrayList();
        
        // 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 parseVersionList( byte[] response ) {
        List objs = new ArrayList();
        
        // 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 parseVersionListLong( byte[] response ) {
        List objs = new ArrayList();
        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 parseObjectListWithMetadata( byte[] response ) {
        List objs = new ArrayList();
        
        // 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 parseDirectoryListing( byte[] data, 
    		ObjectPath basePath ) {
    	
        // Parse
        List objs = new ArrayList();

        // 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 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 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 {
        // Use %20, not +
        return URLEncoder.encode(value, "UTF-8").replace("+", "%20");
    }
    
    protected String decodeUtf8(String value) throws UnsupportedEncodingException {
        return URLDecoder.decode(value, "UTF-8");
    }

    /**
     * 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 headers) {
        StringBuffer userGrants = new StringBuffer();
        StringBuffer groupGrants = new StringBuffer();

        for (Iterator 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> 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 filterIdList(List list) {
		List result = new ArrayList( 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 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 );
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy