com.microsoft.azure.storage.core.Utility.java Source code

Java tutorial

Introduction

Here is the source code for com.microsoft.azure.storage.core.Utility.java

Source

/**
 * Copyright Microsoft Corporation
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.microsoft.azure.storage.core;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Constructor;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TimeZone;
import java.util.concurrent.TimeoutException;

import javax.crypto.Cipher;
import javax.crypto.CipherOutputStream;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;

import org.xml.sax.SAXException;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.microsoft.azure.storage.Constants;
import com.microsoft.azure.storage.OperationContext;
import com.microsoft.azure.storage.RequestOptions;
import com.microsoft.azure.storage.ResultContinuation;
import com.microsoft.azure.storage.ResultContinuationType;
import com.microsoft.azure.storage.StorageErrorCode;
import com.microsoft.azure.storage.StorageErrorCodeStrings;
import com.microsoft.azure.storage.StorageException;
import com.microsoft.azure.storage.StorageExtendedErrorInformation;

/**
 * RESERVED FOR INTERNAL USE. A class which provides utility methods.
 */
public final class Utility {
    /**
     * Stores a reference to the GMT time zone.
     */
    public static final TimeZone GMT_ZONE = TimeZone.getTimeZone("GMT");

    /**
     * Stores a reference to the UTC time zone.
     */
    public static final TimeZone UTC_ZONE = TimeZone.getTimeZone("UTC");

    /**
     * Stores a reference to the US locale.
     */
    public static final Locale LOCALE_US = Locale.US;

    /**
     * Stores a reference to the RFC1123 date/time pattern.
     */
    private static final String RFC1123_PATTERN = "EEE, dd MMM yyyy HH:mm:ss z";

    /**
     * Stores a reference to the ISO8601 date/time pattern.
     */
    private static final String ISO8601_PATTERN_NO_SECONDS = "yyyy-MM-dd'T'HH:mm'Z'";

    /**
     * Stores a reference to the ISO8601 date/time pattern.
     */
    private static final String ISO8601_PATTERN = "yyyy-MM-dd'T'HH:mm:ss'Z'";

    /**
     * Stores a reference to the Java version of ISO8601_LONG date/time pattern.  The full version cannot be used
     * because Java Dates have millisecond precision.
     */
    private static final String JAVA_ISO8601_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";

    /**
     * List of ports used for path style addressing.
     */
    private static final List<Integer> pathStylePorts = Arrays.asList(10000, 10001, 10002, 10003, 10004, 10100,
            10101, 10102, 10103, 10104, 11000, 11001, 11002, 11003, 11004, 11100, 11101, 11102, 11103, 11104);

    /**
     * Used to create Json parsers and generators.
     */
    private static final JsonFactory jsonFactory = new JsonFactory();

    /**
     * A factory to create SAXParser instances.
     */
    private static final ThreadLocal<SAXParserFactory> saxParserFactory = new ThreadLocal<SAXParserFactory>() {
        @Override
        public SAXParserFactory initialValue() {
            return SAXParserFactory.newInstance();
        }
    };

    /**
     * A factory to create XMLStreamWriter instances.
     */
    private static final XMLOutputFactory xmlOutputFactory = XMLOutputFactory.newInstance();

    /**
     * Stores a reference to the date/time pattern with the greatest precision Java.util.Date is capable of expressing.
     */
    private static final String MAX_PRECISION_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS";

    /**
     * The length of a datestring that matches the MAX_PRECISION_PATTERN.
     */
    private static final int MAX_PRECISION_DATESTRING_LENGTH = MAX_PRECISION_PATTERN.replaceAll("'", "").length();

    /**
     * 
     * Determines the size of an input stream, and optionally calculates the MD5 hash for the stream.
     * 
     * @param sourceStream
     *            A <code>InputStream</code> object that represents the stream to measure.
     * @param writeLength
     *            The number of bytes to read from the stream.
     * @param abandonLength
     *            The number of bytes to read before the analysis is abandoned. Set this value to <code>-1</code> to
     *            force the entire stream to be read. This parameter is provided to support upload thresholds.
     * @param rewindSourceStream
     *            <code>true</code> if the stream should be rewound after it is read; otherwise, <code>false</code>.
     * @param calculateMD5
     *            <code>true</code> if an MD5 hash will be calculated; otherwise, <code>false</code>.
     * 
     * @return A {@link StreamMd5AndLength} object that contains the stream length, and optionally the MD5 hash.
     * 
     * @throws IOException
     *             If an I/O error occurs.
     * @throws StorageException
     *             If a storage service error occurred.
     */
    public static StreamMd5AndLength analyzeStream(final InputStream sourceStream, long writeLength,
            long abandonLength, final boolean rewindSourceStream, final boolean calculateMD5)
            throws IOException, StorageException {
        if (abandonLength < 0) {
            abandonLength = Long.MAX_VALUE;
        }

        if (rewindSourceStream) {
            if (!sourceStream.markSupported()) {
                throw new IllegalArgumentException(SR.INPUT_STREAM_SHOULD_BE_MARKABLE);
            }

            sourceStream.mark(Constants.MAX_MARK_LENGTH);
        }

        MessageDigest digest = null;
        if (calculateMD5) {
            try {
                digest = MessageDigest.getInstance("MD5");
            } catch (final NoSuchAlgorithmException e) {
                // This wont happen, throw fatal.
                throw Utility.generateNewUnexpectedStorageException(e);
            }
        }

        if (writeLength < 0) {
            writeLength = Long.MAX_VALUE;
        }

        final StreamMd5AndLength retVal = new StreamMd5AndLength();
        int count = -1;
        final byte[] retrievedBuff = new byte[Constants.BUFFER_COPY_LENGTH];

        int nextCopy = (int) Math.min(retrievedBuff.length, writeLength - retVal.getLength());
        count = sourceStream.read(retrievedBuff, 0, nextCopy);

        while (nextCopy > 0 && count != -1) {
            if (calculateMD5) {
                digest.update(retrievedBuff, 0, count);
            }
            retVal.setLength(retVal.getLength() + count);

            if (retVal.getLength() > abandonLength) {
                // Abandon operation
                retVal.setLength(-1);
                retVal.setMd5(null);
                break;
            }

            nextCopy = (int) Math.min(retrievedBuff.length, writeLength - retVal.getLength());
            count = sourceStream.read(retrievedBuff, 0, nextCopy);
        }

        if (retVal.getLength() != -1 && calculateMD5) {
            retVal.setMd5(Base64.encode(digest.digest()));
        }

        if (retVal.getLength() != -1 && writeLength > 0) {
            retVal.setLength(Math.min(retVal.getLength(), writeLength));
        }

        if (rewindSourceStream) {
            sourceStream.reset();
            sourceStream.mark(Constants.MAX_MARK_LENGTH);
        }

        return retVal;
    }

