diff --git a/sdk/core/azure-core-http-jdk-httpclient/src/main/java/com/azure/core/http/jdk/httpclient/implementation/JdkHttpUtils.java b/sdk/core/azure-core-http-jdk-httpclient/src/main/java/com/azure/core/http/jdk/httpclient/implementation/JdkHttpUtils.java index 85ae06b2f5a3..bc9a29d746c2 100644 --- a/sdk/core/azure-core-http-jdk-httpclient/src/main/java/com/azure/core/http/jdk/httpclient/implementation/JdkHttpUtils.java +++ b/sdk/core/azure-core-http-jdk-httpclient/src/main/java/com/azure/core/http/jdk/httpclient/implementation/JdkHttpUtils.java @@ -3,6 +3,7 @@ package com.azure.core.http.jdk.httpclient.implementation; import com.azure.core.http.HttpHeaders; +import com.azure.core.implementation.util.HttpHeadersAccessHelper; import com.azure.core.util.CoreUtils; import java.nio.ByteBuffer; @@ -25,13 +26,14 @@ public final class JdkHttpUtils { * @param headers the JDK Http headers * @return the azure-core Http headers */ - @SuppressWarnings("deprecation") public static HttpHeaders fromJdkHttpHeaders(java.net.http.HttpHeaders headers) { final HttpHeaders httpHeaders = new HttpHeaders((int) (headers.map().size() / 0.75F)); for (Map.Entry> kvp : headers.map().entrySet()) { if (!CoreUtils.isNullOrEmpty(kvp.getValue())) { - httpHeaders.set(kvp.getKey(), kvp.getValue()); + // JDK HttpClient parses headers to lower case, use the access helper to bypass lowercasing the header + // name (or in this case, just checking that the header name is lowercased). + HttpHeadersAccessHelper.setInternal(httpHeaders, kvp.getKey(), kvp.getKey(), kvp.getValue()); } } diff --git a/sdk/core/azure-core-http-netty/src/main/java/com/azure/core/http/netty/implementation/NettyAsyncHttpResponseBase.java b/sdk/core/azure-core-http-netty/src/main/java/com/azure/core/http/netty/implementation/NettyAsyncHttpResponseBase.java index 5346aeb2948a..6621a80eea46 100644 --- a/sdk/core/azure-core-http-netty/src/main/java/com/azure/core/http/netty/implementation/NettyAsyncHttpResponseBase.java +++ b/sdk/core/azure-core-http-netty/src/main/java/com/azure/core/http/netty/implementation/NettyAsyncHttpResponseBase.java @@ -7,10 +7,13 @@ import com.azure.core.http.HttpHeaders; import com.azure.core.http.HttpRequest; import com.azure.core.http.HttpResponse; +import com.azure.core.implementation.util.HttpHeadersAccessHelper; +import io.netty.util.AsciiString; import reactor.netty.http.client.HttpClientResponse; import java.util.Iterator; import java.util.Map; +import java.util.Objects; /** * Base response class for Reactor Netty with implementations for response metadata. @@ -40,8 +43,19 @@ public abstract class NettyAsyncHttpResponseBase extends HttpResponse { while (nettyHeadersIterator.hasNext()) { Map.Entry next = nettyHeadersIterator.next(); // Value may be null and that needs to be guarded but key should never be null. - CharSequence value = next.getValue(); - this.headers.add(next.getKey().toString(), (value == null) ? null : value.toString()); + String value = Objects.toString(next.getValue(), null); + CharSequence key = next.getKey(); + + // Check for the header name being a Netty AsciiString as it has optimizations around lowercasing. + if (key instanceof AsciiString) { + // Hook into optimizations exposed through shared implementation to speed up the conversion. + AsciiString asciiString = (AsciiString) key; + HttpHeadersAccessHelper.addInternal(headers, asciiString.toLowerCase().toString(), + asciiString.toString(), value); + } else { + // If it isn't an AsciiString, then fallback to the shared, albeit, slower path. + this.headers.add(key.toString(), value); + } } } else { this.headers = new NettyToAzureCoreHttpHeadersWrapper(nettyHeaders); diff --git a/sdk/core/azure-core-http-netty/src/main/java/com/azure/core/http/netty/implementation/Utility.java b/sdk/core/azure-core-http-netty/src/main/java/com/azure/core/http/netty/implementation/Utility.java index 33a18a4fe343..670e41710e34 100644 --- a/sdk/core/azure-core-http-netty/src/main/java/com/azure/core/http/netty/implementation/Utility.java +++ b/sdk/core/azure-core-http-netty/src/main/java/com/azure/core/http/netty/implementation/Utility.java @@ -51,10 +51,9 @@ public final class Utility { * @return A newly allocated {@link ByteBuffer} containing the copied bytes. */ public static ByteBuffer deepCopyBuffer(ByteBuf byteBuf) { - ByteBuffer buffer = ByteBuffer.allocate(byteBuf.readableBytes()); - byteBuf.readBytes(buffer); - buffer.rewind(); - return buffer; + byte[] bytes = new byte[byteBuf.readableBytes()]; + byteBuf.getBytes(byteBuf.readerIndex(), bytes); + return ByteBuffer.wrap(bytes); } /** diff --git a/sdk/core/azure-core/spotbugs-exclude.xml b/sdk/core/azure-core/spotbugs-exclude.xml index f41181ab8e8e..f43dd8615ced 100644 --- a/sdk/core/azure-core/spotbugs-exclude.xml +++ b/sdk/core/azure-core/spotbugs-exclude.xml @@ -441,4 +441,10 @@ + + + + + + diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/http/HttpHeaders.java b/sdk/core/azure-core/src/main/java/com/azure/core/http/HttpHeaders.java index b42992287c1a..c52fa19e5cbd 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/http/HttpHeaders.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/http/HttpHeaders.java @@ -33,7 +33,22 @@ public class HttpHeaders implements Iterable { private final Map headers; static { - HttpHeadersAccessHelper.setAccessor(headers -> headers.headers); + HttpHeadersAccessHelper.setAccessor(new HttpHeadersAccessHelper.HttpHeadersAccessor() { + @Override + public Map getRawHeaderMap(HttpHeaders headers) { + return headers.headers; + } + + @Override + public void addInternal(HttpHeaders headers, String formattedName, String name, String value) { + headers.addInternal(formattedName, name, value); + } + + @Override + public void setInternal(HttpHeaders headers, String formattedName, String name, List values) { + headers.setInternal(formattedName, name, values); + } + }); } /** diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/http/rest/RestProxy.java b/sdk/core/azure-core/src/main/java/com/azure/core/http/rest/RestProxy.java index 6e3c8d9a611e..de36dac2045e 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/http/rest/RestProxy.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/http/rest/RestProxy.java @@ -82,8 +82,6 @@ public Mono send(HttpRequest request, Context contextData) { @Override public Object invoke(Object proxy, final Method method, Object[] args) { - RestProxyUtils.validateResumeOperationIsNotPresent(method); - // Note: request options need to be evaluated here, as it is a public class with package private methods. // Evaluating here allows the package private methods to be invoked here for downstream use. final SwaggerMethodParser methodParser = getMethodParser(method); diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/implementation/http/rest/AsyncRestProxy.java b/sdk/core/azure-core/src/main/java/com/azure/core/implementation/http/rest/AsyncRestProxy.java index a09db64f1d79..2548bec2a64f 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/implementation/http/rest/AsyncRestProxy.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/implementation/http/rest/AsyncRestProxy.java @@ -75,8 +75,6 @@ Mono send(HttpRequest request, Context contextData) { @SuppressWarnings({ "try", "unused" }) public Object invoke(Object proxy, Method method, RequestOptions options, EnumSet errorOptions, Consumer requestCallback, SwaggerMethodParser methodParser, HttpRequest request, Context context) { - RestProxyUtils.validateResumeOperationIsNotPresent(method); - context = startTracingSpan(methodParser, context); // If there is 'RequestOptions' apply its request callback operations before validating the body. diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/implementation/http/rest/RestProxyBase.java b/sdk/core/azure-core/src/main/java/com/azure/core/implementation/http/rest/RestProxyBase.java index af018bfc0d95..67f4ea40662e 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/implementation/http/rest/RestProxyBase.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/implementation/http/rest/RestProxyBase.java @@ -98,8 +98,6 @@ public RestProxyBase(HttpPipeline httpPipeline, SerializerAdapter serializer, */ public final Object invoke(Object proxy, Method method, RequestOptions options, EnumSet errorOptions, Consumer requestCallback, SwaggerMethodParser methodParser, boolean isAsync, Object[] args) { - RestProxyUtils.validateResumeOperationIsNotPresent(method); - try { HttpRequest request = createHttpRequest(methodParser, serializer, isAsync, args); diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/implementation/http/rest/SwaggerMethodParser.java b/sdk/core/azure-core/src/main/java/com/azure/core/implementation/http/rest/SwaggerMethodParser.java index 7401e138f985..94f1674f3348 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/implementation/http/rest/SwaggerMethodParser.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/implementation/http/rest/SwaggerMethodParser.java @@ -285,6 +285,8 @@ public SwaggerMethodParser(Method swaggerMethod) { this.responseEagerlyRead = isResponseEagerlyRead(unwrappedReturnType); this.ignoreResponseBody = isResponseBodyIgnored(unwrappedReturnType); this.spanName = interfaceParser.getServiceName() + "." + swaggerMethod.getName(); + + RestProxyUtils.validateResumeOperationIsNotPresent(swaggerMethod); } /** diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/implementation/util/HttpHeadersAccessHelper.java b/sdk/core/azure-core/src/main/java/com/azure/core/implementation/util/HttpHeadersAccessHelper.java index 304f082277fa..bbf6c87e2250 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/implementation/util/HttpHeadersAccessHelper.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/implementation/util/HttpHeadersAccessHelper.java @@ -5,6 +5,8 @@ import com.azure.core.http.HttpHeader; import com.azure.core.http.HttpHeaders; +import java.util.List; +import java.util.Locale; import java.util.Map; /** @@ -24,6 +26,34 @@ public interface HttpHeadersAccessor { * @return The raw header map. */ Map getRawHeaderMap(HttpHeaders headers); + + /** + * Adds a header value to the backing map in {@link HttpHeaders}. + *

+ * This bypasses using {@link HttpHeaders#add(String, String)} which uses {@link String#toLowerCase(Locale)}, + * which may be slower than options available by implementing HTTP stacks (such as Netty which has an ASCII + * string class which has optimizations around lowercasing due to ASCII constraints). + * + * @param headers The {@link HttpHeaders} to add the header to. + * @param formattedName The lower-cased header name. + * @param name The original header name. + * @param value The header value. + */ + void addInternal(HttpHeaders headers, String formattedName, String name, String value); + + /** + * Sets a header value to the backing map in {@link HttpHeaders}. + *

+ * This bypasses using {@link HttpHeaders#set(String, List)} which uses {@link String#toLowerCase(Locale)}, + * which may be slower than options available by implementing HTTP stacks (such as JDK HttpClient where all + * response header names are already lowercased). + * + * @param headers The {@link HttpHeaders} to set the header to. + * @param formattedName The lower-cased header name. + * @param name The original header name. + * @param values The header values. + */ + void setInternal(HttpHeaders headers, String formattedName, String name, List values); } /** @@ -36,6 +66,38 @@ public static Map getRawHeaderMap(HttpHeaders headers) { return accessor.getRawHeaderMap(headers); } + /** + * Adds a header value to the backing map in {@link HttpHeaders}. + *

+ * This bypasses using {@link HttpHeaders#add(String, String)} which uses {@link String#toLowerCase(Locale)}, + * which may be slower than options available by implementing HTTP stacks (such as Netty which has an ASCII + * string class which has optimizations around lowercasing due to ASCII constraints). + * + * @param headers The {@link HttpHeaders} to add the header to. + * @param formattedName The lower-cased header name. + * @param name The original header name. + * @param value The header value. + */ + public static void addInternal(HttpHeaders headers, String formattedName, String name, String value) { + accessor.addInternal(headers, formattedName, name, value); + } + + /** + * Sets a header value to the backing map in {@link HttpHeaders}. + *

+ * This bypasses using {@link HttpHeaders#set(String, List)} which uses {@link String#toLowerCase(Locale)}, + * which may be slower than options available by implementing HTTP stacks (such as JDK HttpClient where all + * response header names are already lowercased). + * + * @param headers The {@link HttpHeaders} to set the header to. + * @param formattedName The lower-cased header name. + * @param name The original header name. + * @param values The header values. + */ + public static void setInternal(HttpHeaders headers, String formattedName, String name, List values) { + accessor.setInternal(headers, formattedName, name, values); + } + /** * Sets the {@link HttpHeadersAccessor}. * diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/util/serializer/SerializerEncoding.java b/sdk/core/azure-core/src/main/java/com/azure/core/util/serializer/SerializerEncoding.java index 4d4bf4985d5f..c8207428a790 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/util/serializer/SerializerEncoding.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/util/serializer/SerializerEncoding.java @@ -9,9 +9,6 @@ import com.azure.core.util.logging.ClientLogger; import com.azure.core.util.logging.LogLevel; -import java.util.Map; -import java.util.TreeMap; - /** * Supported serialization encoding formats. */ @@ -32,20 +29,6 @@ public enum SerializerEncoding { TEXT; private static final ClientLogger LOGGER = new ClientLogger(SerializerEncoding.class); - private static final Map SUPPORTED_MIME_TYPES; - - static { - // Encodings and suffixes from: https://tools.ietf.org/html/rfc6838 - SUPPORTED_MIME_TYPES = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); - SUPPORTED_MIME_TYPES.put("text/xml", XML); - SUPPORTED_MIME_TYPES.put("application/xml", XML); - SUPPORTED_MIME_TYPES.put("application/json", JSON); - SUPPORTED_MIME_TYPES.put("text/css", TEXT); - SUPPORTED_MIME_TYPES.put("text/csv", TEXT); - SUPPORTED_MIME_TYPES.put("text/html", TEXT); - SUPPORTED_MIME_TYPES.put("text/javascript", TEXT); - SUPPORTED_MIME_TYPES.put("text/plain", TEXT); - } /** * Determines the serializer encoding to use based on the Content-Type header. @@ -63,7 +46,7 @@ public static SerializerEncoding fromHeaders(HttpHeaders headers) { int contentTypeEnd = mimeContentType.indexOf(';'); String contentType = (contentTypeEnd == -1) ? mimeContentType : mimeContentType.substring(0, contentTypeEnd); - final SerializerEncoding encoding = SUPPORTED_MIME_TYPES.get(contentType); + SerializerEncoding encoding = checkForKnownEncoding(contentType); if (encoding != null) { return encoding; } @@ -97,4 +80,43 @@ public static SerializerEncoding fromHeaders(HttpHeaders headers) { return JSON; } + + /* + * There is a limited set of serialization encodings that are known ahead of time. Instead of using a TreeMap with + * a case-insensitive comparator, use an optimized search specifically for the known encodings. + */ + private static SerializerEncoding checkForKnownEncoding(String contentType) { + int length = contentType.length(); + + // Check the length of the content type first as it is a quick check. + if (length != 8 && length != 9 && length != 10 && length != 15 && length != 16) { + return null; + } + + if ("text/".regionMatches(true, 0, contentType, 0, 5)) { + if (length == 8) { + if ("xml".regionMatches(true, 0, contentType, 5, 3)) { + return XML; + } else if ("csv".regionMatches(true, 0, contentType, 5, 3)) { + return TEXT; + } else if ("css".regionMatches(true, 0, contentType, 5, 3)) { + return TEXT; + } + } else if (length == 9 && "html".regionMatches(true, 0, contentType, 5, 4)) { + return TEXT; + } else if (length == 10 && "plain".regionMatches(true, 0, contentType, 5, 5)) { + return TEXT; + } else if (length == 15 && "javascript".regionMatches(true, 0, contentType, 5, 10)) { + return TEXT; + } + } else if ("application/".regionMatches(true, 0, contentType, 0, 12)) { + if (length == 16 && "json".regionMatches(true, 0, contentType, 12, 4)) { + return JSON; + } else if (length == 15 && "xml".regionMatches(true, 0, contentType, 12, 3)) { + return XML; + } + } + + return null; + } }