com.microsoft.azure.storage.core.Utility Maven / Gradle / Ivy
Show all versions of azure-storage Show documentation
/**
* 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 {
/**
* Thread local for storing GMT date format.
*/
private static ThreadLocal
RFC1123_GMT_DATE_TIME_FORMATTER = new ThreadLocal() {
@Override
protected DateFormat initialValue() {
final DateFormat formatter = new SimpleDateFormat(RFC1123_PATTERN, LOCALE_US);
formatter.setTimeZone(GMT_ZONE);
return formatter;
}
};
/**
* 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 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();
/**
* Thread local for SAXParser.
*/
private static final ThreadLocal saxParserThreadLocal = new ThreadLocal() {
SAXParserFactory factory;
@Override public SAXParser initialValue() {
factory = SAXParserFactory.newInstance();
factory.setNamespaceAware(true);
try {
return factory.newSAXParser();
} catch (SAXException e) {
throw new RuntimeException("Unable to create SAXParser", e);
} catch (ParserConfigurationException e) {
throw new RuntimeException("Check parser configuration", e);
}
}
};
/**
* 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 InputStream
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 -1
to
* force the entire stream to be read. This parameter is provided to support upload thresholds.
* @param rewindSourceStream
* true
if the stream should be rewound after it is read; otherwise, false
.
* @param calculateMD5
* true
if an MD5 hash will be calculated; otherwise, false
.
*
* @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 InputStream
object that represents the stream to measure.
* @param targetStream
* A ByteArrayOutputStream
object that represents the stream to write the encrypted data.
* @param cipher
* The Cipher
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 -1
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 null
.
*
* @param param
* A String
that represents the name of the parameter, which becomes the exception message
* text if the value
parameter is null
.
* @param value
* An Object
object that represents the value of the specified parameter. This is the value
* being asserted as not null
.
*/
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 null
or empty.
*
* @param param
* A String
that represents the name of the parameter, which becomes the exception message
* text if the value
parameter is null
or an empty string.
* @param value
* A String
that represents the value of the specified parameter. This is the value being
* asserted as not null
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 String
that represents the name of the parameter, which becomes the exception message
* text if the value
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 String
that represents the name of the parameter, which becomes the exception message
* text if the value
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 true
if the maximum execution time would be surpassed; otherwise, false
.
*/
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 true
if the maximum execution time would be surpassed; otherwise, false
.
*/
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 java.net.URI
value that represents the URI being checked.
* @return true
if the specified URI is path-style; otherwise, false
.
*/
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 true
if the host is DNS; otherwise, false
.
*/
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 XMLStreamReader
object that represents the source XML stream reader.
*
* @return A String
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 Exception
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 String
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 Date
object that represents the date to convert to GMT date/time in the RFC1123
* pattern.
*
* @return A String
that represents the GMT date/time for the specified value using the RFC1123
* pattern.
*/
public static String getGMTTime(final Date date) {
return RFC1123_GMT_DATE_TIME_FORMATTER.get().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 Date
object that represents the date to convert to UTC date/time in Java's version
* of the ISO8601 pattern.
*
* @return A String
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 JsonGenerator
with the specified StringWriter
.
*
* @param strWriter
* The StringWriter
to use to create the JsonGenerator
instance.
* @return A JsonGenerator
instance
*
* @throws IOException
*/
public static JsonGenerator getJsonGenerator(StringWriter strWriter) throws IOException {
return jsonFactory.createGenerator(strWriter);
}
/**
* Returns a JsonGenerator
with the specified OutputStream
.
*
* @param outStream
* The OutputStream
to use to create the JsonGenerator
instance.
* @return A JsonGenerator
instance
*
* @throws IOException
*/
public static JsonGenerator getJsonGenerator(OutputStream outStream) throws IOException {
return jsonFactory.createGenerator(outStream);
}
/**
* Returns a JsonParser
with the specified String
. This JsonParser
* will allow non-numeric numbers.
*
* @param jsonString
* The String
to use to create the JsonGenerator
instance.
* @return A JsonGenerator
instance.
*
* @throws IOException
*/
public static JsonParser getJsonParser(final String jsonString) throws JsonParseException, IOException {
JsonParser parser = jsonFactory.createParser(jsonString);
return setupJsonParser(parser);
}
/**
* Returns a JsonParser
with the specified InputStream
. This JsonParser
* will allow non-numeric numbers.
*
* @param inStream
* The InputStream
to use to create the JsonGenerator
instance.
* @return A JsonGenerator
instance.
*
* @throws IOException
*/
public static JsonParser getJsonParser(final InputStream inStream) throws JsonParseException, IOException {
JsonParser parser = jsonFactory.createParser(inStream);
return setupJsonParser(parser);
}
/**
* Returns a JsonParser
This JsonParser will allow non-numeric numbers.
* @param parser
* A JsonParser
to setup.
* @return
* A JsonParser
with settings configured.
*/
private static JsonParser setupJsonParser(final JsonParser parser) {
// IMPORTANT: DO NOT REMOVE!
// don't close the stream and allow it to be drained completely in ExecutionEngine to improve socket reuse
parser.disable(JsonParser.Feature.AUTO_CLOSE_SOURCE);
// allows handling of infinity, -infinity, and NaN for Doubles
return parser.enable(JsonParser.Feature.ALLOW_NON_NUMERIC_NUMBERS);
}
/**
* Returns a namespace aware SAXParser
.
*
* @return A SAXParser
instance which is namespace aware
*
* @throws ParserConfigurationException
* @throws SAXException
*/
public static SAXParser getSAXParser() throws ParserConfigurationException, SAXException {
SAXParser parser = saxParserThreadLocal.get();
parser.reset(); //reset to original config
return parser;
}
/**
* 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 HttpURLConnection
object that represents the request.
* @param headerName
* A String
that represents the name of the header being requested.
*
* @return A String
that represents the header value, or null
if there is no corresponding
* header value for headerName
.
*/
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 Date
object that represents the date to convert to UTC date/time in the ISO8601
* pattern. If this value is null
, this method returns an empty string.
*
* @return A String
that represents the UTC date/time for the specified value using the ISO8601
* pattern, or an empty string if value
is null
.
*/
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 XMLStreamWriter
with the specified StringWriter
.
*
* @param outWriter
* The StringWriter
to use to create the XMLStreamWriter
instance.
* @return A XMLStreamWriter
instance
*
* @throws XMLStreamException
*/
public static XMLStreamWriter createXMLStreamWriter(StringWriter outWriter) throws XMLStreamException {
return xmlOutputFactory.createXMLStreamWriter(outWriter);
}
/**
* Creates an instance of the IOException
class using the specified exception.
*
* @param ex
* An Exception
object that represents the exception used to create the IO exception.
*
* @return A java.io.IOException
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 null
or empty.
*
* @param value
* A String
being examined for null
or empty.
*
* @return true
if the specified value is null
or empty; otherwise, false
*/
public static boolean isNullOrEmpty(final String value) {
return value == null || value.length() == 0;
}
/**
* Returns a value that indicates whether the specified string is null
, empty, or whitespace.
*
* @param value
* A String
being examined for null
, empty, or whitespace.
*
* @return true
if the specified value is null
, empty, or whitespace; otherwise,
* false
*/
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 String
that represents the connection string to parse.
*
* @return A java.util.HashMap
object that represents the hash map of the key / value pairs parsed from
* the connection string.
*/
public static HashMap parseAccountString(final String parseString) {
// 1. split name value pairs by splitting on the ';' character
final String[] valuePairs = parseString.split(";");
final HashMap retVals = new HashMap();
// 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 String
that represents the string to parse.
*
* @return A Date
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 {
return RFC1123_GMT_DATE_TIME_FORMATTER.get().parse(value);
}
/**
* Performs safe decoding of the specified string, taking care to preserve each +
character, rather
* than replacing it with a space character.
*
* @param stringToDecode
* A String
that represents the string to decode.
*
* @return A String
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 %20
for each space character,
* instead of inserting the +
character.
*
* @param stringToEncode
* A String
that represents the string to encode.
*
* @return A String
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 java.net.URI
object that represents the base URI for which toUri
will be
* made relative.
* @param toUri
* A java.net.URI
object that represents the URI to make relative to baseURI
.
*
* @return A String
that either represents the relative URI of toUri
to
* baseURI
, or the URI of toUri
itself, depending on whether the hostname and
* scheme are identical for toUri
and baseURI
. If the hostname and scheme of
* baseURI
and toUri
are identical, this method returns an unencoded relative URI
* such that if appended to baseURI
, it will yield toUri
. If the hostname or
* scheme of baseURI
and toUri
are not identical, this method returns an unencoded
* full URI specified by toUri
.
*
* @throws URISyntaxException
* If baseURI
or toUri
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 details = extendedError.getAdditionalDetails();
if (details != null) {
bld.append(", AdditionalDetails= { ");
for (Entry 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> 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> 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 String
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 String
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 InputStream
object that represents the input stream to use as the source.
* @param outStream
* An OutputStream
object that represents the output stream to use as the destination.
* @param writeLength
* The number of bytes to read from the stream.
* @param rewindSourceStream
* true
if the input stream should be rewound before it is read; otherwise,
* false
* @param calculateMD5
* true
if an MD5 hash will be calculated; otherwise, false
.
* @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 InputStream
object that represents the input stream to use as the source.
* @param outStream
* An OutputStream
object that represents the output stream to use as the destination.
* @param writeLength
* The number of bytes to read from the stream.
* @param rewindSourceStream
* true
if the input stream should be rewound before it is read; otherwise,
* false
* @param calculateMD5
* true
if an MD5 hash will be calculated; otherwise, false
.
* @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*/, null /* descriptor */);
}
/**
* 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 InputStream
object that represents the input stream to use as the source.
* @param outStream
* An OutputStream
object that represents the output stream to use as the destination.
* @param writeLength
* The number of bytes to read from the stream.
* @param rewindSourceStream
* true
if the input stream should be rewound before it is read; otherwise,
* false
* @param calculateMD5
* true
if an MD5 hash will be calculated; otherwise, false
.
* @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.
* @param descriptor
* A {@Link StreamMd5AndLength} object to append to in the case of recovery action or null if this is not called
* from a recovery. This value needs to be passed for recovery in case part of the body has already been read,
* the recovery will attempt to download the remaining bytes but will do MD5 validation on the originally
* requested range size.
* @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 request, StreamMd5AndLength descriptor)
throws IOException, StorageException {
if (rewindSourceStream && sourceStream.markSupported()) {
sourceStream.reset();
sourceStream.mark(Constants.MAX_MARK_LENGTH);
}
if (descriptor == null) {
descriptor = new StreamMd5AndLength();
if (calculateMD5) {
try {
descriptor.setDigest(MessageDigest.getInstance("MD5"));
}
catch (final NoSuchAlgorithmException e) {
// This wont happen, throw fatal.
throw Utility.generateNewUnexpectedStorageException(e);
}
}
}
else {
descriptor.setMd5(null);
}
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) {
descriptor.getDigest().update(retrievedBuff, 0, count);
}
descriptor.setLength(descriptor.getLength() + count);
descriptor.setCurrentOperationByteCount(descriptor.getCurrentOperationByteCount() + count);
if (request != null) {
request.setCurrentRequestByteCount(request.getCurrentRequestByteCount() + count);
request.setCurrentDescriptor(descriptor);
}
nextCopy = (int) Math.min(retrievedBuff.length, writeLength - descriptor.getLength());
count = sourceStream.read(retrievedBuff, 0, nextCopy);
}
if (outStream != null && shouldFlush) {
outStream.flush();
}
return descriptor;
}
/**
* 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 String
to be interpreted as a Date
*
* @return the corresponding Date
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
* dateBackwardCompatibility
is needed.
*
* See here for more details.
*
* @param dateString
* the String
to be interpreted as a Date
* @param dateBackwardCompatibility
* true
to correct Date values that may have been written
* using versions of this library prior to 2.0.0; otherwise, false
*
* @return the corresponding Date
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;
}
}