    /**
     * Encrypts an input stream up to a given length.
     * Exits early if the encrypted data is longer than the abandon length.
     * 
     * @param sourceStream
     *            A <code>InputStream</code> object that represents the stream to measure.
     * @param targetStream
     *            A <code>ByteArrayOutputStream</code> object that represents the stream to write the encrypted data.
     * @param cipher
     *            The <code>Cipher</code> to use to encrypt the data. 
     * @param writeLength
     *            The number of bytes to read and encrypt from the sourceStream.
     * @param abandonLength
     *            The number of bytes to read before the analysis is abandoned. Set this value to <code>-1</code> to
     *            force the entire stream to be read. This parameter is provided to support upload thresholds.
     * @return
     *            The size of the encrypted stream, or -1 if the encrypted stream would be over the abandonLength.
     * @throws IOException
     *            If an I/O error occurs.
     */
    public static long encryptStreamIfUnderThreshold(final InputStream sourceStream,
            final ByteArrayOutputStream targetStream, Cipher cipher, long writeLength, long abandonLength)
            throws IOException {
        if (abandonLength < 0) {
            abandonLength = Long.MAX_VALUE;
        }

        if (!sourceStream.markSupported()) {
            throw new IllegalArgumentException(SR.INPUT_STREAM_SHOULD_BE_MARKABLE);
        }

        sourceStream.mark(Constants.MAX_MARK_LENGTH);

        if (writeLength < 0) {
            writeLength = Long.MAX_VALUE;
        }

        CipherOutputStream encryptStream = new CipherOutputStream(targetStream, cipher);

        int count = -1;
        long totalEncryptedLength = targetStream.size();
        final byte[] retrievedBuff = new byte[Constants.BUFFER_COPY_LENGTH];

        int nextCopy = (int) Math.min(retrievedBuff.length, writeLength - totalEncryptedLength);
        count = sourceStream.read(retrievedBuff, 0, nextCopy);

        while (nextCopy > 0 && count != -1) {

            // Note: We are flushing the CryptoStream on every write here.  This way, we don't end up encrypting more data than we intend here, if
            // we go over the abandonLength.
            encryptStream.write(retrievedBuff, 0, count);
            encryptStream.flush();
            totalEncryptedLength = targetStream.size();

            if (totalEncryptedLength > abandonLength) {
                // Abandon operation
                break;
            }

            nextCopy = (int) Math.min(retrievedBuff.length, writeLength - totalEncryptedLength);
            count = sourceStream.read(retrievedBuff, 0, nextCopy);
        }

        sourceStream.reset();
        sourceStream.mark(Constants.MAX_MARK_LENGTH);

        encryptStream.close();
        totalEncryptedLength = targetStream.size();
        if (totalEncryptedLength > abandonLength) {
            totalEncryptedLength = -1;
        }

        return totalEncryptedLength;
    }

    /**
     * Asserts a continuation token is of the specified type.
     * 
     * @param continuationToken
     *            A {@link ResultContinuation} object that represents the continuation token whose type is being
     *            examined.
     * @param continuationType
     *            A {@link ResultContinuationType} value that represents the continuation token type being asserted with
     *            the specified continuation token.
     */
    public static void assertContinuationType(final ResultContinuation continuationToken,
            final ResultContinuationType continuationType) {
        if (continuationToken != null) {
            if (!(continuationToken.getContinuationType() == ResultContinuationType.NONE
                    || continuationToken.getContinuationType() == continuationType)) {
                final String errorMessage = String.format(Utility.LOCALE_US, SR.UNEXPECTED_CONTINUATION_TYPE,
                        continuationToken.getContinuationType(), continuationType);
                throw new IllegalArgumentException(errorMessage);
            }
        }
    }

    /**
     * Asserts that a value is not <code>null</code>.
     * 
     * @param param
     *            A <code>String</code> that represents the name of the parameter, which becomes the exception message
     *            text if the <code>value</code> parameter is <code>null</code>.
     * @param value
     *            An <code>Object</code> object that represents the value of the specified parameter. This is the value
     *            being asserted as not <code>null</code>.
     */
    public static void assertNotNull(final String param, final Object value) {
        if (value == null) {
            throw new IllegalArgumentException(String.format(Utility.LOCALE_US, SR.ARGUMENT_NULL_OR_EMPTY, param));
        }
    }

    /**
     * Asserts that the specified string is not <code>null</code> or empty.
     * 
     * @param param
     *            A <code>String</code> that represents the name of the parameter, which becomes the exception message
     *            text if the <code>value</code> parameter is <code>null</code> or an empty string.
     * @param value
     *            A <code>String</code> that represents the value of the specified parameter. This is the value being
     *            asserted as not <code>null</code> and not an empty string.
     */
    public static void assertNotNullOrEmpty(final String param, final String value) {
        assertNotNull(param, value);

        if (Utility.isNullOrEmpty(value)) {
            throw new IllegalArgumentException(String.format(Utility.LOCALE_US, SR.ARGUMENT_NULL_OR_EMPTY, param));
        }
    }

    /**
     * Asserts that the specified integer is in the valid range.
     * 
     * @param param
     *            A <code>String</code> that represents the name of the parameter, which becomes the exception message
     *            text if the <code>value</code> parameter is out of bounds.
     * @param value
     *            The value of the specified parameter.
     * @param min
     *            The minimum value for the specified parameter.
     * @param max
     *            The maximum value for the specified parameter.
     */
    public static void assertInBounds(final String param, final long value, final long min, final long max) {
        if (value < min || value > max) {
            throw new IllegalArgumentException(String.format(SR.PARAMETER_NOT_IN_RANGE, param, min, max));
        }
    }

    /**
     * Asserts that the specified value is greater than or equal to the min value.
     * 
     * @param param
     *            A <code>String</code> that represents the name of the parameter, which becomes the exception message
     *            text if the <code>value</code> parameter is out of bounds.
     * @param value
     *            The value of the specified parameter.
     * @param min
     *            The minimum value for the specified parameter.
     */
    public static void assertGreaterThanOrEqual(final String param, final long value, final long min) {
        if (value < min) {
            throw new IllegalArgumentException(String.format(SR.PARAMETER_SHOULD_BE_GREATER_OR_EQUAL, param, min));
        }
    }

