Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add RequestOptions & handling in RestProxy #22334

6 changes: 6 additions & 0 deletions sdk/core/azure-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,12 @@
<version>1.22</version> <!-- {x-version-update;org.openjdk.jmh:jmh-generator-annprocess;external_dependency} -->
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.json</groupId>
<artifactId>javax.json-api</artifactId>
<version>1.1.4</version> <!-- {x-version-update;javax.json:javax.json-api;external_dependency} -->
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.core.http.rest;

import com.azure.core.annotation.QueryParam;
import com.azure.core.http.HttpHeader;
import com.azure.core.http.HttpRequest;
import com.azure.core.util.BinaryData;

import java.util.function.Consumer;

/**
* This class contains the options to customize a HTTP request. {@link RequestOptions} can be
* used to configure the request headers, query params, the request body, or add a callback
* to modify all aspects of the HTTP request.
*
* <p>
* An instance of fully configured {@link RequestOptions} can be passed to a service method that
* preconfigures known components of the request like URL, path params etc, further modifying both
* un-configured, or preconfigured components.
* </p>
*
* <p>
* To demonstrate how this class can be used to construct a request, let's use a Pet Store service as an example. The
* list of APIs available on this service are <a href="https://petstore.swagger.io/#/pet">documented in the swagger definition.</a>
* </p>
*
* <p><strong>Creating an instance of RequestOptions</strong></p>
* {@codesnippet com.azure.core.http.rest.requestoptions.instantiation}
*
* <p><strong>Configuring the request with JSON body and making a HTTP POST request</strong></p>
* To <a href="https://petstore.swagger.io/#/pet/addPet">add a new pet to the pet store</a>, a HTTP POST call should
* be made to the service with the details of the pet that is to be added. The details of the pet are included as the
* request body in JSON format.
*
* The JSON structure for the request is defined as follows:
* <pre>{@code
* {
* "id": 0,
* "category": {
* "id": 0,
* "name": "string"
* },
* "name": "doggie",
* "photoUrls": [
* "string"
* ],
* "tags": [
* {
* "id": 0,
* "name": "string"
* }
* ],
* "status": "available"
* }
* }</pre>
*
* To create a concrete request, Json builder provided in javax package is used here for demonstration. However, any
* other Json building library can be used to achieve similar results.
*
* {@codesnippet com.azure.core.http.rest.requestoptions.createjsonrequest}
*
* Now, this string representation of the JSON request can be set as body of RequestOptions
*
* {@codesnippet com.azure.core.http.rest.requestoptions.postrequest}
*/
public final class RequestOptions {
private Consumer<HttpRequest> requestCallback = request -> { };
private boolean throwOnError = true;
private BinaryData requestBody;

/**
* Gets the request callback, applying all the configurations set on this RequestOptions.
* @return the request callback
*/
Consumer<HttpRequest> getRequestCallback() {
return this.requestCallback;
}

/**
* Gets whether or not to throw an exception when an HTTP response with a status code indicating an error
* (400 or above) is received.
*
* @return true if to throw on status codes of 400 or above, false if not. Default is true.
*/
boolean isThrowOnError() {
return this.throwOnError;
}

/**
* Adds a header to the HTTP request.
* @param header the header key
* @param value the header value
*
* @return the modified RequestOptions object
*/
public RequestOptions addHeader(String header, String value) {
this.requestCallback = this.requestCallback.andThen(request -> {
HttpHeader httpHeader = request.getHeaders().get(header);
if (httpHeader == null) {
request.getHeaders().set(header, value);
} else {
httpHeader.addValue(value);
}
});
return this;
}

/**
* Adds a query parameter to the request URL. The parameter name and value will be URL encoded.
* To use an already encoded parameter name and value, call {@code addQueryParam("name", "value", true)}.
*
* @param parameterName the name of the query parameter
* @param value the value of the query parameter
* @return the modified RequestOptions object
*/
public RequestOptions addQueryParam(String parameterName, String value) {
return addQueryParam(parameterName, value, false);
}

/**
* Adds a query parameter to the request URL, specifying whether the parameter is already encoded.
* A value true for this argument indicates that value of {@link QueryParam#value()} is already encoded
* hence engine should not encode it, by default value will be encoded.
*
* @param parameterName the name of the query parameter
* @param value the value of the query parameter
* @param encoded whether or not this query parameter is already encoded
* @return the modified RequestOptions object
*/
public RequestOptions addQueryParam(String parameterName, String value, boolean encoded) {
this.requestCallback = this.requestCallback.andThen(request -> {
String url = request.getUrl().toString();
String encodedParameterName = encoded ? parameterName : UrlEscapers.QUERY_ESCAPER.escape(parameterName);
String encodedParameterValue = encoded ? value : UrlEscapers.QUERY_ESCAPER.escape(value);
request.setUrl(url + (url.contains("?") ? "&" : "?") + encodedParameterName + "=" + encodedParameterValue);
});
return this;
}

/**
* Adds a custom request callback to modify the HTTP request before it's sent by the HttpClient.
* The modifications made on a RequestOptions object is applied in order on the request.
*
* @param requestCallback the request callback
* @return the modified RequestOptions object
*/
public RequestOptions addRequestCallback(Consumer<HttpRequest> requestCallback) {
this.requestCallback = this.requestCallback.andThen(requestCallback);
return this;
}

/**
* Sets the body to send as part of the HTTP request.
* @param requestBody the request body data
* @return the modified RequestOptions object
*/
public RequestOptions setBody(BinaryData requestBody) {
this.requestCallback = this.requestCallback.andThen(request -> {
request.setBody(requestBody.toBytes());
});
return this;
}

/**
* Sets whether or not to throw an exception when an HTTP response with a status code indicating an error
* (400 or above) is received. By default an exception will be thrown when an error response is received.
*
* @param throwOnError true if to throw on status codes of 400 or above, false if not. Default is true.
* @return the modified RequestOptions object
*/
public RequestOptions setThrowOnError(boolean throwOnError) {
this.throwOnError = throwOnError;
return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import com.azure.core.implementation.serializer.HttpResponseDecoder;
import com.azure.core.implementation.serializer.HttpResponseDecoder.HttpDecodedResponse;
import com.azure.core.util.Base64Url;
import com.azure.core.util.BinaryData;
import com.azure.core.util.Context;
import com.azure.core.util.FluxUtil;
import com.azure.core.util.UrlBuilder;
Expand Down Expand Up @@ -134,11 +135,17 @@ public Object invoke(Object proxy, final Method method, Object[] args) {
request.setBody(validateLength(request));
}

RequestOptions options = methodParser.setRequestOptions(args);
if (options != null) {
options.getRequestCallback().accept(request);
}

final Mono<HttpResponse> asyncResponse = send(request, context);

Mono<HttpDecodedResponse> asyncDecodedResponse = this.decoder.decode(asyncResponse, methodParser);

return handleRestReturnType(asyncDecodedResponse, methodParser, methodParser.getReturnType(), context);
return handleRestReturnType(asyncDecodedResponse, methodParser,
methodParser.getReturnType(), context, options);
} catch (IOException e) {
throw logger.logExceptionAsError(Exceptions.propagate(e));
}
Expand Down Expand Up @@ -286,7 +293,9 @@ private HttpRequest configRequest(final HttpRequest request, final SwaggerMethod
}
}

