com.azure.core.implementation.ImplUtils Maven / Gradle / Ivy
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.core.implementation;
import com.azure.core.http.HttpHeaderName;
import com.azure.core.http.HttpHeaders;
import com.azure.core.util.CoreUtils;
import com.azure.core.util.DateTimeRfc1123;
import com.azure.core.util.FluxUtil;
import com.azure.core.util.UrlBuilder;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URL;
import java.nio.ByteBuffer;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.OffsetDateTime;
import java.time.temporal.ChronoUnit;
import java.util.AbstractMap;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.function.Function;
import java.util.function.Supplier;
/**
* Utility class containing implementation specific methods.
*/
public final class ImplUtils {
private static final HttpHeaderName RETRY_AFTER_MS_HEADER = HttpHeaderName.fromString("retry-after-ms");
private static final HttpHeaderName X_MS_RETRY_AFTER_MS_HEADER = HttpHeaderName.fromString("x-ms-retry-after-ms");
// future improvement - make this configurable
public static final int MAX_CACHE_SIZE = 10000;
/**
* Attempts to extract a retry after duration from a given set of {@link HttpHeaders}.
*
* This searches for the well-known retry after headers {@code Retry-After}, {@code retry-after-ms}, and
* {@code x-ms-retry-after-ms}.
*
* If no well-known headers are found null will be returned.
*
* @param headers The set of headers to search for a well-known retry after header.
* @param nowSupplier A supplier for the current time used when {@code Retry-After} is using relative retry after
* time.
* @return The retry after duration if a well-known retry after header was found, otherwise null.
*/
public static Duration getRetryAfterFromHeaders(HttpHeaders headers, Supplier nowSupplier) {
// Found 'x-ms-retry-after-ms' header, use a Duration of milliseconds based on the value.
Duration retryDelay = tryGetRetryDelay(headers, X_MS_RETRY_AFTER_MS_HEADER, ImplUtils::tryGetDelayMillis);
if (retryDelay != null) {
return retryDelay;
}
// Found 'retry-after-ms' header, use a Duration of milliseconds based on the value.
retryDelay = tryGetRetryDelay(headers, RETRY_AFTER_MS_HEADER, ImplUtils::tryGetDelayMillis);
if (retryDelay != null) {
return retryDelay;
}
// Found 'Retry-After' header. First, attempt to resolve it as a Duration of seconds. If that fails, then
// attempt to resolve it as an HTTP date (RFC1123).
retryDelay = tryGetRetryDelay(headers, HttpHeaderName.RETRY_AFTER,
headerValue -> tryParseLongOrDateTime(headerValue, nowSupplier));
// Either the retry delay will have been found or it'll be null, null indicates no retry after.
return retryDelay;
}
private static Duration tryGetRetryDelay(HttpHeaders headers, HttpHeaderName headerName,
Function delayParser) {
String headerValue = headers.getValue(headerName);
return CoreUtils.isNullOrEmpty(headerValue) ? null : delayParser.apply(headerValue);
}
private static Duration tryGetDelayMillis(String value) {
long delayMillis = tryParseLong(value);
return (delayMillis >= 0) ? Duration.ofMillis(delayMillis) : null;
}
private static Duration tryParseLongOrDateTime(String value, Supplier nowSupplier) {
long delaySeconds;
try {
OffsetDateTime retryAfter = new DateTimeRfc1123(value).getDateTime();
delaySeconds = nowSupplier.get().until(retryAfter, ChronoUnit.SECONDS);
} catch (DateTimeException ex) {
delaySeconds = tryParseLong(value);
}
return (delaySeconds >= 0) ? Duration.ofSeconds(delaySeconds) : null;
}
private static long tryParseLong(String value) {
try {
return Long.parseLong(value);
} catch (NumberFormatException ex) {
return -1;
}
}
/**
* Writes a {@link ByteBuffer} into an {@link OutputStream}.
*
* This method provides writing optimization based on the type of {@link ByteBuffer} and {@link OutputStream}
* passed. For example, if the {@link ByteBuffer} has a backing {@code byte[]} this method will access that directly
* to write to the {@code stream} instead of buffering the contents of the {@link ByteBuffer} into a temporary
* buffer.
*
* @param buffer The {@link ByteBuffer} to write into the {@code stream}.
* @param stream The {@link OutputStream} where the {@code buffer} will be written.
* @throws IOException If an I/O occurs while writing the {@code buffer} into the {@code stream}.
*/
public static void writeByteBufferToStream(ByteBuffer buffer, OutputStream stream) throws IOException {
// First check if the buffer has a backing byte[]. The backing byte[] can be accessed directly and written
// without an additional buffering byte[].
if (buffer.hasArray()) {
// Write the byte[] from the current view position to the length remaining in the view.
stream.write(buffer.array(), buffer.position(), buffer.remaining());
// Update the position of the ByteBuffer to treat this the same as getting from the buffer.
buffer.position(buffer.position() + buffer.remaining());
return;
}
// Next begin checking for specific instances of OutputStream that may provide better writing options for
// direct ByteBuffers.
if (stream instanceof FileOutputStream) {
FileOutputStream fileOutputStream = (FileOutputStream) stream;
// Writing to the FileChannel directly may provide native optimizations for moving the OS managed memory
// into the file.
// Write will move both the OutputStream's and ByteBuffer's position so there is no need to perform
// additional updates that are required when using the backing array.
fileOutputStream.getChannel().write(buffer);
return;
}
// All optimizations have been exhausted, fallback to buffering write.
stream.write(FluxUtil.byteBufferToArray(buffer));
}
/**
* Utility method for parsing a {@link URL} into a {@link UrlBuilder}.
*
* @param url The URL being parsed.
* @param includeQuery Whether the query string should be excluded.
* @return The UrlBuilder that represents the parsed URL.
*/
public static UrlBuilder parseUrl(URL url, boolean includeQuery) {
final UrlBuilder result = new UrlBuilder();
if (url != null) {
final String protocol = url.getProtocol();
if (protocol != null && !protocol.isEmpty()) {
result.setScheme(protocol);
}
final String host = url.getHost();
if (host != null && !host.isEmpty()) {
result.setHost(host);
}
final int port = url.getPort();
if (port != -1) {
result.setPort(port);
}
final String path = url.getPath();
if (path != null && !path.isEmpty()) {
result.setPath(path);
}
final String query = url.getQuery();
if (query != null && !query.isEmpty() && includeQuery) {
result.setQuery(query);
}
}
return result;
}
public static Iterator> parseQueryParameters(String queryParameters) {
return (CoreUtils.isNullOrEmpty(queryParameters))
? Collections.emptyIterator()
: new QueryParameterIterator(queryParameters);
}
private static final class QueryParameterIterator implements Iterator> {
private final String queryParameters;
private boolean done = false;
private int position;
QueryParameterIterator(String queryParameters) {
this.queryParameters = queryParameters;
// If the URL query begins with '?' the first possible start of a query parameter key is the
// second character in the query.
position = (queryParameters.startsWith("?")) ? 1 : 0;
}
@Override
public boolean hasNext() {
return !done;
}
@Override
public Map.Entry next() {
if (done) {
throw new NoSuchElementException();
}
int nextPosition = queryParameters.indexOf('=', position);
if (nextPosition == -1) {
// Query parameters completed with a key only 'https://example.com?param'
done = true;
return new AbstractMap.SimpleImmutableEntry<>(queryParameters.substring(position), "");
}
String key = queryParameters.substring(position, nextPosition);
// Position is set to nextPosition + 1 to skip over the '='
position = nextPosition + 1;
nextPosition = queryParameters.indexOf('&', position);
String value = null;
if (nextPosition == -1) {
// This was the last key-value pair in the query parameters 'https://example.com?param=done'
done = true;
value = queryParameters.substring(position);
} else {
value = queryParameters.substring(position, nextPosition);
// Position is set to nextPosition + 1 to skip over the '&'
position = nextPosition + 1;
}
return new AbstractMap.SimpleImmutableEntry<>(key, value);
}
}
private ImplUtils() {
}
}