    /**
     * Appends 2 byte arrays.
     * @param arr1
     *          First array.
     * @param arr2
     *          Second array.
     * @return The result byte array.
     */
    public static byte[] binaryAppend(byte[] arr1, byte[] arr2) {
        byte[] result = new byte[arr1.length + arr2.length];

        System.arraycopy(arr1, 0, result, 0, arr1.length);
        System.arraycopy(arr2, 0, result, arr1.length, arr2.length);

        return result;
    }

    /**
     * Returns a value representing whether the maximum execution time would be surpassed.
     * 
     * @param operationExpiryTimeInMs
     *            the time the request expires
     * @return <code>true</code> if the maximum execution time would be surpassed; otherwise, <code>false</code>.
     */
    public static boolean validateMaxExecutionTimeout(Long operationExpiryTimeInMs) {
        return validateMaxExecutionTimeout(operationExpiryTimeInMs, 0);
    }

    /**
     * Returns a value representing whether the maximum execution time would be surpassed.
     * 
     * @param operationExpiryTimeInMs
     *            the time the request expires
     * @param additionalInterval
     *            any additional time required from now
     * @return <code>true</code> if the maximum execution time would be surpassed; otherwise, <code>false</code>.
     */
    public static boolean validateMaxExecutionTimeout(Long operationExpiryTimeInMs, long additionalInterval) {
        if (operationExpiryTimeInMs != null) {
            long currentTime = new Date().getTime();
            return operationExpiryTimeInMs < currentTime + additionalInterval;
        }
        return false;
    }

    /**
     * Returns a value representing the remaining time before the operation expires.
     * 
     * @param operationExpiryTimeInMs
     *            the time the request expires
     * @param timeoutIntervalInMs
     *            the server side timeout interval
     * @return the remaining time before the operation expires
     * @throws StorageException
     *             wraps a TimeoutException if there is no more time remaining
     */
    public static int getRemainingTimeout(Long operationExpiryTimeInMs, Integer timeoutIntervalInMs)
            throws StorageException {
        if (operationExpiryTimeInMs != null) {
            long remainingTime = operationExpiryTimeInMs - new Date().getTime();
            if (remainingTime > Integer.MAX_VALUE) {
                return Integer.MAX_VALUE;
            } else if (remainingTime > 0) {
                return (int) remainingTime;
            } else {
                TimeoutException timeoutException = new TimeoutException(SR.MAXIMUM_EXECUTION_TIMEOUT_EXCEPTION);
                StorageException translatedException = new StorageException(
                        StorageErrorCodeStrings.OPERATION_TIMED_OUT, SR.MAXIMUM_EXECUTION_TIMEOUT_EXCEPTION,
                        Constants.HeaderConstants.HTTP_UNUSED_306, null, timeoutException);
                throw translatedException;
            }
        } else if (timeoutIntervalInMs != null) {
            return timeoutIntervalInMs + Constants.DEFAULT_READ_TIMEOUT;
        } else {
            return Constants.DEFAULT_READ_TIMEOUT;
        }
    }

    /**
     * Returns a value that indicates whether a specified URI is a path-style URI.
     * 
     * @param baseURI
     *            A <code>java.net.URI</code> value that represents the URI being checked.
     * @return <code>true</code> if the specified URI is path-style; otherwise, <code>false</code>.
     */
    public static boolean determinePathStyleFromUri(final URI baseURI) {
        String path = baseURI.getPath();
        if (path != null && path.startsWith("/")) {
            path = path.substring(1);
        }

        // if the path is null or empty, this is not path-style
        if (Utility.isNullOrEmpty(path)) {
            return false;
        }

        // if this contains a port or has a host which is not DNS, this is path-style
        return pathStylePorts.contains(baseURI.getPort()) || !isHostDnsName(baseURI);
    }

    /**
     * Returns a boolean indicating whether the host of the specified URI is DNS.
     * 
     * @param uri
     *            The URI whose host to evaluate.
     * @return <code>true</code> if the host is DNS; otherwise, <code>false</code>.
     */
    private static boolean isHostDnsName(URI uri) {
        String host = uri.getHost();
        for (int i = 0; i < host.length(); i++) {
            char hostChar = host.charAt(i);
            if (!Character.isDigit(hostChar) && !(hostChar == '.')) {
                return true;
            }
        }
        return false;
    }

    /**
     * Reads character data for the Etag element from an XML stream reader.
     * 
     * @param xmlr
     *            An <code>XMLStreamReader</code> object that represents the source XML stream reader.
     * 
     * @return A <code>String</code> that represents the character data for the Etag element.
     */
    public static String formatETag(final String etag) {
        if (etag.startsWith("\"") && etag.endsWith("\"")) {
            return etag;
        } else {
            return String.format("\"%s\"", etag);
        }
    }

    /**
     * Returns an unexpected storage exception.
     * 
     * @param cause
     *            An <code>Exception</code> object that represents the initial exception that caused the unexpected
     *            error.
     * 
     * @return A {@link StorageException} object that represents the unexpected storage exception being thrown.
     */
    public static StorageException generateNewUnexpectedStorageException(final Exception cause) {
        final StorageException exceptionRef = new StorageException(StorageErrorCode.NONE.toString(),
                "Unexpected internal storage client error.", 306, // unused
                null, null);
        exceptionRef.initCause(cause);
        return exceptionRef;
    }

    /**
     * Returns the current GMT date/time String using the RFC1123 pattern.
     * 
     * @return A <code>String</code> that represents the current GMT date/time using the RFC1123 pattern.
     */
    public static String getGMTTime() {
        return getGMTTime(new Date());
    }

    /**
     * Returns the GTM date/time String for the specified value using the RFC1123 pattern.
     * 
     * @param date
     *            A <code>Date</code> object that represents the date to convert to GMT date/time in the RFC1123
     *            pattern.
     * 
     * @return A <code>String</code> that represents the GMT date/time for the specified value using the RFC1123
     *         pattern.
     */
    public static String getGMTTime(final Date date) {
        final DateFormat formatter = new SimpleDateFormat(RFC1123_PATTERN, LOCALE_US);
        formatter.setTimeZone(GMT_ZONE);
        return formatter.format(date);
    }

