From d3b5536d401adcdb85322ddd675074a8c01a88de Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 14 Aug 2023 12:56:14 -0400 Subject: [PATCH] - adds headers inspection middleware --- CHANGELOG.md | 6 ++ .../microsoft/kiota/CaseInsensitiveMap.java | 5 +- .../java/com/microsoft/kiota/Headers.java | 13 ++- .../http/okHttp/spotBugsExcludeFilter.xml | 5 ++ .../kiota/http/KiotaClientFactory.java | 4 +- .../middleware/HeadersInspectionHandler.java | 79 +++++++++++++++++ .../options/HeadersInspectionOption.java | 87 +++++++++++++++++++ .../options/UserAgentHandlerOption.java | 2 +- .../http/HeadersInspectionHandlerTest.java | 74 ++++++++++++++++ .../kiota/http/UserAgentHandlerTest.java | 10 +-- gradle.properties | 2 +- 11 files changed, 274 insertions(+), 13 deletions(-) create mode 100644 components/http/okHttp/src/main/java/com/microsoft/kiota/http/middleware/HeadersInspectionHandler.java create mode 100644 components/http/okHttp/src/main/java/com/microsoft/kiota/http/middleware/options/HeadersInspectionOption.java create mode 100644 components/http/okHttp/src/test/java/com/microsoft/kiota/http/HeadersInspectionHandlerTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 92f926408..a04db00c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +## [0.7.0] - 2023-08-18 + +### Added + +- Added headers inspection option and handler. + ## [0.6.0] - 2023-08-08 ### Changed diff --git a/components/abstractions/src/main/java/com/microsoft/kiota/CaseInsensitiveMap.java b/components/abstractions/src/main/java/com/microsoft/kiota/CaseInsensitiveMap.java index cfb113aba..1cc5bc0da 100644 --- a/components/abstractions/src/main/java/com/microsoft/kiota/CaseInsensitiveMap.java +++ b/components/abstractions/src/main/java/com/microsoft/kiota/CaseInsensitiveMap.java @@ -13,7 +13,10 @@ import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; -class CaseInsensitiveMap implements Map>{ +/** + * A map that is case insensitive on the keys + */ +public class CaseInsensitiveMap implements Map>{ private final HashMap> internalMap = new HashMap<>(); /** diff --git a/components/abstractions/src/main/java/com/microsoft/kiota/Headers.java b/components/abstractions/src/main/java/com/microsoft/kiota/Headers.java index 8be7e6a74..6517812db 100644 --- a/components/abstractions/src/main/java/com/microsoft/kiota/Headers.java +++ b/components/abstractions/src/main/java/com/microsoft/kiota/Headers.java @@ -6,14 +6,19 @@ import jakarta.annotation.Nonnull; -class Headers extends CaseInsensitiveMap { +/** + * A class representing the headers of a request or a response. + */ +public abstract class Headers extends CaseInsensitiveMap { /** Default constructor */ - public Headers() { + protected Headers() { super(); } - /** Copy constructor */ - public Headers(@Nonnull Headers headers) { + /** Copy constructor + * @param headers The headers to initialize with. + */ + protected Headers(@Nonnull Headers headers) { super(); Objects.requireNonNull(headers); putAll(headers); diff --git a/components/http/okHttp/spotBugsExcludeFilter.xml b/components/http/okHttp/spotBugsExcludeFilter.xml index ed240fade..d410faed0 100644 --- a/components/http/okHttp/spotBugsExcludeFilter.xml +++ b/components/http/okHttp/spotBugsExcludeFilter.xml @@ -7,9 +7,14 @@ xsi:schemaLocation="https://github.com/spotbugs/filter/3.0.0 https://raw.githubu + + + + + diff --git a/components/http/okHttp/src/main/java/com/microsoft/kiota/http/KiotaClientFactory.java b/components/http/okHttp/src/main/java/com/microsoft/kiota/http/KiotaClientFactory.java index 9517ddf60..9d139e86c 100644 --- a/components/http/okHttp/src/main/java/com/microsoft/kiota/http/KiotaClientFactory.java +++ b/components/http/okHttp/src/main/java/com/microsoft/kiota/http/KiotaClientFactory.java @@ -6,6 +6,7 @@ import com.microsoft.kiota.http.middleware.RedirectHandler; import com.microsoft.kiota.http.middleware.RetryHandler; import com.microsoft.kiota.http.middleware.UserAgentHandler; +import com.microsoft.kiota.http.middleware.HeadersInspectionHandler; import com.microsoft.kiota.http.middleware.ParametersNameDecodingHandler; import okhttp3.Interceptor; @@ -50,7 +51,8 @@ public static Interceptor[] createDefaultInterceptors() { new RedirectHandler(), new RetryHandler(), new ParametersNameDecodingHandler(), - new UserAgentHandler() + new UserAgentHandler(), + new HeadersInspectionHandler() }; } } \ No newline at end of file diff --git a/components/http/okHttp/src/main/java/com/microsoft/kiota/http/middleware/HeadersInspectionHandler.java b/components/http/okHttp/src/main/java/com/microsoft/kiota/http/middleware/HeadersInspectionHandler.java new file mode 100644 index 000000000..3c17324f0 --- /dev/null +++ b/components/http/okHttp/src/main/java/com/microsoft/kiota/http/middleware/HeadersInspectionHandler.java @@ -0,0 +1,79 @@ +package com.microsoft.kiota.http.middleware; + +import jakarta.annotation.Nonnull; +import kotlin.Pair; + +import java.io.IOException; +import java.util.Objects; +import java.util.Set; + +import com.microsoft.kiota.http.middleware.options.HeadersInspectionOption; +import com.microsoft.kiota.http.middleware.options.RetryHandlerOption; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Scope; +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +/** + * The middleware responsible for inspecting the request and response headers + */ +public class HeadersInspectionHandler implements Interceptor { + /** + * Create a new instance of the HeadersInspectionHandler class with the default options + */ + public HeadersInspectionHandler() { + this(new HeadersInspectionOption()); + } + /** + * Create a new instance of the HeadersInspectionHandler class with the provided options + * @param options The options to use for the handler + */ + public HeadersInspectionHandler(@Nonnull final HeadersInspectionOption options) { + this.options = Objects.requireNonNull(options); + } + private final HeadersInspectionOption options; + + /** {@inheritDoc} */ + @Nonnull + @Override + @SuppressWarnings("UnknownNullness") + public Response intercept(final Chain chain) throws IOException { + Objects.requireNonNull(chain, "parameter chain cannot be null"); + Request request = chain.request(); + HeadersInspectionOption inspectionOption = request.tag(HeadersInspectionOption.class); + if(inspectionOption == null) { inspectionOption = options; } + final Span span = ObservabilityHelper.getSpanForRequest(request, "HeadersInspectionHandler_Intercept"); + Scope scope = null; + if (span != null) { + scope = span.makeCurrent(); + span.setAttribute("com.microsoft.kiota.handler.headersInspection.enable", true); + } + try { + if (span != null) { + request = request.newBuilder().tag(Span.class, span).build(); + } + if (inspectionOption.getInspectRequestHeaders()) { + for(final Pair header : request.headers()) { + inspectionOption.getRequestHeaders().put(header.getFirst(), Set.of(header.getSecond())); + } + } + final Response response = chain.proceed(request); + if (inspectionOption.getInspectResponseHeaders()) { + for(final Pair header : response.headers()) { + inspectionOption.getResponseHeaders().put(header.getFirst(), Set.of(header.getSecond())); + } + } + return response; + } finally { + if (scope != null) { + scope.close(); + } + if (span != null) { + span.end(); + } + } + } + +} diff --git a/components/http/okHttp/src/main/java/com/microsoft/kiota/http/middleware/options/HeadersInspectionOption.java b/components/http/okHttp/src/main/java/com/microsoft/kiota/http/middleware/options/HeadersInspectionOption.java new file mode 100644 index 000000000..f084072a5 --- /dev/null +++ b/components/http/okHttp/src/main/java/com/microsoft/kiota/http/middleware/options/HeadersInspectionOption.java @@ -0,0 +1,87 @@ +package com.microsoft.kiota.http.middleware.options; + +import jakarta.annotation.Nonnull; + +import com.microsoft.kiota.RequestHeaders; +import com.microsoft.kiota.RequestOption; +import com.microsoft.kiota.ResponseHeaders; + +/** + * The options to be passed to the headers inspection middleware. + */ +public class HeadersInspectionOption implements RequestOption { + private boolean inspectRequestHeaders; + /** + * Gets whether to inspect request headers + * @return Whether to inspect request headers + */ + public boolean getInspectRequestHeaders() { + return inspectRequestHeaders; + } + /** + * Sets whether to inspect request headers + * @param inspectRequestHeaders Whether to inspect request headers + */ + public void setInspectRequestHeaders(boolean inspectRequestHeaders) { + this.inspectRequestHeaders = inspectRequestHeaders; + } + + private boolean inspectResponseHeaders; + /** + * Gets whether to inspect response headers + * @return Whether to inspect response headers + */ + public boolean getInspectResponseHeaders() { + return inspectResponseHeaders; + } + /** + * Sets whether to inspect response headers + * @param inspectResponseHeaders Whether to inspect response headers + */ + public void setInspectResponseHeaders(boolean inspectResponseHeaders) { + this.inspectResponseHeaders = inspectResponseHeaders; + } + + private final RequestHeaders requestHeaders = new RequestHeaders(); + private final ResponseHeaders responseHeaders = new ResponseHeaders(); + /** + * Create default instance of headers inspection options, with default values of inspectRequestHeaders and inspectResponseHeaders. + */ + public HeadersInspectionOption() { + this(false, false); + } + /** + * Create an instance with provided values + * @param shouldInspectResponseHeaders Whether to inspect response headers + * @param shouldInspectRequestHeaders Whether to inspect request headers + */ + public HeadersInspectionOption(final boolean shouldInspectRequestHeaders, final boolean shouldInspectResponseHeaders) { + this.inspectResponseHeaders = shouldInspectResponseHeaders; + this.inspectRequestHeaders = shouldInspectRequestHeaders; + } + /** + * Get the request headers + * @return The request headers + */ + @Nonnull + public RequestHeaders getRequestHeaders() { + return this.requestHeaders; + } + /** + * Get the response headers + * @return The response headers + */ + @Nonnull + public ResponseHeaders getResponseHeaders() { + return this.responseHeaders; + } + + /** {@inheritDoc} */ + @SuppressWarnings("unchecked") + @Override + @Nonnull + public Class getType() { + return (Class) HeadersInspectionOption.class; + } + +} diff --git a/components/http/okHttp/src/main/java/com/microsoft/kiota/http/middleware/options/UserAgentHandlerOption.java b/components/http/okHttp/src/main/java/com/microsoft/kiota/http/middleware/options/UserAgentHandlerOption.java index a9e49801b..c59fd07a3 100644 --- a/components/http/okHttp/src/main/java/com/microsoft/kiota/http/middleware/options/UserAgentHandlerOption.java +++ b/components/http/okHttp/src/main/java/com/microsoft/kiota/http/middleware/options/UserAgentHandlerOption.java @@ -14,7 +14,7 @@ public UserAgentHandlerOption() { } @Nonnull private String productName = "kiota-java"; @Nonnull - private String productVersion = "0.6.0"; + private String productVersion = "0.7.0"; /** * Gets the product name to be used in the user agent header * @return the product name diff --git a/components/http/okHttp/src/test/java/com/microsoft/kiota/http/HeadersInspectionHandlerTest.java b/components/http/okHttp/src/test/java/com/microsoft/kiota/http/HeadersInspectionHandlerTest.java new file mode 100644 index 000000000..327371e6f --- /dev/null +++ b/components/http/okHttp/src/test/java/com/microsoft/kiota/http/HeadersInspectionHandlerTest.java @@ -0,0 +1,74 @@ +package com.microsoft.kiota.http; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import com.microsoft.kiota.http.middleware.HeadersInspectionHandler; +import com.microsoft.kiota.http.middleware.options.HeadersInspectionOption; + +import okhttp3.Headers; +import okhttp3.Interceptor.Chain; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; + +class HeadersInspectionHandlerTest { + private final Chain mockChain; + private final Response mockResponse; + public HeadersInspectionHandlerTest() throws IOException { + mockResponse = mock(Response.class); + when(mockResponse.code()).thenReturn(200); + when(mockResponse.message()).thenReturn("OK"); + when(mockResponse.body()).thenReturn(mock(ResponseBody.class)); + when(mockResponse.headers()).thenReturn(new Headers.Builder().add("test", "test").build()); + mockChain = mock(Chain.class); + when(mockChain.proceed(any(Request.class))).thenAnswer(new Answer() { + public Response answer(InvocationOnMock invocation) { + Object[] args = invocation.getArguments(); + Request request = (Request) args[0]; + when(mockResponse.request()).thenReturn(request); + return mockResponse; + } + }); + } + @Test + void instantiatesWithDefaults() { + final HeadersInspectionHandler handler = new HeadersInspectionHandler(); + assertNotNull(handler); + } + @Test + void getsRequestHeaders() throws IOException { + final HeadersInspectionOption option = new HeadersInspectionOption(true, false); + final HeadersInspectionHandler handler = new HeadersInspectionHandler(option); + final Request request = new Request.Builder().url("http://localhost").addHeader("test", "test").build(); + when(mockChain.request()).thenReturn(request); + handler.intercept(mockChain); + assertNotNull(option.getRequestHeaders()); + assertNotNull(option.getResponseHeaders()); + assertEquals(1, option.getRequestHeaders().size()); + assertEquals(0, option.getResponseHeaders().size()); + assertEquals("test", option.getRequestHeaders().get("test").toArray()[0]); + } + @Test + void getsResponseHeaders() throws IOException { + final HeadersInspectionOption option = new HeadersInspectionOption(false, true); + final HeadersInspectionHandler handler = new HeadersInspectionHandler(option); + final Request request = new Request.Builder().url("http://localhost").addHeader("test", "test").build(); + when(mockChain.request()).thenReturn(request); + handler.intercept(mockChain); + assertNotNull(option.getRequestHeaders()); + assertNotNull(option.getResponseHeaders()); + assertEquals(0, option.getRequestHeaders().size()); + assertEquals(1, option.getResponseHeaders().size()); + assertEquals("test", option.getResponseHeaders().get("test").toArray()[0]); + } +} diff --git a/components/http/okHttp/src/test/java/com/microsoft/kiota/http/UserAgentHandlerTest.java b/components/http/okHttp/src/test/java/com/microsoft/kiota/http/UserAgentHandlerTest.java index c57c01bec..bbbcbc510 100644 --- a/components/http/okHttp/src/test/java/com/microsoft/kiota/http/UserAgentHandlerTest.java +++ b/components/http/okHttp/src/test/java/com/microsoft/kiota/http/UserAgentHandlerTest.java @@ -22,7 +22,7 @@ import com.microsoft.kiota.http.middleware.UserAgentHandler; import com.microsoft.kiota.http.middleware.options.UserAgentHandlerOption; -public class UserAgentHandlerTest { +class UserAgentHandlerTest { private final Chain mockChain; private final Response mockResponse; public UserAgentHandlerTest() throws IOException { @@ -39,10 +39,10 @@ public Object answer(InvocationOnMock invocation) { when(mockResponse.request()).thenReturn(request); return mockResponse; } - }); + }); } @Test - public void addsTheProduct() throws IOException { + void addsTheProduct() throws IOException { final UserAgentHandler handler = new UserAgentHandler(); final Request request = new Request.Builder().url("http://localhost").build(); when(mockChain.request()).thenReturn(request); @@ -53,7 +53,7 @@ public void addsTheProduct() throws IOException { assertEquals("kiota-java", result.header("User-Agent").split("/")[0]); } @Test - public void addsTheProductOnce() throws IOException { + void addsTheProductOnce() throws IOException { final UserAgentHandler handler = new UserAgentHandler(); final Request request = new Request.Builder().url("http://localhost").build(); when(mockChain.request()).thenReturn(request); @@ -65,7 +65,7 @@ public void addsTheProductOnce() throws IOException { assertEquals(1, result.header("User-Agent").split("kiota-java").length - 1); } @Test - public void doesNotAddTheProductWhenDisabled() throws IOException { + void doesNotAddTheProductWhenDisabled() throws IOException { final UserAgentHandler handler = new UserAgentHandler(new UserAgentHandlerOption() {{ setEnabled(false); }}); final Request request = new Request.Builder().url("http://localhost").build(); when(mockChain.request()).thenReturn(request); diff --git a/gradle.properties b/gradle.properties index 6969c564c..ffbdc25e4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -25,7 +25,7 @@ org.gradle.caching=true mavenGroupId = com.microsoft.kiota mavenMajorVersion = 0 -mavenMinorVersion = 6 +mavenMinorVersion = 7 mavenPatchVersion = 0 mavenArtifactSuffix =