diff --git a/pom.client.xml b/pom.client.xml index ed51d0abaa3db..6abb523a61e16 100644 --- a/pom.client.xml +++ b/pom.client.xml @@ -744,6 +744,7 @@ ./sdk/identity/azure-identity + ./storage/client/common ./storage/client/blob ./storage/client/file ./storage/client/queue diff --git a/storage/client/blob/pom.xml b/storage/client/blob/pom.xml index b661ff0ad7e2d..05594745609a7 100644 --- a/storage/client/blob/pom.xml +++ b/storage/client/blob/pom.xml @@ -52,8 +52,9 @@ 1.0.0-preview.2 - org.slf4j - slf4j-api + com.azure + azure-storage-common + 12.0.0-preview.2 + + com.google.code.findbugs + jsr305 + 3.0.2 + provided + + + + com.azure + azure-core-test + 1.0.0-preview.2 + test + + + com.azure + azure-identity + 1.0.0-preview.1 + test + + + junit + junit + test + + + io.projectreactor + reactor-test + test + + + + diff --git a/storage/client/common/src/main/java/com/azure/storage/common/Constants.java b/storage/client/common/src/main/java/com/azure/storage/common/Constants.java new file mode 100644 index 0000000000000..f793eaa107b1c --- /dev/null +++ b/storage/client/common/src/main/java/com/azure/storage/common/Constants.java @@ -0,0 +1,216 @@ +package com.azure.storage.common; + +public final class Constants { + private static final int KB = 1024; + + /** + * Constant representing a megabyte (Non-SI version). + */ + public static final int MB = 1024 * KB; + + /** + * An empty {@code String} to use for comparison. + */ + public static final String EMPTY_STRING = ""; + + /** + * The default type for content-type and accept. + */ + static final String UTF8_CHARSET = "UTF-8"; + + /** + * The query parameter for snapshots. + */ + public static final String SNAPSHOT_QUERY_PARAMETER = "snapshot"; + + static final String HTTPS = "https"; + static final String HTTPS_HTTP = "https,http"; + + private Constants() { + } + + /** + * Defines constants for use with connection strings. + */ + public static final class ConnectionStringConstants { + /** + * The AccountName key. + */ + public static final String ACCOUNT_NAME = "accountname"; + + /** + * The AccountKey key. + */ + public static final String ACCOUNT_KEY = "accountkey"; + + /** + * The DefaultEndpointProtocol key. + */ + public static final String ENDPOINT_PROTOCOL = "defaultendpointprotocol"; + + /** + * The EndpointSuffix key. + */ + public static final String ENDPOINT_SUFFIX = "endpointsuffix"; + + private ConnectionStringConstants() { + } + } + + /** + * Defines constants for use with HTTP headers. + */ + public static final class HeaderConstants { + + /** + * The current storage version header value. + */ + public static final String TARGET_STORAGE_VERSION = "2018-11-09"; + + private HeaderConstants() { + // Private to prevent construction. + } + } + + public static final class UrlConstants { + + /** + * The SAS service version parameter. + */ + public static final String SAS_SERVICE_VERSION = "sv"; + + /** + * The SAS services parameter. + */ + public static final String SAS_SERVICES = "ss"; + + /** + * The SAS resource types parameter. + */ + public static final String SAS_RESOURCES_TYPES = "srt"; + + /** + * The SAS protocol parameter. + */ + public static final String SAS_PROTOCOL = "spr"; + + /** + * The SAS start time parameter. + */ + public static final String SAS_START_TIME = "st"; + + /** + * The SAS expiration time parameter. + */ + public static final String SAS_EXPIRY_TIME = "se"; + + /** + * The SAS IP range parameter. + */ + public static final String SAS_IP_RANGE = "sip"; + + /** + * The SAS signed identifier parameter. + */ + public static final String SAS_SIGNED_IDENTIFIER = "si"; + + /** + * The SAS signed resource parameter. + */ + public static final String SAS_SIGNED_RESOURCE = "sr"; + + /** + * The SAS signed permissions parameter. + */ + public static final String SAS_SIGNED_PERMISSIONS = "sp"; + + /** + * The SAS signature parameter. + */ + public static final String SAS_SIGNATURE = "sig"; + + /** + * The SAS cache control parameter. + */ + public static final String SAS_CACHE_CONTROL = "rscc"; + + /** + * The SAS content disposition parameter. + */ + public static final String SAS_CONTENT_DISPOSITION = "rscd"; + + /** + * The SAS content encoding parameter. + */ + public static final String SAS_CONTENT_ENCODING = "rsce"; + + /** + * The SAS content language parameter. + */ + public static final String SAS_CONTENT_LANGUAGE = "rscl"; + + /** + * The SAS content type parameter. + */ + public static final String SAS_CONTENT_TYPE = "rsct"; + + /** + * The SAS signed object id parameter for user delegation SAS. + */ + public static final String SAS_SIGNED_OBJECT_ID = "skoid"; + + /** + * The SAS signed tenant id parameter for user delegation SAS. + */ + public static final String SAS_SIGNED_TENANT_ID = "sktid"; + + /** + * The SAS signed key-start parameter for user delegation SAS. + */ + public static final String SAS_SIGNED_KEY_START = "skt"; + + /** + * The SAS signed key-expiry parameter for user delegation SAS. + */ + public static final String SAS_SIGNED_KEY_EXPIRY = "ske"; + + /** + * The SAS signed service parameter for user delegation SAS. + */ + public static final String SAS_SIGNED_KEY_SERVICE = "sks"; + + /** + * The SAS signed version parameter for user delegation SAS. + */ + public static final String SAS_SIGNED_KEY_VERSION = "skv"; + + /** + * The SAS blob constant. + */ + public static final String SAS_BLOB_CONSTANT = "b"; + + /** + * The SAS blob snapshot constant. + */ + public static final String SAS_BLOB_SNAPSHOT_CONSTANT = "bs"; + + /** + * The SAS blob snapshot constant. + */ + public static final String SAS_CONTAINER_CONSTANT = "c"; + + private UrlConstants() { + // Private to prevent construction. + } + } + + static final class MessageConstants { + static final String ARGUMENT_NULL_OR_EMPTY = "The argument must not be null or an empty string. Argument name: %s."; + static final String PARAMETER_NOT_IN_RANGE = "The value of the parameter '%s' should be between %s and %s."; + static final String INVALID_DATE_STRING = "Invalid Date String: %s."; + static final String NO_PATH_SEGMENTS = "URL %s does not contain path segments."; + + private MessageConstants() { + } + } +} diff --git a/storage/client/blob/src/main/java/com/azure/storage/blob/IPRange.java b/storage/client/common/src/main/java/com/azure/storage/common/IPRange.java similarity index 61% rename from storage/client/blob/src/main/java/com/azure/storage/blob/IPRange.java rename to storage/client/common/src/main/java/com/azure/storage/common/IPRange.java index 24665c7aaedda..ca92f4ef83a54 100644 --- a/storage/client/blob/src/main/java/com/azure/storage/blob/IPRange.java +++ b/storage/client/common/src/main/java/com/azure/storage/common/IPRange.java @@ -1,49 +1,51 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.storage.blob; +package com.azure.storage.common; /** * This type specifies a continuous range of IP addresses. It is used to limit permissions on SAS tokens. Null may be - * set if it is not desired to confine the sas permissions to an IP range. Please refer to - * {@link AccountSASSignatureValues} or {@link ServiceSASSignatureValues} for more information. + * set if it is not desired to confine the sas permissions to an IP range. */ -final class IPRange { - +public final class IPRange { private String ipMin; - private String ipMax; - IPRange() { + /** + * Constructs an IPRange object. + */ + public IPRange() { } /** * Creates a {@code IPRange} from the specified string. * - * @param rangeStr - * The {@code String} representation of the {@code IPRange}. - * + * @param rangeStr The {@code String} representation of the {@code IPRange}. * @return The {@code IPRange} generated from the {@code String}. */ public static IPRange parse(String rangeStr) { String[] addrs = rangeStr.split("-"); - IPRange range = new IPRange(); - range.ipMin = addrs[0]; + + IPRange range = new IPRange().ipMin(addrs[0]); if (addrs.length > 1) { - range.ipMax = addrs[1]; + range.ipMax(addrs[1]); } + return range; } /** - * The minimum IP address of the range. + * @return the minimum IP address of the range */ public String ipMin() { return ipMin; } /** - * The minimum IP address of the range. + * Sets the minimum IP address of the range. + * + * @param ipMin IP address to set as the minimum + * @return the updated IPRange object */ public IPRange ipMin(String ipMin) { this.ipMin = ipMin; @@ -51,14 +53,17 @@ public IPRange ipMin(String ipMin) { } /** - * The maximum IP address of the range. + * @return the maximum IP address of the range */ public String ipMax() { return ipMax; } /** - * The maximum IP address of the range. + * Sets the maximum IP address of the range. + * + * @param ipMax IP address to set as the maximum + * @return the updated IPRange object */ public IPRange ipMax(String ipMax) { this.ipMax = ipMax; @@ -74,14 +79,10 @@ public IPRange ipMax(String ipMax) { public String toString() { if (this.ipMin == null) { return ""; + } else if (this.ipMax == null) { + return this.ipMin; + } else { + return this.ipMin + "-" + this.ipMax; } - this.ipMax = this.ipMax == null ? this.ipMin : this.ipMax; - StringBuilder str = new StringBuilder(this.ipMin); - if (!this.ipMin.equals(this.ipMax)) { - str.append('-'); - str.append(this.ipMax); - } - - return str.toString(); } } diff --git a/storage/client/blob/src/main/java/com/azure/storage/blob/SASProtocol.java b/storage/client/common/src/main/java/com/azure/storage/common/SASProtocol.java similarity index 81% rename from storage/client/blob/src/main/java/com/azure/storage/blob/SASProtocol.java rename to storage/client/common/src/main/java/com/azure/storage/common/SASProtocol.java index 64df03714ecb5..b7012d3d16eb4 100644 --- a/storage/client/blob/src/main/java/com/azure/storage/blob/SASProtocol.java +++ b/storage/client/common/src/main/java/com/azure/storage/common/SASProtocol.java @@ -1,15 +1,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.storage.blob; +package com.azure.storage.common; import java.util.Locale; /** - * Specifies the set of possible permissions for a shared access signature protocol. Values of this type can be used - * to set the fields on the {@link AccountSASSignatureValues} and {@link ServiceSASSignatureValues} types. + * Specifies the set of possible permissions for a shared access signature protocol. */ -enum SASProtocol { +public enum SASProtocol { /** * Permission to use SAS only through https granted. */ @@ -42,7 +41,7 @@ public static SASProtocol parse(String str) { return SASProtocol.HTTPS_HTTP; } throw new IllegalArgumentException(String.format(Locale.ROOT, - "%s could not be parsed into a SASProtocl value.", str)); + "%s could not be parsed into a SASProtocol value.", str)); } @Override diff --git a/storage/client/common/src/main/java/com/azure/storage/common/Utility.java b/storage/client/common/src/main/java/com/azure/storage/common/Utility.java new file mode 100644 index 0000000000000..ecbbe58ad8e4e --- /dev/null +++ b/storage/client/common/src/main/java/com/azure/storage/common/Utility.java @@ -0,0 +1,437 @@ +package com.azure.storage.common; + +import com.azure.core.http.HttpHeader; +import com.azure.core.http.HttpHeaders; +import com.azure.core.http.HttpPipeline; +import com.azure.core.http.policy.HttpPipelinePolicy; +import com.azure.core.implementation.http.UrlBuilder; +import com.azure.core.implementation.util.ImplUtils; +import com.azure.storage.common.credentials.SharedKeyCredential; +import com.azure.storage.common.policy.SharedKeyCredentialPolicy; +import reactor.core.publisher.Mono; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.io.UnsupportedEncodingException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.Base64; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.TreeMap; +import java.util.function.Function; + +public final class Utility { + private static final String DESERIALIZED_HEADERS = "deserializedHeaders"; + private static final String ETAG = "eTag"; + + public static final DateTimeFormatter ISO_8601_UTC_DATE_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT).withZone(ZoneId.of("UTC")); + /** + * 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"; + /** + * 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 ISO8601 date/time pattern. + */ + private static final String ISO8601_PATTERN_NO_SECONDS = "yyyy-MM-dd'T'HH:mm'Z'"; + /** + * The length of a datestring that matches the MAX_PRECISION_PATTERN. + */ + private static final int MAX_PRECISION_DATESTRING_LENGTH = MAX_PRECISION_PATTERN.replaceAll("'", "").length(); + + /** + *Parses the query string into a key-value pair map that maintains key, query parameter key, order. + * + * @param queryString Query string to parse + * @return a mapping of query string pieces as key-value pairs. + */ + public static TreeMap parseQueryString(final String queryString) { + TreeMap pieces = new TreeMap<>(String::compareTo); + + if (ImplUtils.isNullOrEmpty(queryString)) { + return pieces; + } + + for (String kvp : queryString.split("&")) { + int equalIndex = kvp.indexOf("="); + String key = URLDecode(kvp.substring(0, equalIndex)).toLowerCase(Locale.ROOT); + String value = URLDecode(kvp.substring(equalIndex + 1)); + + pieces.putIfAbsent(key, value); + } + + return pieces; + } + + /** + * Performs a safe decoding of the passed string, taking care to preserve each {@code +} character rather than + * replacing it with a space character. + * + * @param stringToDecode String value to decode + * @return the decoded string value + * @throws RuntimeException If the UTF-8 charset isn't supported + */ + public static String URLDecode(final String stringToDecode) { + if (ImplUtils.isNullOrEmpty(stringToDecode)) { + return ""; + } + + if (stringToDecode.contains("+")) { + StringBuilder outBuilder = new StringBuilder(); + + int startDex = 0; + for (int m = 0; m < stringToDecode.length(); m++) { + if (stringToDecode.charAt(m) == '+') { + if (m > startDex) { + outBuilder.append(decode(stringToDecode.substring(startDex, m))); + } + + outBuilder.append("+"); + startDex = m + 1; + } + } + + if (startDex != stringToDecode.length()) { + outBuilder.append(decode(stringToDecode.substring(startDex))); + } + + return outBuilder.toString(); + } else { + return decode(stringToDecode); + } + } + + /* + * Helper method to reduce duplicate calls of URLDecoder.decode + */ + private static String decode(final String stringToDecode) { + try { + return URLDecoder.decode(stringToDecode, Constants.UTF8_CHARSET); + } catch (UnsupportedEncodingException ex) { + throw new RuntimeException(ex); + } + } + + /** + * Performs a safe encoding of the specified string, taking care to insert %20 for each space character instead of + * inserting the {@code +} character. + * + * @param stringToEncode String value to encode + * @return the encoded string value + * @throws RuntimeException If the UTF-8 charset ins't supported + */ + public static String URLEncode(final String stringToEncode) { + if (stringToEncode == null) { + return null; + } + + if (stringToEncode.length() == 0) { + return Constants.EMPTY_STRING; + } + + if (stringToEncode.contains(" ")) { + StringBuilder outBuilder = new StringBuilder(); + + int startDex = 0; + for (int m = 0; m < stringToEncode.length(); m++) { + if (stringToEncode.charAt(m) == ' ') { + if (m > startDex) { + outBuilder.append(encode(stringToEncode.substring(startDex, m))); + } + + outBuilder.append("%20"); + startDex = m + 1; + } + } + + if (startDex != stringToEncode.length()) { + outBuilder.append(encode(stringToEncode.substring(startDex))); + } + + return outBuilder.toString(); + } else { + return encode(stringToEncode); + } + } + + /* + * Helper method to reduce duplicate calls of URLEncoder.encode + */ + private static String encode(final String stringToEncode) { + try { + return URLEncoder.encode(stringToEncode, Constants.UTF8_CHARSET); + } catch (UnsupportedEncodingException ex) { + throw new RuntimeException(ex); + } + } + + /** + * Parses the connection string into key-value pair map. + * + * @param connectionString Connection string to parse + * @return a mapping of connection string pieces as key-value pairs. + */ + public static Map parseConnectionString(final String connectionString) { + Map parts = new HashMap<>(); + + for (String part : connectionString.split(";")) { + String[] kvp = part.split("=", 2); + parts.put(kvp[0].toLowerCase(Locale.ROOT), kvp[1]); + } + + return parts; + } + + /** + * Blocks an asynchronous response with an optional timeout. + * + * @param response Asynchronous response to block + * @param timeout Optional timeout + * @param Return type of the asynchronous response + * @return the value of the asynchronous response + * @throws RuntimeException If the asynchronous response doesn't complete before the timeout expires. + */ + public static T blockWithOptionalTimeout(Mono response, Duration timeout) { + if (timeout == null) { + return response.block(); + } else { + return response.block(timeout); + } + } + + /** + * Asserts that a value is not {@code null}. + * + * @param param Name of the parameter + * @param value Value of the parameter + * @throws IllegalArgumentException If {@code value} is {@code null} + */ + public static void assertNotNull(final String param, final Object value) { + if (value == null) { + throw new IllegalArgumentException(String.format(Locale.ROOT, Constants.MessageConstants.ARGUMENT_NULL_OR_EMPTY, param)); + } + } + + /** + * Asserts that the specified number is in the valid range. The range is inclusive. + * + * @param param Name of the parameter + * @param value Value of the parameter + * @param min The minimum allowed value + * @param max The maximum allowed value + * @throws IllegalArgumentException If {@code value} is less than {@code min} or {@code value} is greater than + * {@code max}. + */ + 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(Locale.ROOT, Constants.MessageConstants.PARAMETER_NOT_IN_RANGE, param, min, max)); + } + } + + /** + * 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} to be interpreted as a Date + * @return the corresponding Date object + * @throws IllegalArgumentException If {@code dateString} doesn't match an ISO8601 pattern + */ + public static OffsetDateTime 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(Locale.ROOT, Constants.MessageConstants.INVALID_DATE_STRING, dateString)); + } + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern, Locale.ROOT); + return LocalDateTime.parse(dateString, formatter).atZone(ZoneOffset.UTC).toOffsetDateTime(); + } + + /** + * Wraps any potential error responses from the service and applies post processing of the response's eTag header + * to standardize the value. + * + * @param response Response from a service call + * @param errorWrapper Error wrapping function that is applied to the response + * @param Value type of the response + * @return an updated response with post processing steps applied. + */ + public static Mono postProcessResponse(Mono response, Function, Mono> errorWrapper) { + return scrubETagHeader(errorWrapper.apply(response)); + } + + /* + The service is inconsistent in whether or not the etag header value has quotes. This method will check if the + response returns an etag value, and if it does, remove any quotes that may be present to give the user a more + predictable format to work with. + */ + private static Mono scrubETagHeader(Mono unprocessedResponse) { + return unprocessedResponse.map(response -> { + String eTag = null; + + try { + Object headers = response.getClass().getMethod(DESERIALIZED_HEADERS).invoke(response); + Method eTagGetterMethod = headers.getClass().getMethod(ETAG); + eTag = (String) eTagGetterMethod.invoke(headers); + + if (eTag == null) { + return response; + } + + eTag = eTag.replace("\"", ""); + headers.getClass().getMethod(ETAG, String.class).invoke(headers, eTag); + } catch (NoSuchMethodException ex) { + // Response did not return an eTag value. + } catch (IllegalAccessException | InvocationTargetException ex) { + // Unable to access the method or the invoked method threw an exception. + } + + try { + HttpHeaders rawHeaders = (HttpHeaders) response.getClass().getMethod("headers").invoke(response); + // + if (eTag != null) { + rawHeaders.put(ETAG, eTag); + } else { + HttpHeader eTagHeader = rawHeaders.get(ETAG); + if (eTagHeader != null && eTagHeader.value() != null) { + eTag = eTagHeader.value().replace("\"", ""); + rawHeaders.put(ETAG, eTag); + } + } + } catch (NoSuchMethodException e) { + // Response did not return an eTag value. No change necessary. + } catch (IllegalAccessException | InvocationTargetException e) { + // Unable to access the method or the invoked method threw an exception. + } + + return response; + }); + } + + /** + * Computes a signature for the specified string using the HMAC-SHA256 algorithm. + * + * @param base64Key Base64 encoded key used to sign the string + * @param stringToSign UTF-8 encoded string to sign + * @return the HMAC-SHA256 encoded signature + * @throws RuntimeException If the HMAC-SHA256 algorithm isn't support, if the key isn't a valid Base64 encoded + * string, or the UTF-8 charset isn't supported. + */ + public static String computeHMac256(final String base64Key, final String stringToSign) { + try { + byte[] key = Base64.getDecoder().decode(base64Key); + Mac hmacSHA256 = Mac.getInstance("HmacSHA256"); + hmacSHA256.init(new SecretKeySpec(key, "HmacSHA256")); + byte[] utf8Bytes = stringToSign.getBytes(StandardCharsets.UTF_8); + return Base64.getEncoder().encodeToString(hmacSHA256.doFinal(utf8Bytes)); + } catch (NoSuchAlgorithmException | InvalidKeyException ex) { + throw new RuntimeException(ex); + } + } + + /** + * Appends a string to the end of the passed URL's path. + * + * @param baseURL URL having a path appended + * @param name Name of the path + * @return a URL with the path appended. + * @throws IllegalArgumentException If {@code name} causes the URL to become malformed. + */ + public static URL appendToURLPath(URL baseURL, String name) { + UrlBuilder builder = UrlBuilder.parse(baseURL); + + if (builder.path() == null) { + builder.path("/"); + } else if (!builder.path().endsWith("/")) { + builder.path(builder.path() + "/"); + } + + builder.path(builder.path() + name); + + try { + return builder.toURL(); + } catch (MalformedURLException ex) { + throw new IllegalArgumentException(ex); + } + } + + /** + * Strips the last path segment from the passed URL. + * + * @param baseURL URL having its last path segment stripped + * @return a URL with the path segment stripped. + * @throws IllegalArgumentException If stripping the last path segment causes the URL to become malformed or it + * doesn't contain any path segments. + */ + public static URL stripLastPathSegment(URL baseURL) { + UrlBuilder builder = UrlBuilder.parse(baseURL); + + if (builder.path() == null || !builder.path().contains("/")) { + throw new IllegalArgumentException(String.format(Locale.ROOT, Constants.MessageConstants.NO_PATH_SEGMENTS, baseURL)); + } + + builder.path(builder.path().substring(0, builder.path().lastIndexOf("/"))); + try { + return builder.toURL(); + } catch (MalformedURLException ex) { + throw new IllegalArgumentException(ex); + } + } + + /** + * Searches for a {@link SharedKeyCredential} in the passed {@link HttpPipeline}. + * + * @param httpPipeline Pipeline being searched + * @return a SharedKeyCredential if the pipeline contains one, otherwise null. + */ + public static SharedKeyCredential getSharedKeyCredential(HttpPipeline httpPipeline) { + for (int i = 0; i < httpPipeline.getPolicyCount(); i++) { + HttpPipelinePolicy httpPipelinePolicy = httpPipeline.getPolicy(i); + if (httpPipelinePolicy instanceof SharedKeyCredentialPolicy) { + SharedKeyCredentialPolicy sharedKeyCredentialPolicy = (SharedKeyCredentialPolicy) httpPipelinePolicy; + return sharedKeyCredentialPolicy.sharedKeyCredential(); + } + } + return null; + } +} diff --git a/storage/client/blob/src/main/java/com/azure/storage/common/credentials/SASTokenCredential.java b/storage/client/common/src/main/java/com/azure/storage/common/credentials/SASTokenCredential.java similarity index 61% rename from storage/client/blob/src/main/java/com/azure/storage/common/credentials/SASTokenCredential.java rename to storage/client/common/src/main/java/com/azure/storage/common/credentials/SASTokenCredential.java index 8a18f449c8c9f..1018fedf94e6d 100644 --- a/storage/client/blob/src/main/java/com/azure/storage/common/credentials/SASTokenCredential.java +++ b/storage/client/common/src/main/java/com/azure/storage/common/credentials/SASTokenCredential.java @@ -4,12 +4,15 @@ package com.azure.storage.common.credentials; import com.azure.core.implementation.util.ImplUtils; -import com.azure.storage.blob.SASQueryParameters; + +import java.util.Map; /** * Holds a SAS token used for authenticating requests. */ public final class SASTokenCredential { + private static final String SIGNATURE = "sig"; + private final String sasToken; /** @@ -43,17 +46,27 @@ public static SASTokenCredential fromSASTokenString(String sasToken) { } /** - * Creates a SAS token credential from the passed {@link SASQueryParameters}. + * Creates a SAS token credential from the passed query string parameters. * - * @param queryParameters SAS token query parameters object + * @param queryParameters URL query parameters * @return a SAS token credential if {@code queryParameters} is not {@code null} and has - * {@link SASQueryParameters#signature() signature} set, otherwise returns {@code null}. + * the signature ("sig") query parameter, otherwise returns {@code null}. */ - public static SASTokenCredential fromQueryParameters(SASQueryParameters queryParameters) { - if (queryParameters == null || ImplUtils.isNullOrEmpty(queryParameters.signature())) { + public static SASTokenCredential fromQueryParameters(Map queryParameters) { + if (ImplUtils.isNullOrEmpty(queryParameters) || !queryParameters.containsKey(SIGNATURE)) { return null; } - return new SASTokenCredential(queryParameters.encode()); + StringBuilder sb = new StringBuilder(); + for (Map.Entry kvp : queryParameters.entrySet()) { + if (sb.length() != 0) { + sb.append("&"); + + } + + sb.append(kvp.getKey()).append("=").append(kvp.getValue()); + } + + return new SASTokenCredential(sb.toString()); } } diff --git a/storage/client/blob/src/main/java/com/azure/storage/common/credentials/SharedKeyCredential.java b/storage/client/common/src/main/java/com/azure/storage/common/credentials/SharedKeyCredential.java similarity index 79% rename from storage/client/blob/src/main/java/com/azure/storage/common/credentials/SharedKeyCredential.java rename to storage/client/common/src/main/java/com/azure/storage/common/credentials/SharedKeyCredential.java index 2f2b0111a91e6..c71a220640163 100644 --- a/storage/client/blob/src/main/java/com/azure/storage/common/credentials/SharedKeyCredential.java +++ b/storage/client/common/src/main/java/com/azure/storage/common/credentials/SharedKeyCredential.java @@ -4,16 +4,11 @@ package com.azure.storage.common.credentials; import com.azure.core.implementation.util.ImplUtils; +import com.azure.storage.common.Utility; import io.netty.handler.codec.http.QueryStringDecoder; -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; import java.util.ArrayList; -import java.util.Base64; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -33,7 +28,7 @@ public final class SharedKeyCredential { private static final String ACCOUNT_KEY = "accountkey"; private final String accountName; - private final byte[] accountKey; + private final String accountKey; /** * Initializes a new instance of SharedKeyCredential contains an account's name and its primary or secondary @@ -46,7 +41,7 @@ public SharedKeyCredential(String accountName, String accountKey) { Objects.requireNonNull(accountName); Objects.requireNonNull(accountKey); this.accountName = accountName; - this.accountKey = Base64.getDecoder().decode(accountKey); + this.accountKey = accountKey; } /** @@ -89,7 +84,8 @@ public String accountName() { * @return the SharedKey authorization value */ public String generateAuthorizationHeader(URL requestURL, String httpMethod, Map headers) { - return computeHMACSHA256(buildStringToSign(requestURL, httpMethod, headers)); + String signature = Utility.computeHMac256(accountKey, buildStringToSign(requestURL, httpMethod, headers)); + return String.format(AUTHORIZATION_HEADER_FORMAT, accountName, signature); } /** @@ -98,23 +94,11 @@ public String generateAuthorizationHeader(URL requestURL, String httpMethod, Map * * @param stringToSign The UTF-8-encoded string to sign. * @return A {@code String} that contains the HMAC-SHA256-encoded signature. - * @throws InvalidKeyException If the accountKey is not a valid Base64-encoded string. - * @throws RuntimeException If the {@code HmacSHA256} algorithm isn't supported. + * @throws RuntimeException If the HMAC-SHA256 algorithm isn't support, if the key isn't a valid Base64 encoded + * string, or the UTF-8 charset isn't supported. */ - public String computeHmac256(final String stringToSign) throws InvalidKeyException { - try { - /* - We must get a new instance of the Mac calculator for each signature calculated because the instances are - not threadsafe and there is some suggestion online that they may not even be safe for reuse, so we use a - new one each time to be sure. - */ - Mac hmacSha256 = Mac.getInstance("HmacSHA256"); - hmacSha256.init(new SecretKeySpec(this.accountKey, "HmacSHA256")); - byte[] utf8Bytes = stringToSign.getBytes(StandardCharsets.UTF_8); - return Base64.getEncoder().encodeToString(hmacSha256.doFinal(utf8Bytes)); - } catch (final NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } + public String computeHmac256(final String stringToSign) { + return Utility.computeHMac256(accountKey, stringToSign); } private String buildStringToSign(URL requestURL, String httpMethod, Map headers) { @@ -216,16 +200,4 @@ private String getCanonicalizedResource(URL requestURL) { // append to main string builder the join of completed params with new line return canonicalizedResource.toString(); } - - private String computeHMACSHA256(String stringToSign) { - try { - Mac hmacSha256 = Mac.getInstance("HmacSHA256"); - hmacSha256.init(new SecretKeySpec(accountKey, "HmacSHA256")); - byte[] utf8Bytes = stringToSign.getBytes(StandardCharsets.UTF_8); - String signature = Base64.getEncoder().encodeToString(hmacSha256.doFinal(utf8Bytes)); - return String.format(AUTHORIZATION_HEADER_FORMAT, accountName, signature); - } catch (NoSuchAlgorithmException | InvalidKeyException ex) { - throw new Error(ex); - } - } } diff --git a/storage/client/blob/src/main/java/com/azure/storage/common/credentials/package-info.java b/storage/client/common/src/main/java/com/azure/storage/common/credentials/package-info.java similarity index 100% rename from storage/client/blob/src/main/java/com/azure/storage/common/credentials/package-info.java rename to storage/client/common/src/main/java/com/azure/storage/common/credentials/package-info.java diff --git a/storage/client/queue/src/main/java/com/azure/storage/common/policy/RequestRetryOptions.java b/storage/client/common/src/main/java/com/azure/storage/common/policy/RequestRetryOptions.java similarity index 61% rename from storage/client/queue/src/main/java/com/azure/storage/common/policy/RequestRetryOptions.java rename to storage/client/common/src/main/java/com/azure/storage/common/policy/RequestRetryOptions.java index 23bdec3f5d48f..50247964bd7c1 100644 --- a/storage/client/queue/src/main/java/com/azure/storage/common/policy/RequestRetryOptions.java +++ b/storage/client/common/src/main/java/com/azure/storage/common/policy/RequestRetryOptions.java @@ -6,9 +6,9 @@ import java.util.concurrent.TimeUnit; /** - * Options for configuring the {@link RequestRetryPolicy}. Please refer to the Factory for more information. Note - * that there is no option for overall operation timeout. This is because Rx object have a timeout field which provides - * this functionality. + * Options for configuring the {@link RequestRetryPolicy}. Please refer to the Factory for more information. Note that + * there is no option for overall operation timeout. This is because Rx object have a timeout field which provides this + * functionality. */ public final class RequestRetryOptions { @@ -28,50 +28,52 @@ public final class RequestRetryOptions { */ public RequestRetryOptions() { this(RetryPolicyType.EXPONENTIAL, null, - null, null, null, null); + null, null, null, null); } /** * Configures how the {@link com.azure.core.http.HttpPipeline} should retry requests. * - * @param retryPolicyType - * A {@link RetryPolicyType} specifying the type of retry pattern to use. A value of {@code null} accepts - * the default. - * @param maxTries - * Specifies the maximum number of attempts an operation will be tried before producing an error. A value of - * {@code null} means that you accept our default policy. A value of 1 means 1 try and no retries. - * @param tryTimeout - * Indicates the maximum time allowed for any single try of an HTTP request. A value of {@code null} means - * that you accept our default. NOTE: When transferring large amounts of data, the default TryTimeout will - * probably not be sufficient. You should override this value based on the bandwidth available to the host - * machine and proximity to the Storage service. A good starting point may be something like (60 seconds per - * MB of anticipated-payload-size). - * @param retryDelayInMs - * Specifies the amount of delay to use before retrying an operation. A value of {@code null} means you - * accept the default value. The delay increases (exponentially or linearly) with each retry up to a maximum - * specified by MaxRetryDelay. If you specify {@code null}, then you must also specify {@code null} for - * MaxRetryDelay. - * @param maxRetryDelayInMs - * Specifies the maximum delay allowed before retrying an operation. A value of {@code null} means you - * accept the default value. If you specify {@code null}, then you must also specify {@code null} for - * RetryDelay. - * @param secondaryHost - * If a secondaryHost is specified, retries will be tried against this host. If secondaryHost is - * {@code null} (the default) then operations are not retried against another host. NOTE: Before setting - * this field, make sure you understand the issues around reading stale and potentially-inconsistent data at - * this webpage + * @param retryPolicyType A {@link RetryPolicyType} specifying the type of retry pattern to use. A value of {@code + * null} accepts the default. + * @param maxTries Specifies the maximum number of attempts an operation will be tried before producing an error. A + * value of {@code null} means that you accept our default policy. A value of 1 means 1 try and no retries. + * @param tryTimeout Indicates the maximum time allowed for any single try of an HTTP request. A value of {@code + * null} means that you accept our default. NOTE: When transferring large amounts of data, the default TryTimeout + * will probably not be sufficient. You should override this value based on the bandwidth available to the host + * machine and proximity to the Storage service. A good starting point may be something like (60 seconds per MB of + * anticipated-payload-size). + * @param retryDelayInMs Specifies the amount of delay to use before retrying an operation. A value of {@code null} + * means you accept the default value. The delay increases (exponentially or linearly) with each retry up to a + * maximum specified by MaxRetryDelay. If you specify {@code null}, then you must also specify {@code null} for + * MaxRetryDelay. + * @param maxRetryDelayInMs Specifies the maximum delay allowed before retrying an operation. A value of {@code + * null} means you accept the default value. If you specify {@code null}, then you must also specify {@code null} + * for RetryDelay. + * @param secondaryHost If a secondaryHost is specified, retries will be tried against this host. If secondaryHost + * is {@code null} (the default) then operations are not retried against another host. NOTE: Before setting this + * field, make sure you understand the issues around reading stale and potentially-inconsistent data at + * this + * webpage + * @throws IllegalArgumentException If {@code retryDelayInMs} and {@code maxRetryDelayInMs} are not both null or + * non-null or {@code retryPolicyType} isn't {@link RetryPolicyType#EXPONENTIAL} or {@link RetryPolicyType#FIXED}. * *

Sample Code

* +<<<<<<< HEAD:storage/client/common/src/main/java/com/azure/storage/common/policy/RequestRetryOptions.java + *

For more samples, please see the + * samples file

+======= *

For more samples, please see the samples file

* @throws IllegalArgumentException If one of the following case exists: *
    *
  • There is only one null value for retryDelay and maxRetryDelay.
  • *
  • Unrecognized retry policy type.
  • *
+>>>>>>> ea2f43a046f365617389bbd8ccf852fd2688f51c:storage/client/queue/src/main/java/com/azure/storage/common/policy/RequestRetryOptions.java */ public RequestRetryOptions(RetryPolicyType retryPolicyType, Integer maxTries, Integer tryTimeout, - Long retryDelayInMs, Long maxRetryDelayInMs, String secondaryHost) { + Long retryDelayInMs, Long maxRetryDelayInMs, String secondaryHost) { this.retryPolicyType = retryPolicyType == null ? RetryPolicyType.EXPONENTIAL : retryPolicyType; if (maxTries != null) { assertInBounds("maxRetries", maxTries, 1, Integer.MAX_VALUE); @@ -88,7 +90,7 @@ public RequestRetryOptions(RetryPolicyType retryPolicyType, Integer maxTries, In } if ((retryDelayInMs == null && maxRetryDelayInMs != null) - || (retryDelayInMs != null && maxRetryDelayInMs == null)) { + || (retryDelayInMs != null && maxRetryDelayInMs == null)) { throw new IllegalArgumentException("Both retryDelay and maxRetryDelay must be null or neither can be null"); } @@ -136,9 +138,7 @@ long maxRetryDelayInMs() { /** * Calculates how long to delay before sending the next request. * - * @param tryCount - * An {@code int} indicating which try we are on. - * + * @param tryCount An {@code int} indicating which try we are on. * @return A {@code long} value of how many milliseconds to delay. */ long calculateDelayInMs(int tryCount) { diff --git a/storage/client/blob/src/main/java/com/azure/storage/common/policy/RequestRetryPolicy.java b/storage/client/common/src/main/java/com/azure/storage/common/policy/RequestRetryPolicy.java similarity index 56% rename from storage/client/blob/src/main/java/com/azure/storage/common/policy/RequestRetryPolicy.java rename to storage/client/common/src/main/java/com/azure/storage/common/policy/RequestRetryPolicy.java index 774a44999ce76..c57a1d92fd905 100644 --- a/storage/client/blob/src/main/java/com/azure/storage/common/policy/RequestRetryPolicy.java +++ b/storage/client/common/src/main/java/com/azure/storage/common/policy/RequestRetryPolicy.java @@ -22,10 +22,10 @@ import java.util.concurrent.TimeoutException; /** - * This is a request policy in an {@link com.azure.core.http.HttpPipeline} for retrying a given HTTP request. The request - * that is retried will be identical each time it is reissued. Retries will try against a secondary if one is specified - * and the type of operation/error indicates that the secondary can handle the request. Exponential and fixed backoff are - * supported. The policy must only be used directly when creating a custom pipeline. + * This is a request policy in an {@link com.azure.core.http.HttpPipeline} for retrying a given HTTP request. The + * request that is retried will be identical each time it is reissued. Retries will try against a secondary if one is + * specified and the type of operation/error indicates that the secondary can handle the request. Exponential and fixed + * backoff are supported. The policy must only be used directly when creating a custom pipeline. */ public final class RequestRetryPolicy implements HttpPipelinePolicy { private final RequestRetryOptions requestRetryOptions; @@ -43,37 +43,32 @@ public RequestRetryPolicy(RequestRetryOptions requestRetryOptions) { public Mono process(HttpPipelineCallContext context, HttpPipelineNextPolicy next) { HttpRequest httpRequest = context.httpRequest(); boolean considerSecondary = (httpRequest.httpMethod().equals(HttpMethod.GET) - || httpRequest.httpMethod().equals(HttpMethod.HEAD)) - && (this.requestRetryOptions.secondaryHost() != null); + || httpRequest.httpMethod().equals(HttpMethod.HEAD)) + && (this.requestRetryOptions.secondaryHost() != null); return this.attemptAsync(httpRequest, next, 1, considerSecondary, 1); } /** - * This method actually attempts to send the request and determines if we should attempt again and, if so, how - * long to wait before sending out the next request. + * This method actually attempts to send the request and determines if we should attempt again and, if so, how long + * to wait before sending out the next request. *

- * Exponential retry algorithm: ((2 ^ attempt) - 1) * delay * random(0.8, 1.2) When to retry: connection failure - * or an HTTP status code of 500 or greater, except 501 and 505 If using a secondary: Odd tries go against - * primary; even tries go against the secondary For a primary wait ((2 ^ primaryTries - 1) * delay * random(0.8, - * 1.2) If secondary gets a 404, don't fail, retry but future retries are only against the primary When retrying - * against a secondary, ignore the retry count and wait (.1 second * random(0.8, 1.2)) + * Exponential retry algorithm: ((2 ^ attempt) - 1) * delay * random(0.8, 1.2) When to retry: connection failure or + * an HTTP status code of 500 or greater, except 501 and 505 If using a secondary: Odd tries go against primary; + * even tries go against the secondary For a primary wait ((2 ^ primaryTries - 1) * delay * random(0.8, 1.2) If + * secondary gets a 404, don't fail, retry but future retries are only against the primary When retrying against a + * secondary, ignore the retry count and wait (.1 second * random(0.8, 1.2)) * - * @param httpRequest - * The request to try. - * @param primaryTry - * This indicates how man tries we've attempted against the primary DC. - * @param considerSecondary - * Before each try, we'll select either the primary or secondary URL if appropriate. - * @param attempt - * This indicates the total number of attempts to send the request. - * - * @return A single containing either the successful response or an error that was not retryable because either - * the maxTries was exceeded or retries will not mitigate the issue. + * @param httpRequest The request to try. + * @param primaryTry This indicates how man tries we've attempted against the primary DC. + * @param considerSecondary Before each try, we'll select either the primary or secondary URL if appropriate. + * @param attempt This indicates the total number of attempts to send the request. + * @return A single containing either the successful response or an error that was not retryable because either the + * maxTries was exceeded or retries will not mitigate the issue. */ private Mono attemptAsync(final HttpRequest httpRequest, HttpPipelineNextPolicy next, final int primaryTry, - final boolean considerSecondary, - final int attempt) { + final boolean considerSecondary, + final int attempt) { // Determine which endpoint to try. It's primary if there is no secondary or if it is an odd number attempt. final boolean tryingPrimary = !considerSecondary || (attempt % 2 != 0); @@ -97,7 +92,7 @@ stream, the buffers that were emitted will have already been consumed (their pos duplicates the ByteBuffer object, not the underlying data. */ Flux bufferedBody = httpRequest.body() == null - ? null : httpRequest.body().map(ByteBuf::duplicate); + ? null : httpRequest.body().map(ByteBuf::duplicate); httpRequest.body(bufferedBody); if (!tryingPrimary) { UrlBuilder builder = UrlBuilder.parse(httpRequest.url()); @@ -114,55 +109,55 @@ stream, the buffers that were emitted will have already been consumed (their pos until after the retry backoff delay, so we call delaySubscription. */ return next.clone().process() - .timeout(Duration.ofSeconds(this.requestRetryOptions.tryTimeout())) - .delaySubscription(Duration.ofMillis(delayMs)) - .flatMap(response -> { - boolean newConsiderSecondary = considerSecondary; - String action; - int statusCode = response.statusCode(); + .timeout(Duration.ofSeconds(this.requestRetryOptions.tryTimeout())) + .delaySubscription(Duration.ofMillis(delayMs)) + .flatMap(response -> { + boolean newConsiderSecondary = considerSecondary; + String action; + int statusCode = response.statusCode(); /* If attempt was against the secondary & it returned a StatusNotFound (404), then the resource was not found. This may be due to replication delay. So, in this case, we'll never try the secondary again for this operation. */ - if (!tryingPrimary && statusCode == 404) { - newConsiderSecondary = false; - action = "Retry: Secondary URL returned 404"; - } else if (statusCode == 503 || statusCode == 500) { - action = "Retry: Temporary error or server timeout"; - } else { - action = "NoRetry: Successful HTTP request"; - } - - if (action.charAt(0) == 'R' && attempt < requestRetryOptions.maxTries()) { + if (!tryingPrimary && statusCode == 404) { + newConsiderSecondary = false; + action = "Retry: Secondary URL returned 404"; + } else if (statusCode == 503 || statusCode == 500) { + action = "Retry: Temporary error or server timeout"; + } else { + action = "NoRetry: Successful HTTP request"; + } + + if (action.charAt(0) == 'R' && attempt < requestRetryOptions.maxTries()) { /* We increment primaryTry if we are about to try the primary again (which is when we consider the secondary and tried the secondary this time (tryingPrimary==false) or we do not consider the secondary at all (considerSecondary==false)). This will ensure primaryTry is correct when passed to calculate the delay. */ - int newPrimaryTry = !tryingPrimary || !considerSecondary - ? primaryTry + 1 : primaryTry; - return attemptAsync(httpRequest, next, newPrimaryTry, newConsiderSecondary, - attempt + 1); - } - return Mono.just(response); - }) - .onErrorResume(throwable -> { + int newPrimaryTry = !tryingPrimary || !considerSecondary + ? primaryTry + 1 : primaryTry; + return attemptAsync(httpRequest, next, newPrimaryTry, newConsiderSecondary, + attempt + 1); + } + return Mono.just(response); + }) + .onErrorResume(throwable -> { /* It is likely that many users will not realize that their Flux must be replayable and get an error upon retries when the provided data length does not match the length of the exact data. We cannot enforce the desired Flux behavior, so we provide a hint when this is likely the root cause. */ - if (throwable instanceof IllegalStateException && attempt > 1) { - return Mono.error(new IllegalStateException("The request failed because the " - + "size of the contents of the provided Flux did not match the provided " - + "data size upon attempting to retry. This is likely caused by the Flux " - + "not being replayable. To support retries, all Fluxes must produce the " - + "same data for each subscriber. Please ensure this behavior.", throwable)); - } + if (throwable instanceof IllegalStateException && attempt > 1) { + return Mono.error(new IllegalStateException("The request failed because the " + + "size of the contents of the provided Flux did not match the provided " + + "data size upon attempting to retry. This is likely caused by the Flux " + + "not being replayable. To support retries, all Fluxes must produce the " + + "same data for each subscriber. Please ensure this behavior.", throwable)); + } /* IOException is a catch-all for IO related errors. Technically it includes many types which may @@ -170,28 +165,28 @@ we do not consider the secondary at all (considerSecondary==false)). This will either case, it is better to optimistically retry instead of failing too soon. A Timeout Exception is a client-side timeout coming from Rx. */ - String action; - if (throwable instanceof IOException) { - action = "Retry: Network error"; - } else if (throwable instanceof TimeoutException) { - action = "Retry: Client timeout"; - } else { - action = "NoRetry: Unknown error"; - } - - if (action.charAt(0) == 'R' && attempt < requestRetryOptions.maxTries()) { + String action; + if (throwable instanceof IOException) { + action = "Retry: Network error"; + } else if (throwable instanceof TimeoutException) { + action = "Retry: Client timeout"; + } else { + action = "NoRetry: Unknown error"; + } + + if (action.charAt(0) == 'R' && attempt < requestRetryOptions.maxTries()) { /* We increment primaryTry if we are about to try the primary again (which is when we consider the secondary and tried the secondary this time (tryingPrimary==false) or we do not consider the secondary at all (considerSecondary==false)). This will ensure primaryTry is correct when passed to calculate the delay. */ - int newPrimaryTry = !tryingPrimary || !considerSecondary - ? primaryTry + 1 : primaryTry; - return attemptAsync(httpRequest, next, newPrimaryTry, considerSecondary, - attempt + 1); - } - return Mono.error(throwable); - }); + int newPrimaryTry = !tryingPrimary || !considerSecondary + ? primaryTry + 1 : primaryTry; + return attemptAsync(httpRequest, next, newPrimaryTry, considerSecondary, + attempt + 1); + } + return Mono.error(throwable); + }); } } diff --git a/storage/client/blob/src/main/java/com/azure/storage/common/policy/RetryPolicyType.java b/storage/client/common/src/main/java/com/azure/storage/common/policy/RetryPolicyType.java similarity index 100% rename from storage/client/blob/src/main/java/com/azure/storage/common/policy/RetryPolicyType.java rename to storage/client/common/src/main/java/com/azure/storage/common/policy/RetryPolicyType.java diff --git a/storage/client/queue/src/main/java/com/azure/storage/common/policy/SASTokenCredentialPolicy.java b/storage/client/common/src/main/java/com/azure/storage/common/policy/SASTokenCredentialPolicy.java similarity index 99% rename from storage/client/queue/src/main/java/com/azure/storage/common/policy/SASTokenCredentialPolicy.java rename to storage/client/common/src/main/java/com/azure/storage/common/policy/SASTokenCredentialPolicy.java index ba2debfcd3620..4af3edf7f229b 100644 --- a/storage/client/queue/src/main/java/com/azure/storage/common/policy/SASTokenCredentialPolicy.java +++ b/storage/client/common/src/main/java/com/azure/storage/common/policy/SASTokenCredentialPolicy.java @@ -22,6 +22,7 @@ public final class SASTokenCredentialPolicy implements HttpPipelinePolicy { /** * Creates a SAS token credential policy that appends the SAS token to the request URL's query. + * * @param credential SAS token credential */ public SASTokenCredentialPolicy(SASTokenCredential credential) { diff --git a/storage/client/blob/src/main/java/com/azure/storage/common/policy/SharedKeyCredentialPolicy.java b/storage/client/common/src/main/java/com/azure/storage/common/policy/SharedKeyCredentialPolicy.java similarity index 100% rename from storage/client/blob/src/main/java/com/azure/storage/common/policy/SharedKeyCredentialPolicy.java rename to storage/client/common/src/main/java/com/azure/storage/common/policy/SharedKeyCredentialPolicy.java diff --git a/storage/client/blob/src/main/java/com/azure/storage/common/policy/package-info.java b/storage/client/common/src/main/java/com/azure/storage/common/policy/package-info.java similarity index 100% rename from storage/client/blob/src/main/java/com/azure/storage/common/policy/package-info.java rename to storage/client/common/src/main/java/com/azure/storage/common/policy/package-info.java diff --git a/storage/client/file/pom.xml b/storage/client/file/pom.xml index 5355d42110408..30f886983ae4d 100644 --- a/storage/client/file/pom.xml +++ b/storage/client/file/pom.xml @@ -37,6 +37,11 @@ azure-core 1.0.0-preview.2 + + com.azure + azure-storage-common + 12.0.0-preview.2 +