    /**
     * Returns the UTC date/time String for the specified value using Java's version of the ISO8601 pattern,
     * which is limited to millisecond precision.
     * 
     * @param date
     *            A <code>Date</code> object that represents the date to convert to UTC date/time in Java's version
     *            of the ISO8601 pattern.
     * 
     * @return A <code>String</code> that represents the UTC date/time for the specified value using Java's version
     *            of the ISO8601 pattern.
     */
    public static String getJavaISO8601Time(Date date) {
        final DateFormat formatter = new SimpleDateFormat(JAVA_ISO8601_PATTERN, LOCALE_US);
        formatter.setTimeZone(UTC_ZONE);
        return formatter.format(date);
    }

    /**
     * Returns a <code>JsonGenerator</code> with the specified <code>StringWriter</code>.
     * 
     * @param strWriter
     *            The <code>StringWriter</code> to use to create the <code>JsonGenerator</code> instance.
     * @return A <code>JsonGenerator</code> instance
     * 
     * @throws IOException
     */
    public static JsonGenerator getJsonGenerator(StringWriter strWriter) throws IOException {
        return jsonFactory.createGenerator(strWriter);
    }

    /**
     * Returns a <code>JsonGenerator</code> with the specified <code>OutputStream</code>.
     * 
     * @param outStream
     *            The <code>OutputStream</code> to use to create the <code>JsonGenerator</code> instance.
     * @return A <code>JsonGenerator</code> instance
     * 
     * @throws IOException
     */
    public static JsonGenerator getJsonGenerator(OutputStream outStream) throws IOException {
        return jsonFactory.createGenerator(outStream);
    }

    /**
     * Returns a <code>JsonParser</code> with the specified <code>String</code>. This JsonParser
     * will allow non-numeric numbers.
     * 
     * @param jsonString
     *            The <code>String</code> to use to create the <code>JsonGenerator</code> instance.
     * @return A <code>JsonGenerator</code> instance.
     * 
     * @throws IOException
     */
    public static JsonParser getJsonParser(final String jsonString) throws JsonParseException, IOException {
        JsonParser parser = jsonFactory.createParser(jsonString);

        // allows handling of infinity, -infinity, and NaN for Doubles
        return parser.enable(JsonParser.Feature.ALLOW_NON_NUMERIC_NUMBERS);
    }

    /**
     * Returns a <code>JsonParser</code> with the specified <code>InputStream</code>. This JsonParser
     * will allow non-numeric numbers.
     * 
     * @param inStream
     *            The <code>InputStream</code> to use to create the <code>JsonGenerator</code> instance.
     * @return A <code>JsonGenerator</code> instance.
     * 
     * @throws IOException
     */
    public static JsonParser getJsonParser(final InputStream inStream) throws JsonParseException, IOException {
        JsonParser parser = jsonFactory.createParser(inStream);

        // allows handling of infinity, -infinity, and NaN for Doubles
        return parser.enable(JsonParser.Feature.ALLOW_NON_NUMERIC_NUMBERS);
    }

    /**
     * Returns a namespace aware <code>SAXParser</code>.
     * 
     * @return A <code>SAXParser</code> instance which is namespace aware
     * 
     * @throws ParserConfigurationException
     * @throws SAXException
     */
    public static SAXParser getSAXParser() throws ParserConfigurationException, SAXException {
        saxParserFactory.get().setNamespaceAware(true);
        return saxParserFactory.get().newSAXParser();
    }

    /**
     * Returns the standard header value from the specified connection request, or an empty string if no header value
     * has been specified for the request.
     * 
     * @param conn
     *            An <code>HttpURLConnection</code> object that represents the request.
     * @param headerName
     *            A <code>String</code> that represents the name of the header being requested.
     * 
     * @return A <code>String</code> that represents the header value, or <code>null</code> if there is no corresponding
     *         header value for <code>headerName</code>.
     */
    public static String getStandardHeaderValue(final HttpURLConnection conn, final String headerName) {
        final String headerValue = conn.getRequestProperty(headerName);

        // Coalesce null value
        return headerValue == null ? Constants.EMPTY_STRING : headerValue;
    }

    /**
     * Returns the UTC date/time for the specified value using the ISO8601 pattern.
     * 
     * @param value
     *            A <code>Date</code> object that represents the date to convert to UTC date/time in the ISO8601
     *            pattern. If this value is <code>null</code>, this method returns an empty string.
     * 
     * @return A <code>String</code> that represents the UTC date/time for the specified value using the ISO8601
     *         pattern, or an empty string if <code>value</code> is <code>null</code>.
     */
    public static String getUTCTimeOrEmpty(final Date value) {
        if (value == null) {
            return Constants.EMPTY_STRING;
        }

        final DateFormat iso8601Format = new SimpleDateFormat(ISO8601_PATTERN, LOCALE_US);
        iso8601Format.setTimeZone(UTC_ZONE);

        return iso8601Format.format(value);
    }

    /**
     * Returns a <code>XMLStreamWriter</code> with the specified <code>StringWriter</code>.
     * 
     * @param outWriter
     *            The <code>StringWriter</code> to use to create the <code>XMLStreamWriter</code> instance.
     * @return A <code>XMLStreamWriter</code> instance
     * 
     * @throws XMLStreamException
     */
    public static XMLStreamWriter createXMLStreamWriter(StringWriter outWriter) throws XMLStreamException {
        return xmlOutputFactory.createXMLStreamWriter(outWriter);
    }

    /**
     * Creates an instance of the <code>IOException</code> class using the specified exception.
     * 
     * @param ex
     *            An <code>Exception</code> object that represents the exception used to create the IO exception.
     * 
     * @return A <code>java.io.IOException</code> object that represents the created IO exception.
     */
    public static IOException initIOException(final Exception ex) {
        final IOException retEx = new IOException();
        retEx.initCause(ex);
        return retEx;
    }

    /**
     * Returns a value that indicates whether the specified string is <code>null</code> or empty.
     * 
     * @param value
     *            A <code>String</code> being examined for <code>null</code> or empty.
     * 
     * @return <code>true</code> if the specified value is <code>null</code> or empty; otherwise, <code>false</code>
     */
    public static boolean isNullOrEmpty(final String value) {
        return value == null || value.length() == 0;
    }

    /**
     * Returns a value that indicates whether the specified string is <code>null</code>, empty, or whitespace.
     * 
     * @param value
     *            A <code>String</code> being examined for <code>null</code>, empty, or whitespace.
     * 
     * @return <code>true</code> if the specified value is <code>null</code>, empty, or whitespace; otherwise,
     *         <code>false</code>
     */
    public static boolean isNullOrEmptyOrWhitespace(final String value) {
        return value == null || value.trim().length() == 0;
    }

