com.microsoft.kiota.http.OkHttpRequestAdapter Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of microsoft-kiota-http-okHttp Show documentation
Show all versions of microsoft-kiota-http-okHttp Show documentation
Microsoft Kiota-Http with okHttp
package com.microsoft.kiota.http;
import static com.microsoft.kiota.http.TelemetrySemanticConventions.*;
import com.microsoft.kiota.*;
import com.microsoft.kiota.authentication.AuthenticationProvider;
import com.microsoft.kiota.http.middleware.ParametersNameDecodingHandler;
import com.microsoft.kiota.serialization.Parsable;
import com.microsoft.kiota.serialization.ParsableFactory;
import com.microsoft.kiota.serialization.ParseNode;
import com.microsoft.kiota.serialization.ParseNodeFactory;
import com.microsoft.kiota.serialization.ParseNodeFactoryRegistry;
import com.microsoft.kiota.serialization.SerializationWriterFactory;
import com.microsoft.kiota.serialization.SerializationWriterFactoryRegistry;
import com.microsoft.kiota.serialization.ValuedEnumParser;
import com.microsoft.kiota.store.BackingStoreFactory;
import com.microsoft.kiota.store.BackingStoreFactorySingleton;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import okhttp3.*;
import okio.BufferedSink;
import okio.Okio;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.net.URISyntaxException;
import java.net.URL;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** RequestAdapter implementation for OkHttp */
public class OkHttpRequestAdapter implements com.microsoft.kiota.RequestAdapter {
private static final String CONTENT_LENGTH_HEADER_KEY = "Content-Length";
private static final String CONTENT_TYPE_HEADER_KEY = "Content-Type";
@Nonnull private final Call.Factory client;
@Nonnull private final AuthenticationProvider authProvider;
@Nonnull private final ObservabilityOptions obsOptions;
@Nonnull private ParseNodeFactory pNodeFactory;
@Nonnull private SerializationWriterFactory sWriterFactory;
@Nonnull private String baseUrl = "";
public void setBaseUrl(@Nonnull final String baseUrl) {
this.baseUrl = Objects.requireNonNull(baseUrl);
}
@Nonnull public String getBaseUrl() {
return baseUrl;
}
/**
* Instantiates a new OkHttp request adapter with the provided authentication provider.
* @param authenticationProvider the authentication provider to use for authenticating requests.
*/
public OkHttpRequestAdapter(@Nonnull final AuthenticationProvider authenticationProvider) {
this(authenticationProvider, null, null, null, null);
}
/**
* Instantiates a new OkHttp request adapter with the provided authentication provider, and the parse node factory.
* @param authenticationProvider the authentication provider to use for authenticating requests.
* @param parseNodeFactory the parse node factory to use for parsing responses.
*/
@SuppressWarnings("LambdaLast")
public OkHttpRequestAdapter(
@Nonnull final AuthenticationProvider authenticationProvider,
@Nullable final ParseNodeFactory parseNodeFactory) {
this(authenticationProvider, parseNodeFactory, null, null, null);
}
/**
* Instantiates a new OkHttp request adapter with the provided authentication provider, parse node factory, and the serialization writer factory.
* @param authenticationProvider the authentication provider to use for authenticating requests.
* @param parseNodeFactory the parse node factory to use for parsing responses.
* @param serializationWriterFactory the serialization writer factory to use for serializing requests.
*/
@SuppressWarnings("LambdaLast")
public OkHttpRequestAdapter(
@Nonnull final AuthenticationProvider authenticationProvider,
@Nullable final ParseNodeFactory parseNodeFactory,
@Nullable final SerializationWriterFactory serializationWriterFactory) {
this(authenticationProvider, parseNodeFactory, serializationWriterFactory, null, null);
}
/**
* Instantiates a new OkHttp request adapter with the provided authentication provider, parse node factory, serialization writer factory, and the http client.
* @param authenticationProvider the authentication provider to use for authenticating requests.
* @param parseNodeFactory the parse node factory to use for parsing responses.
* @param serializationWriterFactory the serialization writer factory to use for serializing requests.
* @param client the http client to use for sending requests.
*/
@SuppressWarnings("LambdaLast")
public OkHttpRequestAdapter(
@Nonnull final AuthenticationProvider authenticationProvider,
@Nullable final ParseNodeFactory parseNodeFactory,
@Nullable final SerializationWriterFactory serializationWriterFactory,
@Nullable final Call.Factory client) {
this(authenticationProvider, parseNodeFactory, serializationWriterFactory, client, null);
}
/**
* Instantiates a new OkHttp request adapter with the provided authentication provider, parse node factory, serialization writer factory, http client and observability options.
* @param authenticationProvider the authentication provider to use for authenticating requests.
* @param parseNodeFactory the parse node factory to use for parsing responses.
* @param serializationWriterFactory the serialization writer factory to use for serializing requests.
* @param client the http client to use for sending requests.
* @param observabilityOptions the observability options to use for sending requests.
*/
@SuppressWarnings("LambdaLast")
public OkHttpRequestAdapter(
@Nonnull final AuthenticationProvider authenticationProvider,
@Nullable final ParseNodeFactory parseNodeFactory,
@Nullable final SerializationWriterFactory serializationWriterFactory,
@Nullable final Call.Factory client,
@Nullable final ObservabilityOptions observabilityOptions) {
this.authProvider =
Objects.requireNonNull(
authenticationProvider, "parameter authenticationProvider cannot be null");
if (client == null) {
this.client = KiotaClientFactory.create().build();
} else {
this.client = client;
}
if (parseNodeFactory == null) {
pNodeFactory = ParseNodeFactoryRegistry.defaultInstance;
} else {
pNodeFactory = parseNodeFactory;
}
if (serializationWriterFactory == null) {
sWriterFactory = SerializationWriterFactoryRegistry.defaultInstance;
} else {
sWriterFactory = serializationWriterFactory;
}
if (observabilityOptions == null) {
obsOptions = new ObservabilityOptions();
} else {
obsOptions = observabilityOptions;
}
}
@Nonnull public SerializationWriterFactory getSerializationWriterFactory() {
return sWriterFactory;
}
public void enableBackingStore(@Nullable final BackingStoreFactory backingStoreFactory) {
this.pNodeFactory =
Objects.requireNonNull(
ApiClientBuilder.enableBackingStoreForParseNodeFactory(pNodeFactory));
this.sWriterFactory =
Objects.requireNonNull(
ApiClientBuilder.enableBackingStoreForSerializationWriterFactory(
sWriterFactory));
if (backingStoreFactory != null) {
BackingStoreFactorySingleton.instance = backingStoreFactory;
}
}
private static final String nullRequestInfoParameter = "parameter requestInfo cannot be null";
private static final String nullEnumParserParameter = "parameter enumParser cannot be null";
private static final String nullFactoryParameter = "parameter factory cannot be null";
@Nullable public List sendCollection(
@Nonnull final RequestInformation requestInfo,
@Nullable final HashMap> errorMappings,
@Nonnull final ParsableFactory factory) {
Objects.requireNonNull(requestInfo, nullRequestInfoParameter);
Objects.requireNonNull(factory, nullFactoryParameter);
final Span span = startSpan(requestInfo, "sendCollection");
try (final Scope scope = span.makeCurrent()) {
Response response = this.getHttpResponseMessage(requestInfo, span, span, null);
final ResponseHandler responseHandler = getResponseHandler(requestInfo);
if (responseHandler == null) {
boolean closeResponse = true;
try {
this.throwIfFailedResponse(response, span, errorMappings);
if (this.shouldReturnNull(response)) {
return null;
}
final ParseNode rootNode = getRootParseNode(response, span, span);
if (rootNode == null) {
closeResponse = false;
return null;
}
final Span deserializationSpan =
GlobalOpenTelemetry.getTracer(obsOptions.getTracerInstrumentationName())
.spanBuilder("getCollectionOfObjectValues")
.startSpan();
try (final Scope deserializationScope = deserializationSpan.makeCurrent()) {
final List result =
rootNode.getCollectionOfObjectValues(factory);
setResponseType(result, span);
return result;
} finally {
deserializationSpan.end();
}
} finally {
closeResponse(closeResponse, response);
}
} else {
span.addEvent(eventResponseHandlerInvokedKey);
return responseHandler.handleResponse(response, errorMappings);
}
} finally {
span.end();
}
}
private ResponseHandler getResponseHandler(final RequestInformation requestInfo) {
final Collection requestOptions = requestInfo.getRequestOptions();
for (final RequestOption rOption : requestOptions) {
if (rOption instanceof ResponseHandlerOption) {
final ResponseHandlerOption option = (ResponseHandlerOption) rOption;
return option.getResponseHandler();
}
}
return null;
}
private static final Pattern queryParametersCleanupPattern =
Pattern.compile("\\{\\?[^\\}]+\\}", Pattern.CASE_INSENSITIVE);
private final char[] queryParametersToDecodeForTracing = {'-', '.', '~', '$'};
private Span startSpan(
@Nonnull final RequestInformation requestInfo, @Nonnull final String methodName) {
final String decodedUriTemplate =
ParametersNameDecodingHandler.decodeQueryParameters(
requestInfo.urlTemplate, queryParametersToDecodeForTracing);
final String cleanedUriTemplate =
queryParametersCleanupPattern.matcher(decodedUriTemplate).replaceAll("");
final Span span =
GlobalOpenTelemetry.getTracer(obsOptions.getTracerInstrumentationName())
.spanBuilder(methodName + " - " + cleanedUriTemplate)
.startSpan();
span.setAttribute("http.uri_template", decodedUriTemplate);
return span;
}
/** The key used for the event when a custom response handler is invoked. */
@Nonnull public static final String eventResponseHandlerInvokedKey =
"com.microsoft.kiota.response_handler_invoked";
@Nullable public ModelType send(
@Nonnull final RequestInformation requestInfo,
@Nullable final HashMap> errorMappings,
@Nonnull final ParsableFactory factory) {
Objects.requireNonNull(requestInfo, nullRequestInfoParameter);
Objects.requireNonNull(factory, nullFactoryParameter);
final Span span = startSpan(requestInfo, "send");
try (final Scope scope = span.makeCurrent()) {
Response response = this.getHttpResponseMessage(requestInfo, span, span, null);
final ResponseHandler responseHandler = getResponseHandler(requestInfo);
if (responseHandler == null) {
boolean closeResponse = true;
try {
this.throwIfFailedResponse(response, span, errorMappings);
if (this.shouldReturnNull(response)) {
return null;
}
final ParseNode rootNode = getRootParseNode(response, span, span);
if (rootNode == null) {
closeResponse = false;
return null;
}
final Span deserializationSpan =
GlobalOpenTelemetry.getTracer(obsOptions.getTracerInstrumentationName())
.spanBuilder("getObjectValue")
.setParent(Context.current().with(span))
.startSpan();
try (final Scope deserializationScope = deserializationSpan.makeCurrent()) {
final ModelType result = rootNode.getObjectValue(factory);
setResponseType(result, span);
return result;
} finally {
deserializationSpan.end();
}
} finally {
closeResponse(closeResponse, response);
}
} else {
span.addEvent(eventResponseHandlerInvokedKey);
return responseHandler.handleResponse(response, errorMappings);
}
} finally {
span.end();
}
}
private void setResponseType(final Object result, final Span span) {
if (result != null) {
span.setAttribute("com.microsoft.kiota.response.type", result.getClass().getName());
}
}
private void closeResponse(boolean closeResponse, Response response) {
if (closeResponse && response.code() != 204) {
response.close();
}
}
@Nonnull private String getMediaTypeAndSubType(@Nonnull final MediaType mediaType) {
return mediaType.type() + "/" + mediaType.subtype();
}
@Nullable public ModelType sendPrimitive(
@Nonnull final RequestInformation requestInfo,
@Nullable final HashMap> errorMappings,
@Nonnull final Class targetClass) {
Objects.requireNonNull(requestInfo, nullRequestInfoParameter);
Objects.requireNonNull(targetClass, "parameter targetClass cannot be null");
final Span span = startSpan(requestInfo, "sendPrimitive");
try (final Scope scope = span.makeCurrent()) {
Response response = this.getHttpResponseMessage(requestInfo, span, span, null);
final ResponseHandler responseHandler = getResponseHandler(requestInfo);
if (responseHandler == null) {
boolean closeResponse = true;
try {
this.throwIfFailedResponse(response, span, errorMappings);
if (this.shouldReturnNull(response)) {
return null;
}
if (targetClass == Void.class) {
return null;
} else {
if (targetClass == InputStream.class) {
closeResponse = false;
final ResponseBody body = response.body();
if (body == null) {
return null;
}
final InputStream rawInputStream = body.byteStream();
return (ModelType) rawInputStream;
}
final ParseNode rootNode = getRootParseNode(response, span, span);
if (rootNode == null) {
closeResponse = false;
return null;
}
final Span deserializationSpan =
GlobalOpenTelemetry.getTracer(
obsOptions.getTracerInstrumentationName())
.spanBuilder("get" + targetClass.getName() + "Value")
.setParent(Context.current().with(span))
.startSpan();
try (final Scope deserializationScope = deserializationSpan.makeCurrent()) {
Object result;
if (targetClass == Boolean.class) {
result = rootNode.getBooleanValue();
} else if (targetClass == Byte.class) {
result = rootNode.getByteValue();
} else if (targetClass == String.class) {
result = rootNode.getStringValue();
} else if (targetClass == Short.class) {
result = rootNode.getShortValue();
} else if (targetClass == BigDecimal.class) {
result = rootNode.getBigDecimalValue();
} else if (targetClass == Double.class) {
result = rootNode.getDoubleValue();
} else if (targetClass == Integer.class) {
result = rootNode.getIntegerValue();
} else if (targetClass == Float.class) {
result = rootNode.getFloatValue();
} else if (targetClass == Long.class) {
result = rootNode.getLongValue();
} else if (targetClass == UUID.class) {
result = rootNode.getUUIDValue();
} else if (targetClass == OffsetDateTime.class) {
result = rootNode.getOffsetDateTimeValue();
} else if (targetClass == LocalDate.class) {
result = rootNode.getLocalDateValue();
} else if (targetClass == LocalTime.class) {
result = rootNode.getLocalTimeValue();
} else if (targetClass == PeriodAndDuration.class) {
result = rootNode.getPeriodAndDurationValue();
} else if (targetClass == byte[].class) {
result = rootNode.getByteArrayValue();
} else {
throw new RuntimeException(
"unexpected payload type " + targetClass.getName());
}
setResponseType(result, span);
return (ModelType) result;
} finally {
deserializationSpan.end();
}
}
} finally {
closeResponse(closeResponse, response);
}
} else {
span.addEvent(eventResponseHandlerInvokedKey);
return responseHandler.handleResponse(response, errorMappings);
}
} finally {
span.end();
}
}
@Nullable public > ModelType sendEnum(
@Nonnull final RequestInformation requestInfo,
@Nullable final HashMap> errorMappings,
@Nonnull final ValuedEnumParser enumParser) {
Objects.requireNonNull(requestInfo, nullRequestInfoParameter);
Objects.requireNonNull(enumParser, nullEnumParserParameter);
final Span span = startSpan(requestInfo, "sendEnum");
try (final Scope scope = span.makeCurrent()) {
Response response = this.getHttpResponseMessage(requestInfo, span, span, null);
final ResponseHandler responseHandler = getResponseHandler(requestInfo);
if (responseHandler == null) {
boolean closeResponse = true;
try {
this.throwIfFailedResponse(response, span, errorMappings);
if (this.shouldReturnNull(response)) {
return null;
}
final ParseNode rootNode = getRootParseNode(response, span, span);
if (rootNode == null) {
closeResponse = false;
return null;
}
final Span deserializationSpan =
GlobalOpenTelemetry.getTracer(obsOptions.getTracerInstrumentationName())
.spanBuilder("getEnumValue")
.setParent(Context.current().with(span))
.startSpan();
try (final Scope deserializationScope = deserializationSpan.makeCurrent()) {
final Object result = rootNode.getEnumValue(enumParser::forValue);
setResponseType(result, span);
return (ModelType) result;
} finally {
deserializationSpan.end();
}
} finally {
closeResponse(closeResponse, response);
}
} else {
span.addEvent(eventResponseHandlerInvokedKey);
return responseHandler.handleResponse(response, errorMappings);
}
} finally {
span.end();
}
}
@Nullable public > List sendEnumCollection(
@Nonnull final RequestInformation requestInfo,
@Nullable final HashMap> errorMappings,
@Nonnull final ValuedEnumParser enumParser) {
Objects.requireNonNull(requestInfo, nullRequestInfoParameter);
Objects.requireNonNull(enumParser, nullEnumParserParameter);
final Span span = startSpan(requestInfo, "sendEnumCollection");
try (final Scope scope = span.makeCurrent()) {
Response response = this.getHttpResponseMessage(requestInfo, span, span, null);
final ResponseHandler responseHandler = getResponseHandler(requestInfo);
if (responseHandler == null) {
boolean closeResponse = true;
try {
this.throwIfFailedResponse(response, span, errorMappings);
if (this.shouldReturnNull(response)) {
return null;
}
final ParseNode rootNode = getRootParseNode(response, span, span);
if (rootNode == null) {
closeResponse = false;
return null;
}
final Span deserializationSpan =
GlobalOpenTelemetry.getTracer(obsOptions.getTracerInstrumentationName())
.spanBuilder("getCollectionOfEnumValues")
.setParent(Context.current().with(span))
.startSpan();
try (final Scope deserializationScope = deserializationSpan.makeCurrent()) {
final Object result =
rootNode.getCollectionOfEnumValues(enumParser::forValue);
setResponseType(result, span);
return (List) result;
} finally {
deserializationSpan.end();
}
} finally {
closeResponse(closeResponse, response);
}
} else {
span.addEvent(eventResponseHandlerInvokedKey);
return responseHandler.handleResponse(response, errorMappings);
}
} finally {
span.end();
}
}
@Nullable public List sendPrimitiveCollection(
@Nonnull final RequestInformation requestInfo,
@Nullable final HashMap> errorMappings,
@Nonnull final Class targetClass) {
Objects.requireNonNull(requestInfo, nullRequestInfoParameter);
final Span span = startSpan(requestInfo, "sendPrimitiveCollection");
try (final Scope scope = span.makeCurrent()) {
Response response = getHttpResponseMessage(requestInfo, span, span, null);
final ResponseHandler responseHandler = getResponseHandler(requestInfo);
if (responseHandler == null) {
boolean closeResponse = true;
try {
this.throwIfFailedResponse(response, span, errorMappings);
if (this.shouldReturnNull(response)) {
return null;
}
final ParseNode rootNode = getRootParseNode(response, span, span);
if (rootNode == null) {
closeResponse = false;
return null;
}
final Span deserializationSpan =
GlobalOpenTelemetry.getTracer(obsOptions.getTracerInstrumentationName())
.spanBuilder("getCollectionOfPrimitiveValues")
.setParent(Context.current().with(span))
.startSpan();
try (final Scope deserializationScope = deserializationSpan.makeCurrent()) {
final List result =
rootNode.getCollectionOfPrimitiveValues(targetClass);
setResponseType(result, span);
return result;
} finally {
deserializationSpan.end();
}
} finally {
closeResponse(closeResponse, response);
}
} else {
span.addEvent(eventResponseHandlerInvokedKey);
return responseHandler.handleResponse(response, errorMappings);
}
} finally {
span.end();
}
}
@Nullable private ParseNode getRootParseNode(
final Response response, final Span parentSpan, final Span spanForAttributes) {
final Span span =
GlobalOpenTelemetry.getTracer(obsOptions.getTracerInstrumentationName())
.spanBuilder("getRootParseNode")
.setParent(Context.current().with(parentSpan))
.startSpan();
try (final Scope scope = span.makeCurrent()) {
final ResponseBody body =
response.body(); // closing the response closes the body and stream
// https://square.github.io/okhttp/4.x/okhttp/okhttp3/-response-body/
if (body == null) {
return null;
}
final InputStream rawInputStream = body.byteStream();
final MediaType contentType = body.contentType();
if (contentType == null) {
return null;
}
return pNodeFactory.getParseNode(getMediaTypeAndSubType(contentType), rawInputStream);
} finally {
span.end();
}
}
private boolean shouldReturnNull(final Response response) {
final int statusCode = response.code();
return statusCode == 204;
}
/** key used for the attribute when the error response has models mappings provided */
@Nonnull public static final String errorMappingFoundAttributeName =
"com.microsoft.kiota.error_mapping_found";
/** Key used for the attribute when an error response body is found */
@Nonnull public static final String errorBodyFoundAttributeName = "com.microsoft.kiota.error_body_found";
private Response throwIfFailedResponse(
@Nonnull final Response response,
@Nonnull final Span spanForAttributes,
@Nullable final HashMap> errorMappings) {
final Span span =
GlobalOpenTelemetry.getTracer(obsOptions.getTracerInstrumentationName())
.spanBuilder("throwIfFailedResponse")
.setParent(Context.current().with(spanForAttributes))
.startSpan();
try (final Scope scope = span.makeCurrent()) {
if (response.isSuccessful()) return response;
spanForAttributes.setStatus(StatusCode.ERROR);
final String statusCodeAsString = Integer.toString(response.code());
final int statusCode = response.code();
final ResponseHeaders responseHeaders =
HeadersCompatibility.getResponseHeaders(response.headers());
if (errorMappings == null
|| !errorMappings.containsKey(statusCodeAsString)
&& !(statusCode >= 400
&& statusCode < 500
&& errorMappings.containsKey("4XX"))
&& !(statusCode >= 500
&& statusCode < 600
&& errorMappings.containsKey("5XX"))
&& !errorMappings.containsKey("XXX")) {
spanForAttributes.setAttribute(errorMappingFoundAttributeName, false);
final ApiException result =
new ApiExceptionBuilder()
.withMessage(
"the server returned an unexpected status code and no error"
+ " class is registered for this code "
+ statusCode)
.withResponseStatusCode(statusCode)
.withResponseHeaders(responseHeaders)
.build();
spanForAttributes.recordException(result);
throw result;
}
spanForAttributes.setAttribute(errorMappingFoundAttributeName, true);
final ParsableFactory extends Parsable> errorClass =
errorMappings.containsKey(statusCodeAsString)
? errorMappings.get(statusCodeAsString)
: (statusCode >= 400 && statusCode < 500
? errorMappings.getOrDefault("4XX", errorMappings.get("XXX"))
: errorMappings.getOrDefault("5XX", errorMappings.get("XXX")));
boolean closeResponse = true;
try {
final ParseNode rootNode = getRootParseNode(response, span, span);
if (rootNode == null) {
spanForAttributes.setAttribute(errorBodyFoundAttributeName, false);
closeResponse = false;
final ApiException result =
new ApiExceptionBuilder()
.withMessage(
"service returned status code"
+ statusCode
+ " but no response body was found")
.withResponseStatusCode(statusCode)
.withResponseHeaders(responseHeaders)
.build();
spanForAttributes.recordException(result);
throw result;
}
spanForAttributes.setAttribute(errorBodyFoundAttributeName, true);
final Span deserializationSpan =
GlobalOpenTelemetry.getTracer(obsOptions.getTracerInstrumentationName())
.spanBuilder("getObjectValue")
.setParent(Context.current().with(span))
.startSpan();
try (final Scope deserializationScope = deserializationSpan.makeCurrent()) {
ApiException result =
new ApiExceptionBuilder(() -> rootNode.getObjectValue(errorClass))
.withResponseStatusCode(statusCode)
.withResponseHeaders(responseHeaders)
.build();
spanForAttributes.recordException(result);
throw result;
} finally {
deserializationSpan.end();
}
} finally {
closeResponse(closeResponse, response);
}
} finally {
span.end();
}
}
private static final String claimsKey = "claims";
private Response getHttpResponseMessage(
@Nonnull final RequestInformation requestInfo,
@Nonnull final Span parentSpan,
@Nonnull final Span spanForAttributes,
@Nullable final String claims) {
Objects.requireNonNull(requestInfo, nullRequestInfoParameter);
final Span span =
GlobalOpenTelemetry.getTracer(obsOptions.getTracerInstrumentationName())
.spanBuilder("getHttpResponseMessage")
.setParent(Context.current().with(parentSpan))
.startSpan();
try (final Scope scope = span.makeCurrent()) {
this.setBaseUrlForRequestInformation(requestInfo);
final Map additionalContext = new HashMap();
additionalContext.put("parent-span", span);
if (claims != null && !claims.isEmpty()) {
additionalContext.put(claimsKey, claims);
}
this.authProvider.authenticateRequest(requestInfo, additionalContext);
final Response response =
this.client
.newCall(
getRequestFromRequestInformation(
requestInfo, span, spanForAttributes))
.execute();
final String contentLengthHeaderValue =
getHeaderValue(response, CONTENT_LENGTH_HEADER_KEY);
if (contentLengthHeaderValue != null && !contentLengthHeaderValue.isEmpty()) {
final long contentLengthHeaderValueAsLong =
Long.parseLong(contentLengthHeaderValue);
spanForAttributes.setAttribute(
EXPERIMENTAL_HTTP_RESPONSE_BODY_SIZE, contentLengthHeaderValueAsLong);
}
final String contentTypeHeaderValue = getHeaderValue(response, CONTENT_TYPE_HEADER_KEY);
if (contentTypeHeaderValue != null && !contentTypeHeaderValue.isEmpty()) {
spanForAttributes.setAttribute(
CUSTOM_HTTP_RESPONSE_CONTENT_TYPE, contentTypeHeaderValue);
}
spanForAttributes.setAttribute(HTTP_RESPONSE_STATUS_CODE, response.code());
spanForAttributes.setAttribute(
NETWORK_PROTOCOL_VERSION,
response.protocol().toString().toUpperCase(Locale.ROOT));
return this.retryCAEResponseIfRequired(
response, requestInfo, span, spanForAttributes, claims);
} catch (IOException | URISyntaxException ex) {
spanForAttributes.recordException(ex);
throw new RuntimeException(ex);
} finally {
span.end();
}
}
private String getHeaderValue(final Response response, String key) {
final List headerValue = response.headers().values(key);
if (headerValue != null && headerValue.size() > 0) {
final String firstEntryValue = headerValue.get(0);
if (firstEntryValue != null && !firstEntryValue.isEmpty()) {
return firstEntryValue;
}
}
return null;
}
private static final Pattern bearerPattern =
Pattern.compile("^Bearer\\s.*", Pattern.CASE_INSENSITIVE);
private static final Pattern claimsPattern =
Pattern.compile("\\s?claims=\"([^\"]+)\"", Pattern.CASE_INSENSITIVE);
/** Key used for events when an authentication challenge is returned by the API */
@Nonnull public static final String authenticateChallengedEventKey =
"com.microsoft.kiota.authenticate_challenge_received";
private Response retryCAEResponseIfRequired(
@Nonnull final Response response,
@Nonnull final RequestInformation requestInfo,
@Nonnull final Span parentSpan,
@Nonnull final Span spanForAttributes,
@Nullable final String claims) {
final Span span =
GlobalOpenTelemetry.getTracer(obsOptions.getTracerInstrumentationName())
.spanBuilder("retryCAEResponseIfRequired")
.setParent(Context.current().with(parentSpan))
.startSpan();
try (final Scope scope = span.makeCurrent()) {
final String responseClaims = this.getClaimsFromResponse(response, requestInfo, claims);
if (responseClaims != null && !responseClaims.isEmpty()) {
if (requestInfo.content != null && requestInfo.content.markSupported()) {
try {
requestInfo.content.reset();
} catch (IOException ex) {
spanForAttributes.recordException(ex);
throw new RuntimeException(ex);
}
}
closeResponse(true, response);
span.addEvent(authenticateChallengedEventKey);
spanForAttributes.setAttribute(HTTP_REQUEST_RESEND_COUNT, 1);
return this.getHttpResponseMessage(
requestInfo, span, spanForAttributes, responseClaims);
}
return response;
} finally {
span.end();
}
}
String getClaimsFromResponse(
@Nonnull final Response response,
@Nonnull final RequestInformation requestInfo,
@Nullable final String claims) {
if (response.code() == 401
&& (claims == null || claims.isEmpty())
&& // we avoid infinite loops and retry only once
(requestInfo.content == null || requestInfo.content.markSupported())) {
final List authenticateHeader = response.headers("WWW-Authenticate");
if (!authenticateHeader.isEmpty()) {
String rawHeaderValue = null;
for (final String authenticateEntry : authenticateHeader) {
final Matcher matcher = bearerPattern.matcher(authenticateEntry);
if (matcher.matches()) {
rawHeaderValue = authenticateEntry.replaceFirst("^Bearer\\s", "");
break;
}
}
if (rawHeaderValue != null) {
final String[] parameters = rawHeaderValue.split(",");
for (final String parameter : parameters) {
final Matcher matcher = claimsPattern.matcher(parameter);
if (matcher.matches()) {
return matcher.group(1);
}
}
}
}
}
return null;
}
private void setBaseUrlForRequestInformation(@Nonnull final RequestInformation requestInfo) {
Objects.requireNonNull(requestInfo);
requestInfo.pathParameters.put("baseurl", getBaseUrl());
}
/** {@inheritDoc} */
@SuppressWarnings("unchecked")
@Nonnull public T convertToNativeRequest(@Nonnull final RequestInformation requestInfo) {
Objects.requireNonNull(requestInfo, nullRequestInfoParameter);
final Span span = startSpan(requestInfo, "convertToNativeRequest");
try (final Scope scope = span.makeCurrent()) {
this.authProvider.authenticateRequest(requestInfo, null);
return (T) getRequestFromRequestInformation(requestInfo, span, span);
} catch (URISyntaxException | IOException ex) {
span.recordException(ex);
throw new RuntimeException(ex);
} finally {
span.end();
}
}
/**
* Creates a new request from the request information instance.
*
* @param requestInfo request information instance.
* @param parentSpan the parent span for telemetry.
* @param spanForAttributes the span for the attributes.
* @return the created request instance.
* @throws URISyntaxException if the URI is invalid.
* @throws IOException if the URL is invalid.
*/
protected @Nonnull Request getRequestFromRequestInformation(
@Nonnull final RequestInformation requestInfo,
@Nonnull final Span parentSpan,
@Nonnull final Span spanForAttributes)
throws URISyntaxException, IOException {
final Span span =
GlobalOpenTelemetry.getTracer(obsOptions.getTracerInstrumentationName())
.spanBuilder("getRequestFromRequestInformation")
.setParent(Context.current().with(parentSpan))
.startSpan();
try (final Scope scope = span.makeCurrent()) {
spanForAttributes.setAttribute(HTTP_REQUEST_METHOD, requestInfo.httpMethod.toString());
final URL requestURL = requestInfo.getUri().toURL();
if (obsOptions.getIncludeEUIIAttributes()) {
spanForAttributes.setAttribute(URL_FULL, requestURL.toString());
}
spanForAttributes.setAttribute(SERVER_PORT, requestURL.getPort());
spanForAttributes.setAttribute(SERVER_ADDRESS, requestURL.getHost());
spanForAttributes.setAttribute(URL_SCHEME, requestURL.getProtocol());
RequestBody body =
requestInfo.content == null
? null
: new RequestBody() {
@Override
public MediaType contentType() {
final Set contentTypes =
requestInfo.headers.getOrDefault(
CONTENT_TYPE_HEADER_KEY, new HashSet<>());
if (contentTypes.isEmpty()) {
return null;
} else {
final String contentType =
contentTypes.toArray(new String[] {})[0];
spanForAttributes.setAttribute(
CUSTOM_HTTP_REQUEST_CONTENT_TYPE, contentType);
return MediaType.parse(contentType);
}
}
@Override
public long contentLength() throws IOException {
final Set contentLength =
requestInfo.headers.getOrDefault(
CONTENT_LENGTH_HEADER_KEY, new HashSet<>());
if (!contentLength.isEmpty()) {
return Long.parseLong(
contentLength.toArray(new String[] {})[0]);
}
// super.contentLength() is not relied on since it defaults to
// -1L, causing wrong telemetry added to the attributes.
if (requestInfo.content instanceof ByteArrayInputStream) {
final ByteArrayInputStream contentStream =
(ByteArrayInputStream) requestInfo.content;
// using available() on a byte-array backed input stream is
// reliable because array size is defined.
return contentStream.available();
}
return super.contentLength();
}
@Override
public void writeTo(@Nonnull BufferedSink sink) throws IOException {
sink.writeAll(Okio.source(requestInfo.content));
}
};
// https://stackoverflow.com/a/35743536
if (body == null
&& (requestInfo.httpMethod.equals(HttpMethod.POST)
|| requestInfo.httpMethod.equals(HttpMethod.PATCH)
|| requestInfo.httpMethod.equals(HttpMethod.PUT))) {
body = RequestBody.create(new byte[0]);
}
final Request.Builder requestBuilder =
new Request.Builder()
.url(requestURL)
.method(requestInfo.httpMethod.toString(), body);
for (final Map.Entry> headerEntry :
requestInfo.headers.entrySet()) {
for (final String headerValue : headerEntry.getValue()) {
requestBuilder.addHeader(headerEntry.getKey(), headerValue);
}
}
boolean obsOptionsPresent = false;
for (final RequestOption option : requestInfo.getRequestOptions()) {
if (option.getType() == obsOptions.getType()) {
obsOptionsPresent = true;
}
requestBuilder.tag(option.getType(), option);
}
if (!obsOptionsPresent) {
requestBuilder.tag(obsOptions.getType(), obsOptions);
}
requestBuilder.tag(Span.class, parentSpan);
final Request request = requestBuilder.build();
if (request != null) {
RequestBody requestBody = request.body();
if (requestBody != null) {
long contentLength = requestBody.contentLength();
if (contentLength >= 0) {
spanForAttributes.setAttribute(
EXPERIMENTAL_HTTP_REQUEST_BODY_SIZE, contentLength);
}
}
}
return request;
} finally {
span.end();
}
}
}