if (isJson) {
if (bodyContentObject instanceof BinaryData) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once it is merge this should use RequestContent. Right now, this is creating another copy of the underlying BinaryData data.

request.setBody(((BinaryData) bodyContentObject).toBytes());
} else if (isJson) {
ByteArrayOutputStream stream = new AccessibleByteArrayOutputStream();
serializer.serialize(bodyContentObject, SerializerEncoding.JSON, stream);

Expand Down Expand Up @@ -318,9 +327,9 @@ private HttpRequest configRequest(final HttpRequest request, final SwaggerMethod
}

private Mono<HttpDecodedResponse> ensureExpectedStatus(final Mono<HttpDecodedResponse> asyncDecodedResponse,
final SwaggerMethodParser methodParser) {
final SwaggerMethodParser methodParser, RequestOptions options) {
return asyncDecodedResponse
.flatMap(decodedHttpResponse -> ensureExpectedStatus(decodedHttpResponse, methodParser));
.flatMap(decodedHttpResponse -> ensureExpectedStatus(decodedHttpResponse, methodParser, options));
}

private static Exception instantiateUnexpectedException(final UnexpectedExceptionInformation exception,
Expand Down Expand Up @@ -365,10 +374,11 @@ private static Exception instantiateUnexpectedException(final UnexpectedExceptio
* @return An async-version of the provided decodedResponse.
*/
private Mono<HttpDecodedResponse> ensureExpectedStatus(final HttpDecodedResponse decodedResponse,
final SwaggerMethodParser methodParser) {
final SwaggerMethodParser methodParser, RequestOptions options) {
final int responseStatusCode = decodedResponse.getSourceResponse().getStatusCode();
final Mono<HttpDecodedResponse> asyncResult;
if (!methodParser.isExpectedResponseStatusCode(responseStatusCode)) {
if (!methodParser.isExpectedResponseStatusCode(responseStatusCode)
&& (options == null || options.isThrowOnError())) {
Mono<byte[]> bodyAsBytes = decodedResponse.getSourceResponse().getBodyAsByteArray();

asyncResult = bodyAsBytes.flatMap((Function<byte[], Mono<HttpDecodedResponse>>) responseContent -> {
Expand Down Expand Up @@ -471,6 +481,9 @@ private Mono<?> handleBodyReturnType(final HttpDecodedResponse response,
} else if (FluxUtil.isFluxByteBuffer(entityType)) {
// Mono<Flux<ByteBuffer>>
asyncResult = Mono.just(response.getSourceResponse().getBody());
} else if (TypeUtil.isTypeOrSubTypeOf(entityType, BinaryData.class)) {
// Mono<BinaryData>
asyncResult = BinaryData.fromFlux(response.getSourceResponse().getBody());
} else {
// Mono<Object> or Mono<Page<T>>
asyncResult = response.getDecodedBody((byte[]) null);
Expand All @@ -490,9 +503,10 @@ private Mono<?> handleBodyReturnType(final HttpDecodedResponse response,
private Object handleRestReturnType(final Mono<HttpDecodedResponse> asyncHttpDecodedResponse,
final SwaggerMethodParser methodParser,
final Type returnType,
final Context context) {
final Context context,
final RequestOptions options) {
final Mono<HttpDecodedResponse> asyncExpectedResponse =
ensureExpectedStatus(asyncHttpDecodedResponse, methodParser)
ensureExpectedStatus(asyncHttpDecodedResponse, methodParser, options)
.doOnEach(RestProxy::endTracingSpan)
.contextWrite(reactor.util.context.Context.of("TRACING_CONTEXT", context));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,16 @@ public Context setContext(Object[] swaggerMethodArguments) {
return (context != null) ? context : Context.NONE;
}

/**
* Get the {@link RequestOptions} passed into the proxy method.
*
* @param swaggerMethodArguments the arguments passed to the proxy method
* @return the request options
*/
public RequestOptions setRequestOptions(Object[] swaggerMethodArguments) {
return CoreUtils.findFirstOfType(swaggerMethodArguments, RequestOptions.class);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an ancillary performance question, does the Java proxy interface pass the Object[] in a consistent order? I'm wondering if there could be an optimization for this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AutoRest generated code does, but I'm not sure if azure-core could make the assumption that all code follows the same order.

However, a CoreUtils.findLastOfType() might slightly improve the perf.

}

/**
* Get whether or not the provided response status code is one of the expected status codes for this Swagger
* method.
Expand Down
1 change: 1 addition & 0 deletions sdk/core/azure-core/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
opens com.azure.core.implementation to com.fasterxml.jackson.databind;
opens com.azure.core.implementation.logging to com.fasterxml.jackson.databind;
opens com.azure.core.implementation.serializer to com.fasterxml.jackson.databind;
opens com.azure.core.http.rest to com.fasterxml.jackson.databind;

// Service Provider Interfaces
uses com.azure.core.http.HttpClientProvider;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.core.http.rest;

import com.azure.core.http.HttpMethod;
import com.azure.core.util.BinaryData;

import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonObject;

/**
* JavaDoc code snippets for {@link RequestOptions}.
*/
public class RequestOptionsJavaDocCodeSnippets {

/**
* Sample to demonstrate how to create an instance of {@link RequestOptions}.
* @return An instance of {@link RequestOptions}.
*/
public RequestOptions createInstance() {
// BEGIN: com.azure.core.http.rest.requestoptions.instantiation
RequestOptions options = new RequestOptions()
.setBody(BinaryData.fromString("{\"name\":\"Fluffy\"}"))
.addHeader("x-ms-pet-version", "2021-06-01");
// END: com.azure.core.http.rest.requestoptions.instantiation
return options;
}

/**
* Sample to demonstrate setting the JSON request body in a {@link RequestOptions}.
* @return An instance of {@link RequestOptions}.
*/
public RequestOptions setJsonRequestBodyInRequestOptions() {
// BEGIN: com.azure.core.http.rest.requestoptions.createjsonrequest
JsonArray photoUrls = Json.createArrayBuilder()
.add("https://imgur.com/pet1")
.add("https://imgur.com/pet2")
.build();

JsonArray tags = Json.createArrayBuilder()
.add(Json.createObjectBuilder()
.add("id", 0)
.add("name", "Labrador")
.build())
.add(Json.createObjectBuilder()
.add("id", 1)
.add("name", "2021")
.build())
.build();

JsonObject requestBody = Json.createObjectBuilder()
.add("id", 0)
.add("name", "foo")
.add("status", "available")
.add("category", Json.createObjectBuilder().add("id", 0).add("name", "dog"))
.add("photoUrls", photoUrls)
.add("tags", tags)
.build();

String requestBodyStr = requestBody.toString();
// END: com.azure.core.http.rest.requestoptions.createjsonrequest

// BEGIN: com.azure.core.http.rest.requestoptions.postrequest
RequestOptions options = new RequestOptions()
.addRequestCallback(request -> request
// may already be set if request is created from a client
.setUrl("https://petstore.example.com/pet")
.setHttpMethod(HttpMethod.POST)
.setBody(requestBodyStr)
.setHeader("Content-Type", "application/json"));
// END: com.azure.core.http.rest.requestoptions.postrequest
return options;
}
}
Loading