    /**
     * Parses a connection string and returns its values as a hash map of key/value pairs.
     * 
     * @param parseString
     *            A <code>String</code> that represents the connection string to parse.
     * 
     * @return A <code>java.util.HashMap</code> object that represents the hash map of the key / value pairs parsed from
     *         the connection string.
     */
    public static HashMap<String, String> parseAccountString(final String parseString) {

        // 1. split name value pairs by splitting on the ';' character
        final String[] valuePairs = parseString.split(";");
        final HashMap<String, String> retVals = new HashMap<String, String>();

        // 2. for each field value pair parse into appropriate map entries
        for (int m = 0; m < valuePairs.length; m++) {
            if (valuePairs[m].length() == 0) {
                continue;
            }
            final int equalDex = valuePairs[m].indexOf("=");
            if (equalDex < 1) {
                throw new IllegalArgumentException(SR.INVALID_CONNECTION_STRING);
            }

            final String key = valuePairs[m].substring(0, equalDex);
            final String value = valuePairs[m].substring(equalDex + 1);

            // 2.1 add to map
            retVals.put(key, value);
        }

        return retVals;
    }

    /**
     * Returns a GMT date for the specified string in the RFC1123 pattern.
     * 
     * @param value
     *            A <code>String</code> that represents the string to parse.
     * 
     * @return A <code>Date</code> object that represents the GMT date in the RFC1123 pattern.
     * 
     * @throws ParseException
     *             If the specified string is invalid.
     */
    public static Date parseRFC1123DateFromStringInGMT(final String value) throws ParseException {
        final DateFormat format = new SimpleDateFormat(RFC1123_PATTERN, Utility.LOCALE_US);
        format.setTimeZone(GMT_ZONE);
        return format.parse(value);
    }

    /**
     * Performs safe decoding of the specified string, taking care to preserve each <code>+</code> character, rather
     * than replacing it with a space character.
     * 
     * @param stringToDecode
     *            A <code>String</code> that represents the string to decode.
     * 
     * @return A <code>String</code> that represents the decoded string.
     * 
     * @throws StorageException
     *             If a storage service error occurred.
     */
    public static String safeDecode(final String stringToDecode) throws StorageException {
        if (stringToDecode == null) {
            return null;
        }

        if (stringToDecode.length() == 0) {
            return Constants.EMPTY_STRING;
        }

        try {
            if (stringToDecode.contains("+")) {
                final StringBuilder outBuilder = new StringBuilder();

                int startDex = 0;
                for (int m = 0; m < stringToDecode.length(); m++) {
                    if (stringToDecode.charAt(m) == '+') {
                        if (m > startDex) {
                            outBuilder.append(URLDecoder.decode(stringToDecode.substring(startDex, m),
                                    Constants.UTF8_CHARSET));
                        }

                        outBuilder.append("+");
                        startDex = m + 1;
                    }
                }

                if (startDex != stringToDecode.length()) {
                    outBuilder.append(URLDecoder.decode(stringToDecode.substring(startDex, stringToDecode.length()),
                            Constants.UTF8_CHARSET));
                }

                return outBuilder.toString();
            } else {
                return URLDecoder.decode(stringToDecode, Constants.UTF8_CHARSET);
            }
        } catch (final UnsupportedEncodingException e) {
            throw Utility.generateNewUnexpectedStorageException(e);
        }
    }

    /**
     * Performs safe encoding of the specified string, taking care to insert <code>%20</code> for each space character,
     * instead of inserting the <code>+</code> character.
     * 
     * @param stringToEncode
     *            A <code>String</code> that represents the string to encode.
     * 
     * @return A <code>String</code> that represents the encoded string.
     * 
     * @throws StorageException
     *             If a storage service error occurred.
     */
    public static String safeEncode(final String stringToEncode) throws StorageException {
        if (stringToEncode == null) {
            return null;
        }
        if (stringToEncode.length() == 0) {
            return Constants.EMPTY_STRING;
        }

        try {
            final String tString = URLEncoder.encode(stringToEncode, Constants.UTF8_CHARSET);

            if (stringToEncode.contains(" ")) {
                final StringBuilder outBuilder = new StringBuilder();

                int startDex = 0;
                for (int m = 0; m < stringToEncode.length(); m++) {
                    if (stringToEncode.charAt(m) == ' ') {
                        if (m > startDex) {
                            outBuilder.append(URLEncoder.encode(stringToEncode.substring(startDex, m),
                                    Constants.UTF8_CHARSET));
                        }

                        outBuilder.append("%20");
                        startDex = m + 1;
                    }
                }

                if (startDex != stringToEncode.length()) {
                    outBuilder.append(URLEncoder.encode(stringToEncode.substring(startDex, stringToEncode.length()),
                            Constants.UTF8_CHARSET));
                }

                return outBuilder.toString();
            } else {
                return tString;
            }

        } catch (final UnsupportedEncodingException e) {
            throw Utility.generateNewUnexpectedStorageException(e);
        }
    }

