Skip to content

Commit

Permalink
Add RequestOptions & handling in RestProxy (#22334)
Browse files Browse the repository at this point in the history
* Add RequestOptions & handling in RestProxy

* Handle BinaryData request & response types

* Address some feedback

* Move RequestOptions

* Change ResponseStatusOption to boolean

* Fix issues from cr feedback

* Move javadoc samples

* Fix copy paste error in javadocs

* Fix swagger method parser test

* Fix test broken by encoding

* Fix checkstyle error

Co-authored-by: Jianghao Lu <[email protected]>
  • Loading branch information
jianghaolu and Jianghao Lu authored Jun 24, 2021
1 parent faf0204 commit d4f0b3c
Show file tree
Hide file tree
Showing 8 changed files with 406 additions and 8 deletions.
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) {
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);
}

/**
* 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

0 comments on commit d4f0b3c

Please sign in to comment.