/** * 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; } }