    /**
     * Determines the relative difference between the two specified URIs.
     * 
     * @param baseURI
     *            A <code>java.net.URI</code> object that represents the base URI for which <code>toUri</code> will be
     *            made relative.
     * @param toUri
     *            A <code>java.net.URI</code> object that represents the URI to make relative to <code>baseURI</code>.
     * 
     * @return A <code>String</code> that either represents the relative URI of <code>toUri</code> to
     *         <code>baseURI</code>, or the URI of <code>toUri</code> itself, depending on whether the hostname and
     *         scheme are identical for <code>toUri</code> and <code>baseURI</code>. If the hostname and scheme of
     *         <code>baseURI</code> and <code>toUri</code> are identical, this method returns an unencoded relative URI
     *         such that if appended to <code>baseURI</code>, it will yield <code>toUri</code>. If the hostname or
     *         scheme of <code>baseURI</code> and <code>toUri</code> are not identical, this method returns an unencoded
     *         full URI specified by <code>toUri</code>.
     * 
     * @throws URISyntaxException
     *             If <code>baseURI</code> or <code>toUri</code> is invalid.
     */
    public static String safeRelativize(final URI baseURI, final URI toUri) throws URISyntaxException {
        // For compatibility followed
        // http://msdn.microsoft.com/en-us/library/system.uri.makerelativeuri.aspx

        // if host and scheme are not identical return from uri
        if (!baseURI.getHost().equals(toUri.getHost()) || !baseURI.getScheme().equals(toUri.getScheme())) {
            return toUri.toString();
        }

        final String basePath = baseURI.getPath();
        String toPath = toUri.getPath();

        int truncatePtr = 1;

        // Seek to first Difference
        // int maxLength = Math.min(basePath.length(), toPath.length());
        int m = 0;
        int ellipsesCount = 0;
        for (; m < basePath.length(); m++) {
            if (m >= toPath.length()) {
                if (basePath.charAt(m) == '/') {
                    ellipsesCount++;
                }
            } else {
                if (basePath.charAt(m) != toPath.charAt(m)) {
                    break;
                } else if (basePath.charAt(m) == '/') {
                    truncatePtr = m + 1;
                }
            }
        }

        // ../containername and ../containername/{path} should increment the truncatePtr
        // otherwise toPath will incorrectly begin with /containername
        if (m < toPath.length() && toPath.charAt(m) == '/') {
            // this is to handle the empty directory case with the '/' delimiter
            // for example, ../containername/ and ../containername// should not increment the truncatePtr
            if (!(toPath.charAt(m - 1) == '/' && basePath.charAt(m - 1) == '/')) {
                truncatePtr = m + 1;
            }
        }

        if (m == toPath.length()) {
            // No path difference, return query + fragment
            return new URI(null, null, null, toUri.getQuery(), toUri.getFragment()).toString();
        } else {
            toPath = toPath.substring(truncatePtr);
            final StringBuilder sb = new StringBuilder();
            while (ellipsesCount > 0) {
                sb.append("../");
                ellipsesCount--;
            }

            if (!Utility.isNullOrEmpty(toPath)) {
                sb.append(toPath);
            }

            if (!Utility.isNullOrEmpty(toUri.getQuery())) {
                sb.append("?");
                sb.append(toUri.getQuery());
            }
            if (!Utility.isNullOrEmpty(toUri.getFragment())) {
                sb.append("#");
                sb.append(toUri.getRawFragment());
            }

            return sb.toString();
        }
    }

    /**
     * Serializes the parsed StorageException. If an exception is encountered, returns empty string.
     * 
     * @param ex
     *            The StorageException to serialize.
     * @param opContext 
     *            The operation context which provides the logger.
     */
    public static void logHttpError(StorageException ex, OperationContext opContext) {
        if (Logger.shouldLog(opContext)) {
            try {
                StringBuilder bld = new StringBuilder();
                bld.append("Error response received. ");

                bld.append("HttpStatusCode= ");
                bld.append(ex.getHttpStatusCode());

                bld.append(", HttpStatusMessage= ");
                bld.append(ex.getMessage());

                bld.append(", ErrorCode= ");
                bld.append(ex.getErrorCode());

                StorageExtendedErrorInformation extendedError = ex.getExtendedErrorInformation();
                if (extendedError != null) {
                    bld.append(", ExtendedErrorInformation= {ErrorMessage= ");
                    bld.append(extendedError.getErrorMessage());

                    HashMap<String, String[]> details = extendedError.getAdditionalDetails();
                    if (details != null) {
                        bld.append(", AdditionalDetails= { ");
                        for (Entry<String, String[]> detail : details.entrySet()) {
                            bld.append(detail.getKey());
                            bld.append("= ");

                            for (String value : detail.getValue()) {
                                bld.append(value);
                            }
                            bld.append(",");
                        }
                        bld.setCharAt(bld.length() - 1, '}');
                    }
                    bld.append("}");
                }

                Logger.debug(opContext, bld.toString());
            } catch (Exception e) {
                // Do nothing
            }
        }
    }

    /**
     * Logs the HttpURLConnection request. If an exception is encountered, logs nothing.
     * 
     * @param conn
     *            The HttpURLConnection to serialize.
     * @param opContext 
     *            The operation context which provides the logger.
     */
    public static void logHttpRequest(HttpURLConnection conn, OperationContext opContext) throws IOException {
        if (Logger.shouldLog(opContext)) {
            try {
                StringBuilder bld = new StringBuilder();

                bld.append(conn.getRequestMethod());
                bld.append(" ");
                bld.append(conn.getURL());
                bld.append("\n");

                // The Authorization header will not appear due to a security feature in HttpURLConnection
                for (Map.Entry<String, List<String>> header : conn.getRequestProperties().entrySet()) {
                    if (header.getKey() != null) {
                        bld.append(header.getKey());
                        bld.append(": ");
                    }

                    for (int i = 0; i < header.getValue().size(); i++) {
                        bld.append(header.getValue().get(i));
                        if (i < header.getValue().size() - 1) {
                            bld.append(",");
                        }
                    }
                    bld.append('\n');
                }

                Logger.trace(opContext, bld.toString());
            } catch (Exception e) {
                // Do nothing
            }
        }
    }

    /**
     * Logs the HttpURLConnection response. If an exception is encountered, logs nothing.
     * 
     * @param conn
     *            The HttpURLConnection to serialize.
     * @param opContext 
     *            The operation context which provides the logger.
     */
    public static void logHttpResponse(HttpURLConnection conn, OperationContext opContext) throws IOException {
        if (Logger.shouldLog(opContext)) {
            try {
                StringBuilder bld = new StringBuilder();

                // This map's null key will contain the response code and message
                for (Map.Entry<String, List<String>> header : conn.getHeaderFields().entrySet()) {
                    if (header.getKey() != null) {
                        bld.append(header.getKey());
                        bld.append(": ");
                    }

                    for (int i = 0; i < header.getValue().size(); i++) {
                        bld.append(header.getValue().get(i));
                        if (i < header.getValue().size() - 1) {
                            bld.append(",");
                        }
                    }
                    bld.append('\n');
                }

                Logger.trace(opContext, bld.toString());
            } catch (Exception e) {
                // Do nothing
            }
        }
    }

    /**
     * Trims the specified character from the end of a string.
     * 
     * @param value
     *            A <code>String</code> that represents the string to trim.
     * @param trimChar
     *            The character to trim from the end of the string.
     * 
     * @return The string with the specified character trimmed from the end.
     */
    protected static String trimEnd(final String value, final char trimChar) {
        int stopDex = value.length() - 1;
        while (stopDex > 0 && value.charAt(stopDex) == trimChar) {
            stopDex--;
        }

        return stopDex == value.length() - 1 ? value : value.substring(stopDex);
    }

    /**
     * Trims whitespace from the beginning of a string.
     * 
     * @param value
     *            A <code>String</code> that represents the string to trim.
     * 
     * @return The string with whitespace trimmed from the beginning.
     */
    public static String trimStart(final String value) {
        int spaceDex = 0;
        while (spaceDex < value.length() && value.charAt(spaceDex) == ' ') {
            spaceDex++;
        }

        return value.substring(spaceDex);
    }

    /**
     * Reads data from an input stream and writes it to an output stream, calculates the length of the data written, and
     * optionally calculates the MD5 hash for the data.
     * 
     * @param sourceStream
     *            An <code>InputStream</code> object that represents the input stream to use as the source.
     * @param outStream
     *            An <code>OutputStream</code> object that represents the output stream to use as the destination.
     * @param writeLength
     *            The number of bytes to read from the stream.
     * @param rewindSourceStream
     *            <code>true</code> if the input stream should be rewound <strong>before</strong> it is read; otherwise,
     *            <code>false</code>
     * @param calculateMD5
     *            <code>true</code> if an MD5 hash will be calculated; otherwise, <code>false</code>.
     * @param opContext
     *            An {@link OperationContext} object that represents the context for the current operation. This object
     *            is used to track requests to the storage service, and to provide additional runtime information about
     *            the operation.
     * @param options
     *            A {@link RequestOptions} object that specifies any additional options for the request. Namely, the
     *            maximum execution time.
     * @return A {@link StreamMd5AndLength} object that contains the output stream length, and optionally the MD5 hash.
     * 
     * @throws IOException
     *             If an I/O error occurs.
     * @throws StorageException
     *             If a storage service error occurred.
     */
    public static StreamMd5AndLength writeToOutputStream(final InputStream sourceStream,
            final OutputStream outStream, long writeLength, final boolean rewindSourceStream,
            final boolean calculateMD5, OperationContext opContext, final RequestOptions options)
            throws IOException, StorageException {
        return writeToOutputStream(sourceStream, outStream, writeLength, rewindSourceStream, calculateMD5,
                opContext, options, true);
    }

    /**
     * Reads data from an input stream and writes it to an output stream, calculates the length of the data written, and
     * optionally calculates the MD5 hash for the data.
     * 
     * @param sourceStream
     *            An <code>InputStream</code> object that represents the input stream to use as the source.
     * @param outStream
     *            An <code>OutputStream</code> object that represents the output stream to use as the destination.
     * @param writeLength
     *            The number of bytes to read from the stream.
     * @param rewindSourceStream
     *            <code>true</code> if the input stream should be rewound <strong>before</strong> it is read; otherwise,
     *            <code>false</code>
     * @param calculateMD5
     *            <code>true</code> if an MD5 hash will be calculated; otherwise, <code>false</code>.
     * @param opContext
     *            An {@link OperationContext} object that represents the context for the current operation. This object
     *            is used to track requests to the storage service, and to provide additional runtime information about
     *            the operation.
     * @param options
     *            A {@link RequestOptions} object that specifies any additional options for the request. Namely, the
     *            maximum execution time.
     * @return A {@link StreamMd5AndLength} object that contains the output stream length, and optionally the MD5 hash.
     * 
     * @throws IOException
     *             If an I/O error occurs.
     * @throws StorageException
     *             If a storage service error occurred.
     */
    public static StreamMd5AndLength writeToOutputStream(final InputStream sourceStream,
            final OutputStream outStream, long writeLength, final boolean rewindSourceStream,
            final boolean calculateMD5, OperationContext opContext, final RequestOptions options,
            final Boolean shouldFlush) throws IOException, StorageException {
        return writeToOutputStream(sourceStream, outStream, writeLength, rewindSourceStream, calculateMD5,
                opContext, options, shouldFlush, null /*StorageRequest*/);
    }

    /**
     * Reads data from an input stream and writes it to an output stream, calculates the length of the data written, and
     * optionally calculates the MD5 hash for the data.
     * 
     * @param sourceStream
     *            An <code>InputStream</code> object that represents the input stream to use as the source.
     * @param outStream
     *            An <code>OutputStream</code> object that represents the output stream to use as the destination.
     * @param writeLength
     *            The number of bytes to read from the stream.
     * @param rewindSourceStream
     *            <code>true</code> if the input stream should be rewound <strong>before</strong> it is read; otherwise,
     *            <code>false</code>
     * @param calculateMD5
     *            <code>true</code> if an MD5 hash will be calculated; otherwise, <code>false</code>.
     * @param opContext
     *            An {@link OperationContext} object that represents the context for the current operation. This object
     *            is used to track requests to the storage service, and to provide additional runtime information about
     *            the operation.
     * @param options
     *            A {@link RequestOptions} object that specifies any additional options for the request. Namely, the
     *            maximum execution time.
     * @param request
     *            Used by download resume to set currentRequestByteCount on the request. Otherwise, null is always used.
     * @return A {@link StreamMd5AndLength} object that contains the output stream length, and optionally the MD5 hash.
     * 
     * @throws IOException
     *             If an I/O error occurs.
     * @throws StorageException
     *             If a storage service error occurred.
     */
    public static StreamMd5AndLength writeToOutputStream(final InputStream sourceStream,
            final OutputStream outStream, long writeLength, final boolean rewindSourceStream,
            final boolean calculateMD5, OperationContext opContext, final RequestOptions options,
            final Boolean shouldFlush, StorageRequest<?, ?, Integer> request) throws IOException, StorageException {
        if (rewindSourceStream && sourceStream.markSupported()) {
            sourceStream.reset();
            sourceStream.mark(Constants.MAX_MARK_LENGTH);
        }

        final StreamMd5AndLength retVal = new StreamMd5AndLength();

        if (calculateMD5) {
            try {
                retVal.setDigest(MessageDigest.getInstance("MD5"));
            } catch (final NoSuchAlgorithmException e) {
                // This wont happen, throw fatal.
                throw Utility.generateNewUnexpectedStorageException(e);
            }
        }

        if (writeLength < 0) {
            writeLength = Long.MAX_VALUE;
        }

        final byte[] retrievedBuff = new byte[Constants.BUFFER_COPY_LENGTH];
        int nextCopy = (int) Math.min(retrievedBuff.length, writeLength);
        int count = sourceStream.read(retrievedBuff, 0, nextCopy);

        while (nextCopy > 0 && count != -1) {

            // if maximum execution time would be exceeded
            if (Utility.validateMaxExecutionTimeout(options.getOperationExpiryTimeInMs())) {
                // throw an exception
                TimeoutException timeoutException = new TimeoutException(SR.MAXIMUM_EXECUTION_TIMEOUT_EXCEPTION);
                throw Utility.initIOException(timeoutException);
            }

            if (outStream != null) {
                outStream.write(retrievedBuff, 0, count);
            }

            if (calculateMD5) {
                retVal.getDigest().update(retrievedBuff, 0, count);
            }

            retVal.setLength(retVal.getLength() + count);
            retVal.setCurrentOperationByteCount(retVal.getCurrentOperationByteCount() + count);

            if (request != null) {
                request.setCurrentRequestByteCount(request.getCurrentRequestByteCount() + count);
            }

            nextCopy = (int) Math.min(retrievedBuff.length, writeLength - retVal.getLength());
            count = sourceStream.read(retrievedBuff, 0, nextCopy);
        }

        if (outStream != null && shouldFlush) {
            outStream.flush();
        }

        return retVal;
    }

    /**
     * Private Default Constructor.
     */
    private Utility() {
        // No op
    }

    public static void checkNullaryCtor(Class<?> clazzType) {
        Constructor<?> ctor = null;
        try {
            ctor = clazzType.getDeclaredConstructor((Class<?>[]) null);
        } catch (Exception e) {
            throw new IllegalArgumentException(SR.MISSING_NULLARY_CONSTRUCTOR);
        }

        if (ctor == null) {
            throw new IllegalArgumentException(SR.MISSING_NULLARY_CONSTRUCTOR);
        }
    }

    /**
     * Given a String representing a date in a form of the ISO8601 pattern, generates a Date representing it
     * with up to millisecond precision. 
     * 
     * @param dateString
     *              the <code>String</code> to be interpreted as a <code>Date</code>
     *              
     * @return the corresponding <code>Date</code> object
     */
    public static Date parseDate(String dateString) {
        String pattern = MAX_PRECISION_PATTERN;
        switch (dateString.length()) {
        case 28: // "yyyy-MM-dd'T'HH:mm:ss.SSSSSSS'Z'"-> [2012-01-04T23:21:59.1234567Z] length = 28
        case 27: // "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'"-> [2012-01-04T23:21:59.123456Z] length = 27
        case 26: // "yyyy-MM-dd'T'HH:mm:ss.SSSSS'Z'"-> [2012-01-04T23:21:59.12345Z] length = 26
        case 25: // "yyyy-MM-dd'T'HH:mm:ss.SSSS'Z'"-> [2012-01-04T23:21:59.1234Z] length = 25
        case 24: // "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"-> [2012-01-04T23:21:59.123Z] length = 24
            dateString = dateString.substring(0, MAX_PRECISION_DATESTRING_LENGTH);
            break;
        case 23: // "yyyy-MM-dd'T'HH:mm:ss.SS'Z'"-> [2012-01-04T23:21:59.12Z] length = 23
            // SS is assumed to be milliseconds, so a trailing 0 is necessary
            dateString = dateString.replace("Z", "0");
            break;
        case 22: // "yyyy-MM-dd'T'HH:mm:ss.S'Z'"-> [2012-01-04T23:21:59.1Z] length = 22
            // S is assumed to be milliseconds, so trailing 0's are necessary
            dateString = dateString.replace("Z", "00");
            break;
        case 20: // "yyyy-MM-dd'T'HH:mm:ss'Z'"-> [2012-01-04T23:21:59Z] length = 20
            pattern = Utility.ISO8601_PATTERN;
            break;
        case 17: // "yyyy-MM-dd'T'HH:mm'Z'"-> [2012-01-04T23:21Z] length = 17
            pattern = Utility.ISO8601_PATTERN_NO_SECONDS;
            break;
        default:
            throw new IllegalArgumentException(String.format(SR.INVALID_DATE_STRING, dateString));
        }

        final DateFormat format = new SimpleDateFormat(pattern, Utility.LOCALE_US);
        format.setTimeZone(UTC_ZONE);
        try {
            return format.parse(dateString);
        } catch (final ParseException e) {
            throw new IllegalArgumentException(String.format(SR.INVALID_DATE_STRING, dateString), e);
        }
    }

    /**
     * Given a String representing a date in a form of the ISO8601 pattern, generates a Date representing it
     * with up to millisecond precision. Use {@link #parseDate(String)} instead unless
     * <code>dateBackwardCompatibility</code> is needed.
     * <p>
     * See <a href="http://go.microsoft.com/fwlink/?LinkId=523753">here</a> for more details.
     * 
     * @param dateString
     *              the <code>String</code> to be interpreted as a <code>Date</code>
     * @param dateBackwardCompatibility
     *              <code>true</code> to correct Date values that may have been written
     *              using versions of this library prior to 2.0.0; otherwise, <code>false</code>
     *              
     * @return the corresponding <code>Date</code> object
     */
    public static Date parseDate(String dateString, boolean dateBackwardCompatibility) {
        if (!dateBackwardCompatibility) {
            return parseDate(dateString);
        }

        final int beginMilliIndex = 20; // Length of "yyyy-MM-ddTHH:mm:ss."
        final int endTenthMilliIndex = 24; // Length of "yyyy-MM-ddTHH:mm:ss.SSSS"

        // Check whether the millisecond and tenth of a millisecond digits are all 0.
        if (dateString.length() > endTenthMilliIndex
                && "0000".equals(dateString.substring(beginMilliIndex, endTenthMilliIndex))) {
            // Remove the millisecond and tenth of a millisecond digits.
            // Treat the final three digits (ticks) as milliseconds.
            dateString = dateString.substring(0, beginMilliIndex) + dateString.substring(endTenthMilliIndex);
        }

        return parseDate(dateString);
    }

    /**
     * Determines which location can the listing command target by looking at the
     * continuation token.
     * 
     * @param token
     *            Continuation token
     * @return
     *         Location mode
     */
    public static RequestLocationMode getListingLocationMode(ResultContinuation token) {
        if ((token != null) && token.getTargetLocation() != null) {
            switch (token.getTargetLocation()) {
            case PRIMARY:
                return RequestLocationMode.PRIMARY_ONLY;

            case SECONDARY:
                return RequestLocationMode.SECONDARY_ONLY;

            default:
                throw new IllegalArgumentException(
                        String.format(SR.ARGUMENT_OUT_OF_RANGE_ERROR, "token", token.getTargetLocation()));
            }
        }

        return RequestLocationMode.PRIMARY_OR_SECONDARY;
    }
}